404 Not Found

404 Not Found


nginx

Estratégia de Testes

Testes são uma garantia de qualidade—escrever código sem testes é como dirigir sem cinto de segurança; cedo ou tarde, um acidente vai acontecer.

1. O Que Você Vai Aprender


2. Uma História Real de um Engenheiro de Qualidade

(1) Dor: Corrigir um bug gera mais três

Alice modificou a lógica de cancelamento de pedidos, pensando que afetaria apenas a funcionalidade de cancelamento, mas acabou causando problemas na restauração de estoque, que por sua vez afetou as estatísticas de relatórios. Toda vez que ela altera o código, fica apreensiva porque não há testes automatizados para verificar regressões. Bob é frequentemente acordado no meio da noite por chamadas para correções de emergência, e a equipe gasta 30% do tempo a cada semana corrigindo bugs.

(2) Soluções com Testes Automatizados

Escreva bons testes para fornecer uma "rede de segurança" ao modificar o código:

JAVA
@SpringBootTest
class OrderServiceTest {

    @Test
    void shouldCancelOrderAndRestoreStock() {
        // Dado: produto com estoque 10, pedido com quantidade 3
        // Quando: cancelar pedido
        // Então: estoque restaurado para 13, status do pedido é CANCELLED
    }
}

(3) Resultado

Alice escreveu 50 casos de teste para o processo principal do OrderFlow, alcançando 85% de cobertura de código. Toda vez que o código é modificado, os testes são executados automaticamente, e problemas de regressão são detectados em cinco minutos—assim Bob não precisa mais corrigir bugs no meio da noite.


3. A Pirâmide de Testes

(1) Estratégia de Testes em Três Camadas

100%
graph LR
    A["Testes Unitários<br/>70% - Rápidos, Isolados<br/>JUnit + Mockito"] --> B["Testes de Integração<br/>20% - Velocidade Média<br/>@SpringBootTest + H2"]
    B --> C["Testes E2E<br/>10% - Lentos, Stack Completa<br/>TestContainers + REST Assured"]
Nível Porcentagem Velocidade Dependências Conteúdo do Teste
Testes Unitários 70% Nível de milissegundo Mock Lógica de Negócio
Testes de Integração 20% Segundos H2/TestContainers Interação de Componentes
Testes E2E 10% Minutos Ambiente Completo Processo de Negócio

(1) ▶ Exemplo: Teste Unitário

JAVA
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {

    @Mock
    private OrderRepository orderRepository;

    @Mock
    private ProductRepository productRepository;

    @InjectMocks
    private OrderServiceImpl orderService;

    @Test
    void shouldCreateOrderSuccessfully() {
        // Dado
        Product product = new Product("Laptop", BigDecimal.valueOf(999), 10);
        when(productRepository.findById(1L)).thenReturn(Optional.of(product));

        // Quando
        Order order = orderService.createOrder(1L, 3);

        // Então
        assertThat(order).isNotNull();
        assertThat(product.getStock()).isEqualTo(7);
        verify(orderRepository).save(any(Order.class));
    }

    @Test
    void shouldThrowWhenInsufficientStock() {
        Product product = new Product("Laptop", BigDecimal.valueOf(999), 2);
        when(productRepository.findById(1L)).thenReturn(Optional.of(product));

        assertThatThrownBy(() -> orderService.createOrder(1L, 5))
            .isInstanceOf(InsufficientStockException.class);

        verify(orderRepository, never()).save(any());
    }
}

Saída:

TEXT
// Execução bem-sucedida

4. Testes de Fatia com @WebMvcTest

(1) Testes em Nível de Controller

(1) ▶ Exemplo: Teste de Fatia do Controller

