Capítulo 16: Testes Automatizados com @SpringBootTest, MockMvc e Testcontainers
Especialização em Backend com Java & Spring Boot • Spring Boot 3 & Java 21 LTS • Spring Data JPA, Security, Microsserviços e Cloud
🗺️ Mapa Conceitual do Tópico
flowchart TD
A["Cliente HTTP / Frontend"] --> B["API Gateway / Router"]
B --> C["Controller / Handler"]
C --> D["Service Layer (Regras de Negócio)"]
D --> E["Repository / ORM (Persistência)"]
E --> F["Banco de Dados / Cache"]
subgraph ARQ["Arquitetura do Capítulo"]
G["Conceito: Testes Automatizados com @SpringBootTest, MockMvc e Testcontainers"]
H["Segurança, Validação e Resiliência"]
I["Alta Performance e Escalabilidade"]
end
D --> ARQ
style A fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px
style B fill:#fff3e0,stroke:#ff9800,stroke-width:2px
style C fill:#ede7f6,stroke:#7e57c2,stroke-width:2px
style D fill:#e8f5e9,stroke:#4caf50,stroke-width:2px
style E fill:#fce4ec,stroke:#e91e63,stroke-width:2px
style F fill:#f3e5f5,stroke:#9c27b0,stroke-width:2px
🏛️ 1. Fundamentos Técnicos de Testes Automatizados com @SpringBootTest, MockMvc e Testcontainers
A pirâmide de testes no Spring Boot. Nem todo teste deve subir o contexto completo da aplicação — isso é lento e mascara qual camada realmente quebrou. O Spring Boot oferece anotações de “fatia” (slice tests) que carregam apenas o subconjunto de beans necessário para testar uma camada isoladamente: @WebMvcTest(MeuController.class) sobe só a infraestrutura web (controllers, filtros, conversores JSON, validação de @Valid), sem tocar banco de dados nem camada de serviço real — os colaboradores (Service, Repository) precisam ser substituídos por @MockBean, que registra um mock Mockito no contexto Spring no lugar do bean real. Isso torna o teste rápido (milissegundos) e focado: se ele falhar, o problema está no controller, não em uma dependência.
MockMvc: simular requisições HTTP sem servidor real. Injetado automaticamente por @WebMvcTest ou @AutoConfigureMockMvc, o MockMvc invoca o DispatcherServlet diretamente em memória — sem abrir socket TCP nem porta de rede — mas passando por todo o pipeline real de filtros, serialização e validação. mockMvc.perform(get("/api/v1/produtos/1")).andExpect(status().isOk()).andExpect(jsonPath("$.nome").value("Notebook")) executa a requisição simulada e valida tanto o código de status quanto campos específicos do corpo JSON via jsonPath(), sem precisar desserializar manualmente a resposta.
@SpringBootTest: quando o contexto completo é necessário. Diferente do @WebMvcTest, @SpringBootTest sobe o ApplicationContext inteiro — todos os beans reais, incluindo repositórios JPA e configurações de segurança — simulando o comportamento de produção com fidelidade máxima. É o teste certo para validar a integração real entre camadas (ex.: uma regra de negócio que depende de uma consulta JPQL específica), mas é ordens de magnitude mais lento que um slice test: a estratégia correta é usar @WebMvcTest/@DataJpaTest para a maioria dos casos e reservar @SpringBootTest para poucos testes de integração de ponta a ponta por fluxo crítico.
Mocks com @MockBean e Mockito.when(). @MockBean substitui o bean real no contexto Spring por um dublê controlado pelo teste. Mockito.when(servico.metodo(argumento)).thenReturn(valorEsperado) define o comportamento simulado; sem essa configuração explícita, o mock retorna null (ou o valor padrão do tipo) para qualquer chamada — por isso testes com mock mal configurado frequentemente falham com NullPointerException em vez de mensagem clara. Mockito.verify(servico).metodo(argumento) confirma que a interação esperada de fato ocorreu, essencial para testar efeitos colaterais (ex.: “o serviço de e-mail foi chamado exatamente uma vez”).
Testcontainers: eliminar a discrepância H2 vs. banco real. Um erro clássico é testar contra H2 em memória (por ser rápido de subir) enquanto a produção roda PostgreSQL — dialetos SQL, tipos de dados e comportamento de constraints divergem o suficiente para mascarar bugs reais. A biblioteca Testcontainers sobe um contêiner Docker real e efêmero (PostgreSQLContainer<>("postgres:16-alpine")) automaticamente antes da suíte e o destrói ao final, com @Testcontainers + @Container static gerenciando o ciclo de vida — o teste passa a rodar contra o mesmo motor de banco da produção, com fidelidade total, ao custo de alguns segundos extras de inicialização do contêiner.
Testes de contrato (Contract Testing). Em uma arquitetura de microsserviços, testes de integração ponta a ponta entre times são caros e frágeis de manter. Testes de contrato (Spring Cloud Contract, Pact) invertem o problema: o time consumidor declara as expectativas de resposta de um endpoint, e essas expectativas geram stubs automáticos (via WireMock) usados nos testes do consumidor — enquanto o time produtor roda os mesmos contratos contra sua implementação real, garantindo que qualquer mudança que quebre o contrato seja detectada no CI do produtor, antes de chegar em produção e quebrar o consumidor silenciosamente.
💻 2. Código de Demonstração Corporativo
package com.empresa.api.controller;
import com.empresa.api.service.ProdutoService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.web.servlet.MockMvc;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@WebMvcTest(ProdutoController.class)
class ProdutoControllerTest {
@Autowired private MockMvc mockMvc;
@MockBean private ProdutoService produtoService; // substitui o service real: nenhum acesso a banco
@Test
void deveRetornarProdutoComStatus200EJsonCorreto() throws Exception {
when(produtoService.buscarPorId(1L))
.thenReturn(new ProdutoDTO(1L, "Notebook", new java.math.BigDecimal("4500.00")));
mockMvc.perform(get("/api/v1/produtos/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.nome").value("Notebook"))
.andExpect(jsonPath("$.preco").value(4500.00));
}
@Test
void deveRetornar404QuandoProdutoNaoExiste() throws Exception {
when(produtoService.buscarPorId(99L))
.thenThrow(new ProdutoNaoEncontradoException(99L));
mockMvc.perform(get("/api/v1/produtos/99"))
.andExpect(status().isNotFound());
}
}
package com.empresa.api;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
@SpringBootTest
@Testcontainers
class PedidoRepositoryIntegrationTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine");
// Injeta dinamicamente a URL/credenciais do contêiner efêmero nas propriedades do Spring.
@DynamicPropertySource
static void configurarDatasource(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
}
@Test
void contextoCarregaComBancoPostgresRealEfemero() {
System.out.println("Container Postgres rodando na porta: " + postgres.getFirstMappedPort());
}
}
🔗 Recursos Pedagógicos do Capítulo 16
| Recurso Didático | Finalidade | Link de Acesso |
|---|---|---|
| 📊 Slides de Aula | Apresentação visual interativa com Dark Mode e suporte a teclado | Ver Slides |
| 🧠 Quiz Formativo | Teste interativo de fixação com feedback imediato por alternativa | Fazer Quiz |
| 💻 Exemplos de Código | Demonstrações funcionais com código executável | Ver Exemplos |
| 🧩 Exercícios em 4 Níveis | Lista progressiva de fixação com gabarito em bloco colapsável | Resolver Exercícios |
| ⬅️ Capítulo Anterior | 📚 Sumário de Tópicos | Próximo Capítulo ➡️ |