JAVA
@WebMvcTest(OrderController.class)
class OrderControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private OrderService orderService;

    @Test
    void shouldReturnOrderById() throws Exception {
        OrderResponse order = new OrderResponse(
            1L, "bob", "PENDING", BigDecimal.valueOf(999),
            List.of(), Instant.now());

        when(orderService.getOrder(1L)).thenReturn(order);

        mockMvc.perform(get("/api/v1/orders/1")
                .with(httpBasic("bob", "pass123")))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.id").value(1))
            .andExpect(jsonPath("$.status").value("PENDING"));
    }

    @Test
    void shouldReturn404WhenOrderNotFound() throws Exception {
        when(orderService.getOrder(999L))
            .thenThrow(new ResourceNotFoundException("Order", 999L));

        mockMvc.perform(get("/api/v1/orders/999")
                .with(httpBasic("bob", "pass123")))
            .andExpect(status().isNotFound())
            .andExpect(jsonPath("$.code").value("RESOURCE_NOT_FOUND"));
    }

    @Test
    void shouldReturn400WhenValidationFails() throws Exception {
        String body = """
            {"productId": null, "quantity": 0}
            """;

        mockMvc.perform(post("/api/v1/orders")
                .with(httpBasic("bob", "pass123"))
                .contentType(MediaType.APPLICATION_JSON)
                .content(body))
            .andExpect(status().isBadRequest())
            .andExpect(jsonPath("$.code").value("VALIDATION_ERROR"));
    }
}

Saída:

TEXT
// Execução bem-sucedida
Anotação de Teste Escopo de Carregamento Nível Aplicável
@WebMvcTest Controller + Camada Web Controller
@DataJpaTest Repository + Camada JPA Repository
@SpringBootTest ApplicationContext Completo Teste de Integração

5. Testes da Camada de Dados com @DataJpaTest

(1) ▶ Exemplo: Teste de Repository

JAVA
@DataJpaTest
class ProductRepositoryTest {

    @Autowired
    private ProductRepository productRepository;

    @Autowired
    private TestEntityManager entityManager;

    @Test
    void shouldFindByNameContaining() {
        entityManager.persist(new Product("Laptop Pro", BigDecimal.valueOf(1299), 50));
        entityManager.persist(new Product("Laptop Air", BigDecimal.valueOf(899), 30));
        entityManager.persist(new Product("Wireless Mouse", BigDecimal.valueOf(29), 200));

        List<Product> results = productRepository.findByNameContaining("Laptop");

        assertThat(results).hasSize(2);
    }

    @Test
    void shouldFindOutOfStockProducts() {
        Product outOfStock = new Product("Old Model", BigDecimal.valueOf(99), 0);
        entityManager.persist(outOfStock);
        entityManager.persist(new Product("New Model", BigDecimal.valueOf(199), 10));

        List<Product> results = productRepository.findOutOfStockProducts();

        assertThat(results).hasSize(1);
        assertThat(results.get(0).getName()).isEqualTo("Old Model");
    }

    @Test
    void shouldCheckStockBeforeDeduction() {
        Product product = entityManager.persist(
            new Product("Laptop", BigDecimal.valueOf(999), 5));

        Product found = productRepository.findById(product.getId()).orElseThrow();
        found.deductStock(3);

        Product updated = productRepository.findById(product.getId()).orElseThrow();
        assertThat(updated.getStock()).isEqualTo(2);
    }
}

Saída:

TEXT
// Execução bem-sucedida

6. TestContainers: Testes de Integração Containerizados

(1) Testes com Banco de Dados Real

(1) ▶ Exemplo: MySQL TestContainer

XML
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-testcontainers</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>mysql</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>junit-jupiter</artifactId>
    <scope>test</scope>
</dependency>

Saída:

TEXT
// Execução bem-sucedida
JAVA
@SpringBootTest
@Testcontainers
class OrderServiceIntegrationTest {

    @Container
    static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0")
        .withDatabaseName("orderflow_test")
        .withUsername("test")
        .withPassword("test");

    @DynamicPropertySource
    static void configureProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", mysql::getJdbcUrl);
        registry.add("spring.datasource.username", mysql::getUsername);
        registry.add("spring.datasource.password", mysql::getPassword);
    }

    @Autowired
    private OrderService orderService;

    @Autowired
    private ProductRepository productRepository;

    @Test
    void shouldCreateOrderAndDeductStock() {
        Product product = productRepository.save(
            new Product("Laptop", BigDecimal.valueOf(999), 10));

        Order order = orderService.createOrder(product.getId(), 3);

        assertThat(order.getStatus()).isEqualTo("PENDING");

        Product updated = productRepository.findById(product.getId()).orElseThrow();
        assertThat(updated.getStock()).isEqualTo(7);
    }
}

(2) ▶ Exemplo: Redis TestContainer

JAVA
@SpringBootTest
@Testcontainers
class RedisCacheIntegrationTest {

    @Container
    static GenericContainer<?> redis = new GenericContainer<>("redis:7-alpine")
        .withExposedPorts(6379);

    @DynamicPropertySource
    static void configureRedis(DynamicPropertyRegistry registry) {
        registry.add("spring.data.redis.host", redis::getHost);
        registry.add("spring.data.redis.port", () -> redis.getMappedPort(6379));
    }

    @Autowired
    private ProductService productService;

    @Autowired
    private ProductRepository productRepository;

    @Test
    void shouldCacheProductInRedis() {
        Product product = productRepository.save(
            new Product("Laptop", BigDecimal.valueOf(999), 10));

        // Primeira chamada: consulta ao banco de dados
        productService.getProduct(product.getId());
        // Segunda chamada: cache hit no Redis
        Product cached = productService.getProduct(product.getId());

        assertThat(cached).isNotNull();
    }
}

Saída:

TEXT
// Execução bem-sucedida

7. Cobertura de Testes

(1) ▶ Exemplo: Configuração do JaCoCo

XML
<plugin>
    <groupId>org.jacoco</groupId>
    <artifactId>jacoco-maven-plugin</artifactId>
    <version>0.8.11</version>
    <executions>
        <execution>
            <goals><goal>prepare-agent</goal></goals>
        </execution>
        <execution>
            <id>report</id>
            <phase>test</phase>
            <goals><goal>report</goal></goals>
        </execution>
    </executions>
</plugin>

Saída:

TEXT
// Execução bem-sucedida
Dimensão de Cobertura Objetivo Descrição
Cobertura de Linha ≥ 80% Código de Negócio Principal
Cobertura de Branch ≥ 70% Lógica if/else
Cobertura de Método ≥ 80% Métodos Públicos

8. Exemplo Abrangente: Testando o Processo Principal de Realização de Pedidos no OrderFlow

JAVA
// OrderServiceTest.java (Teste Unitário)
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {

    @Mock private OrderRepository orderRepository;
    @Mock private ProductRepository productRepository;
    @InjectMocks private OrderServiceImpl orderService;

    @Test
    void createOrder_success() {
        Product product = new Product("Laptop", BigDecimal.valueOf(999), 10);
        when(productRepository.findById(1L)).thenReturn(Optional.of(product));
        when(orderRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        Order order = orderService.createOrder(1L, 3);

        assertThat(order.getStatus()).isEqualTo("PENDING");
        assertThat(product.getStock()).isEqualTo(7);
    }

    @Test
    void createOrder_insufficientStock_throws() {
        Product product = new Product("Laptop", BigDecimal.valueOf(999), 2);
        when(productRepository.findById(1L)).thenReturn(Optional.of(product));

        assertThatThrownBy(() -> orderService.createOrder(1L, 5))
            .isInstanceOf(InsufficientStockException.class);
    }

    @Test
    void cancelOrder_success_restoresStock() {
        Product product = new Product("Laptop", BigDecimal.valueOf(999), 7);
        Order order = new Order();
        order.setStatus("PENDING");
        order.addItem(product, 3);
        when(orderRepository.findByIdWithItems(1L)).thenReturn(Optional.of(order));

        orderService.cancelOrder(1L);

        assertThat(order.getStatus()).isEqualTo("CANCELLED");
        assertThat(product.getStock()).isEqualTo(10);
    }

    @Test
    void cancelOrder_nonPending_throws() {
        Order order = new Order();
        order.setStatus("SHIPPED");
        when(orderRepository.findByIdWithItems(1L)).thenReturn(Optional.of(order));

        assertThatThrownBy(() -> orderService.cancelOrder(1L))
            .isInstanceOf(OrderStateException.class);
    }
}

// OrderControllerTest.java (Teste de Fatia)
@WebMvcTest(OrderController.class)
class OrderControllerTest {

    @Autowired MockMvc mockMvc;
    @MockBean OrderService orderService;

    @Test
    @WithMockUser(roles = "CUSTOMER")
    void createOrder_validRequest_returns201() throws Exception {
        when(orderService.createOrder(any(), eq("bob"))).thenReturn(
            new OrderResponse(1L, "bob", "PENDING", BigDecimal.valueOf(999), List.of(), Instant.now()));

        mockMvc.perform(post("/api/v1/orders")
                .contentType(MediaType.APPLICATION_JSON)
                .content("{\"productId\":1,\"quantity\":3}"))
            .andExpect(status().isCreated())
            .andExpect(jsonPath("$.status").value("PENDING"));
    }

    @Test
    void createOrder_unauthenticated_returns401() throws Exception {
        mockMvc.perform(post("/api/v1/orders")
                .contentType(MediaType.APPLICATION_JSON)
                .content("{\"productId\":1,\"quantity\":3}"))
            .andExpect(status().isUnauthorized());
    }
}

❓ Perguntas Frequentes

P Qual é a diferença entre @MockBean e @Mock?
R @Mock é uma anotação do Mockito usada para criar objetos mock puros; não depende do Spring. @MockBean é uma anotação do Spring Boot que injeta objetos mock no ApplicationContext do Spring para substituir beans no contêiner. Use @Mock para testes unitários e @MockBean para testes de integração.
P Qual escolher: @SpringBootTest ou @WebMvcTest?
R @WebMvcTest carrega apenas a camada Web (Controller + Filter), o que é mais rápido e adequado para testes unitários de Controller. @SpringBootTest carrega o ApplicationContext completo e é adequado para testes de integração end-to-end.
P O TestContainers deixará os testes mais lentos?
R Na primeira vez que um container é iniciado, a imagem deve ser baixada; no entanto, os containers são compartilhados entre classes de teste (via campo static), então testes subsequentes os reutilizam diretamente. Overhead típico: 30 segundos na primeira execução, e menos de 5 segundos nas execuções seguintes. Recomendamos pré-aquecer as imagens no ambiente de CI.
P O H2 e o MySQL se comportam da mesma forma?
R Eles são na sua maioria consistentes, mas há algumas diferenças: 1) AUTO_INCREMENT do MySQL vs. IDENTITY do H2; 2) Funções específicas do MySQL (GROUP_CONCAT); 3) Diferentes níveis de isolamento de transação padrão. Para ambientes de produção, recomendamos usar TestContainers + MySQL.
P Como testar métodos @Async?
R 1) Testar o método assíncrono diretamente (sem usar proxy); 2) Usar CompletableFuture.get() para esperar o resultado; 3) Configurar um executor síncrono (SyncTaskExecutor) para simplificar os testes.
P 80% de cobertura de testes é suficiente?
R 80% é o padrão mínimo para código de negócio principal; recomendamos 85% ou mais. Cobertura é apenas uma referência; o que mais importa é a qualidade dos testes—cobrindo condições de contorno, caminhos de exceção e cenários concorrentes, não apenas caminhos de fluxo normal.

📖 Resumo


📝 Exercícios

  1. Exercício Básico (Dificuldade: ⭐): Escreva cinco testes unitários para o OrderService que cubram cenários como criar um pedido, estoque insuficiente, cancelar um pedido, exceções de status e recursos não encontrados.

  2. Exercício Avançado (Dificuldade ⭐⭐): Use @WebMvcTest para escrever testes de fatia para o OrderController, cobrindo cenários como requisições normais, falhas de validação, erros 404 e erros 401 (Não Autorizado). Use @DataJpaTest para escrever testes de consulta para o ProductRepository.

  3. Desafio (Dificuldade: ⭐⭐⭐): Integre TestContainers (MySQL + Redis) e escreva um teste de integração end-to-end—cobrindo todo o processo desde criar um produto, realizar um pedido e deduzir estoque, cache hit no Redis, até cancelar um pedido e restaurar estoque, com taxa de cobertura ≥ 80%.

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%