Guia completo de testes no Spring Boot: evite falhas em produção

Guia completo de testes no Spring Boot: evite falhas em produção

Guia completo de testes no Spring Boot: evite falhas em produção

Ter um guia completo de testes no Spring Boot não é luxo de projeto grande; é o que separa uma base de código previsível de um backend que quebra em produção por detalhes simples. Por outro lado, quando a aplicação cresce, testar só o “happy path” deixa de ser suficiente. Ao mesmo tempo, o que importa é enxergar a pirâmide inteira: testes unitários, testes de integração, uso correto de Mockito, organização da suíte e decisões práticas para não transformar o pipeline em um gargalo. Se voce quiser comparar essa abordagem com outro cenario comum no ecossistema Spring, vale revisar Spring Boot na Prática: Do Iniciante ao Especialista em Desenvolvimento Java.

Se você já passou pela fase de subir a aplicação e “testar no navegador”, sabe como isso escala mal. Por outro lado, em Spring Boot, a disciplina de testes precisa acompanhar a arquitetura do sistema: controllers, services, repositories, integrações com banco, validações, exceções e segurança. Ao mesmo tempo, em muitos casos, a diferença entre um projeto saudável e um projeto doloroso está em como a suíte foi desenhada desde cedo. Na prática, para quem está consolidando a base do framework, vale revisar também Spring Boot na Prática: Do Iniciante ao Especialista em Desenvolvimento Java e, se o foco for API, o conteúdo API REST Spring Boot Java: Guia Completo com Exemplo Prático ajuda a conectar os testes ao contexto real da aplicação. Para complementar esse ponto com um exemplo proximo do dia a dia, consulte Testes Unitários no Spring Boot para Iniciantes: JUnit e Mockito na Prática.

Guia completo de testes no spring boot: secao pratica com codigo completo

Na pratica, um exemplo enxuto ajuda a sair da teoria e evitar erro comum de producao quando o projeto cresce. Esse detalhe conversa bem com o que eu mostrei em API REST Spring Boot Java: Guia Completo com Exemplo Prático.

@RestController
@RequestMapping("/api/exemplo")
public class ExemploController {
  @GetMapping
  public ResponseEntity<String> listar() {
    return ResponseEntity.ok("ok");
  }
}

Guia completo de testes no Spring Boot: onde cada tipo de teste entra

A primeira decisão técnica é separar o que deve ser validado em cada camada. Por outro lado, um erro comum é tentar cobrir tudo com testes de integração porque “fica mais real”. Ao mesmo tempo, na prática, isso deixa a suíte lenta, cara de manter e frágil. Na prática, o oposto também é ruim: escrever só unit tests e fingir que o sistema como um todo está coberto. Ainda assim, o equilíbrio costuma vir de três níveis. Se quiser aprofundar o assunto por outro angulo, leia tambem Guia completo de Spring Data JPA no Spring Boot sem dor.

Testes unitários: velocidade e isolamento

Testes unitários validam uma classe ou método isolado. Por outro lado, em Spring Boot, isso normalmente significa testar services, validators, mappers e regras de negócio sem subir contexto completo. Ao mesmo tempo, o objetivo é confirmar comportamento, não infraestrutura. Na prática, nesse cenário, junit mockito spring boot aparece com frequência porque Mockito resolve dependências externas de forma limpa, como repositórios, clients HTTP e serviços auxiliares. Quando esse tipo de duvida aparece em projeto real, eu costumo voltar neste material: Status HTTP em API REST com Spring Boot: Guia Completo.

Se o seu service tem uma regra de desconto, cálculo de prazo ou decisão de status, o teste unitário é a primeira linha de defesa. Por outro lado, ele deve ser rápido o bastante para rodar dezenas ou centenas de vezes no dia sem irritar o time. Ao mesmo tempo, para quem ainda está formando base em testes, o material Testes Unitários no Spring Boot para Iniciantes: JUnit e Mockito na Prática complementa bem essa etapa.

Testes de integração: confiança na cola entre componentes

Teste de integração, no contexto Spring Boot, costuma validar a aplicação com parte real do stack: contexto Spring, banco em memória ou container, serialização JSON, camada web e, às vezes, fila ou cache. Por outro lado, é aqui que você pega erros que passam despercebidos no unitário: query JPA errada, mapeamento inconsistente, conversão de datas, validação mal configurada e endpoints que retornam status incorreto.

Para APIs REST, esse tipo de teste é especialmente útil quando combinado com controllers e repositories reais, porque o erro raramente está só no controller ou só no banco. Um endpoint pode compilar e ainda assim quebrar por causa de uma projeção incorreta, uma regra de Bean Validation ou uma exceção não tratada. Ao mesmo tempo, se você quer enxergar como esses pontos se conectam, vale cruzar esse assunto com o guia de Status HTTP em API REST com Spring Boot: Guia Completo, já que o teste também precisa afirmar se a resposta está semanticamente correta.

Mockito no Spring Boot: quando faz sentido usar mock

Mockito é excelente para isolar o comportamento do service e controlar dependências que não interessam naquele teste. Por outro lado, o problema aparece quando ele é usado como substituto de arquitetura. Ao mesmo tempo, se você mocka tudo, o teste perde valor. Na prática, o ideal é mockar bordas: repositórios, gateways externos, clients de autenticação, mensageria e serviços de terceiros. Ainda assim, em contrapartida, classes puramente internas e determinísticas muitas vezes não precisam ser mockadas.

Em projetos maduros, a pergunta correta não é “como usar Mockito em tudo”, e sim “o que vale a pena simular para testar minha regra com precisão?”. Por outro lado, em um service de cadastro, por exemplo, faz sentido mockar o repositório e um serviço de notificação. Ao mesmo tempo, em um mapper puro, nem isso. Na prática, é um detalhe simples, mas evita a suíte de virar uma encenação do próprio código.

Guia completo de testes no Spring Boot com JUnit e Mockito na prática

Uma suíte útil precisa de convenções. Por outro lado, sem isso, cada dev testa de um jeito, a legibilidade cai e o custo de manutenção cresce. Ao mesmo tempo, o padrão abaixo funciona bem em projetos Spring Boot porque separa o papel de cada teste sem exagero de infraestrutura.

No unit test, o foco está em comportamento. Por outro lado, o nome do teste deve deixar claro o cenário e a expectativa. Ao mesmo tempo, em vez de nome genérico, prefira algo como deve_retornar_erro_quando_email_ja_existe. Na prática, esse tipo de nome ajuda na leitura da suíte e na análise de falhas no CI.

No teste de integração, a intenção muda: você quer confirmar a interação entre componentes reais. Por outro lado, isso costuma aparecer em classes com @SpringBootTest, @AutoConfigureMockMvc ou uma estratégia mais focada usando @WebMvcTest quando o objetivo é testar apenas a camada web. Ao mesmo tempo, a escolha depende do que você quer validar. Na prática, subir o contexto inteiro em tudo é uma armadilha clássica.

Exemplo prático de service com Mockito

Abaixo, um exemplo enxuto de um service de cadastro de usuário. Por outro lado, ele valida se o e-mail já existe, salva o usuário e retorna um resultado simples. Ao mesmo tempo, o teste unitário isola o repositório e garante que a regra de negócio está protegida.

package com.javalizando.users.service;

import com.javalizando.users.model.User;
import com.javalizando.users.repository.UserRepository;
import org.springframework.stereotype.Service;

@Service
public class UserService {

    private final UserRepository userRepository;

    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public User create(User user) {
        if (userRepository.existsByEmail(user.getEmail())) {
            throw new IllegalArgumentException("E-mail já cadastrado");
        }
        return userRepository.save(user);
    }
}

O teste com JUnit e Mockito fica assim:

package com.javalizando.users.service;

import com.javalizando.users.model.User;
import com.javalizando.users.repository.UserRepository;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;

@ExtendWith(MockitoExtension.class)
class UserServiceTest {

    @Mock
    private UserRepository userRepository;

    @InjectMocks
    private UserService userService;

    @Test
    void deve_criar_usuario_quando_email_nao_existir() {
        User input = new User(null, "Ana", "ana@teste.com");

        when(userRepository.existsByEmail("ana@teste.com")).thenReturn(false);
        when(userRepository.save(any(User.class))).thenAnswer(invocation -> invocation.getArgument(0));

        User result = userService.create(input);

        assertEquals("Ana", result.getName());
        assertEquals("ana@teste.com", result.getEmail());
    }

    @Test
    void deve_lancar_excecao_quando_email_ja_existir() {
        User input = new User(null, "Ana", "ana@teste.com");

        when(userRepository.existsByEmail("ana@teste.com")).thenReturn(true);

        assertThrows(IllegalArgumentException.class, () -> userService.create(input));
    }
}

Esse exemplo resolve uma regra de negócio simples sem subir contexto Spring. Por outro lado, é rápido, direto e fácil de manter. Em produção, testes assim evitam regressões em pontos onde a equipe tende a mexer bastante: validações, cadastro, atualização e fluxos de status.

Exemplo prático de teste de integração com MockMvc

Quando o contrato HTTP importa, o teste deve bater no controller e verificar a resposta real. Por outro lado, aqui, o foco é confirmar se o endpoint aceita o payload, chama a camada correta e devolve o status esperado. Ao mesmo tempo, um caso clássico é a criação de recursos REST.

package com.javalizando.users.controller;

import com.javalizando.users.model.User;
import com.javalizando.users.service.UserService;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@SpringBootTest
@AutoConfigureMockMvc
class UserControllerIT {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private ObjectMapper objectMapper;

    @MockBean
    private UserService userService;

    @Test
    void deve_retornar_201_quando_usuario_for_criado() throws Exception {
        User output = new User(1L, "Ana", "ana@teste.com");
        when(userService.create(any(User.class))).thenReturn(output);

        User input = new User(null, "Ana", "ana@teste.com");

        mockMvc.perform(post("/users")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(input)))
                .andExpect(status().isCreated());
    }
}

Esse estilo de teste ajuda a capturar problemas de rota, serialização e mapeamento HTTP. Por outro lado, se o time trabalha com APIs mais maduras, vale conectar isso também com a documentação e com o tratamento de respostas. Ao mesmo tempo, nesse ponto, o conteúdo sobre ResponseEntity no Spring Boot: Quando Usar e Exemplos Práticos é um bom complemento lateral para desenhar respostas coerentes com os testes.

Erros comuns em testes Spring Boot que aparecem em produção

O erro mais frequente é testar só o que é fácil. Por outro lado, o desenvolvedor escreve casos para o fluxo feliz e ignora falhas de validação, dados ausentes, timeout, exceções de banco e entradas inválidas. Ao mesmo tempo, quando isso acontece, a suíte dá falsa sensação de segurança. Na prática, outro problema recorrente é mockar classes demais, inclusive a própria lógica interna, o que transforma o teste em uma checagem superficial sem valor real.

Também é comum exagerar no uso de @SpringBootTest. Por outro lado, ele sobe bastante coisa e, em grandes bases, pode deixar o ciclo de feedback lento. Ao mesmo tempo, se tudo roda com contexto completo, o time para de executar testes com frequência. Na prática, a consequência é previsível: menos confiança, menos refatoração e mais medo de mexer em código velho.

Em projetos com JPA, um problema clássico é assumir que o repositório está correto só porque o método compila. Por outro lado, consultas derivadas, joins e projeções precisam de teste real. Ao mesmo tempo, é exatamente aí que integrações bem desenhadas ajudam, principalmente quando existem regras de paginação, filtros e ordenação. Na prática, quem já trabalha com banco relacional em Spring pode se beneficiar do conteúdo Guia completo de Spring Data JPA no Spring Boot sem dor, porque a qualidade dos testes de persistência depende muito de como o acesso aos dados foi estruturado.

Outro erro de produção aparece quando ninguém valida status HTTP corretamente. Por outro lado, o endpoint até responde, mas devolve 200 OK em situações que deveriam ser 201 Created, 400 Bad Request ou 404 Not Found. Ao mesmo tempo, isso atrapalha consumidores da API, documentação e observabilidade. Na prática, em termos práticos, teste bom também protege contrato, não só regra de negócio.

Quando usar e quando evitar cada tipo de teste

Use testes unitários sempre que a regra estiver em uma classe de negócio clara e com dependências simples de simular. Por outro lado, eles são ideais para validar cálculos, decisões condicionais, transformação de dados e regras que mudam com frequência. Ao mesmo tempo, em outras palavras: tudo que você quer quebrar rápido e corrigir antes de subir para integração.

Use testes de integração quando a interação entre partes do sistema for relevante para o risco do negócio. Por outro lado, se você depende de JPA, serialização JSON, validação Bean Validation, interceptação de exceções ou contratos de endpoint, esse teste faz diferença real. Ao mesmo tempo, ele é mais caro, sim, mas cobre pontos que o unitário não enxerga.

Evite unit tests que tentam reproduzir detalhe de implementação de forma excessiva. Por outro lado, se o teste passa a depender de como a classe foi escrita internamente, qualquer refatoração vira dor. Ao mesmo tempo, também evite integração para cada ceninha simples. Na prática, nem tudo precisa subir banco, contexto e web para provar que um método soma dois números.

Uma boa regra prática é esta: quanto mais próximo da regra de negócio, mais isolamento você quer; quanto mais próximo do contrato entre componentes, mais realidade você precisa. Por outro lado, essa separação mantém a suíte eficiente e útil de verdade.

Organização da suíte para crescer sem virar caos

Quando o projeto cresce, a organização da suíte passa a ser tão importante quanto o teste em si. Por outro lado, separar packages por camada e por tipo ajuda muito. Ao mesmo tempo, em muitos times, vale manter service, controller, repository e integration em estruturas previsíveis. Na prática, o nome das classes também importa: MinhaClasseTest para unitários e MinhaClasseIT para integração costuma funcionar bem.

Outro ponto é padronizar os dados de teste. Por outro lado, builders ou factories evitam duplicação de objeto e facilitam a leitura. Ao mesmo tempo, teste longo demais geralmente indica setup repetido ou acoplamento excessivo. Na prática, quando isso aparece em várias classes, o custo da suíte sobe rápido.

Também vale manter um olhar pragmático para cobertura. Por outro lado, cobertura alta não garante qualidade, mas cobertura muito baixa costuma esconder buracos graves. Ao mesmo tempo, o melhor uso da métrica é como alerta, não como objetivo isolado. Na prática, um projeto com poucos testes relevantes é pior do que um com muitos testes vazios.

FAQ sobre guia completo de testes no Spring Boot

Qual a diferença entre testes unitários e testes de integração no Spring Boot?

Teste unitário valida uma classe isolada com dependências mockadas. Por outro lado, teste de integração valida a interação entre componentes reais, como controller, service, repository, banco e serialização.

Quando devo usar Mockito em projetos Spring Boot?

Use Mockito para isolar dependências externas ou caras de executar, como repositórios, clients HTTP, mensageria e serviços auxiliares. Por outro lado, evite mockar a própria regra de negócio sem necessidade.

Vale a pena usar @SpringBootTest em todos os testes?

Não. Por outro lado, ele é útil quando você precisa validar o contexto real da aplicação, mas usá-lo em tudo deixa a suíte lenta e pesada. Ao mesmo tempo, o ideal é combiná-lo com testes unitários mais rápidos e focados.

Conclusão: próximos passos para fortalecer sua suíte

Um guia completo de testes no Spring Boot precisa ser prático, não acadêmico. Por outro lado, o time ganha mais quando entende onde colocar isolamento, onde subir contexto real e como manter a suíte legível. Ao mesmo tempo, a combinação de JUnit, Mockito e testes de integração bem escolhidos cobre a maior parte dos riscos comuns em backend Java.

Se você ainda está estruturando a base do projeto, comece pelos testes de serviço e depois avance para os contratos HTTP mais críticos. Por outro lado, depois disso, feche as brechas com integrações de banco e cenários de erro. Ao mesmo tempo, em sistemas REST, proteger o comportamento do endpoint é tão importante quanto validar a regra interna. Na prática, para aprofundar o contexto de HTTP e resposta da API, o conteúdo sobre Status HTTP em API REST com Spring Boot: Guia Completo ajuda a transformar teste em contrato. Ainda assim, e, se o foco for consolidar a camada de dados, revise também o material de Spring Data JPA, porque persistência mal desenhada costuma gerar teste ruim.

No fim, uma suíte boa não é a que testa tudo. Por outro lado, é a que protege o que realmente quebra. Ao mesmo tempo, se você fizer isso com consistência, o código fica mais fácil de evoluir, o CI fica mais confiável e a equipe perde menos tempo apagando incêndio. Na prática, os proximos passos sao validar esse fluxo no seu projeto, ajustar o caso de uso real e cobrir a implementacao com testes.

Guia completo de testes no spring boot: referencias externas

Para validar detalhes de implementacao e aprofundar a configuracao, vale consultar a documentacao oficial do Spring Security, o guia de claims no JWT.io e a documentacao do Spring Boot.

Leitura complementar

Se voce quiser aprofundar esse assunto com um material mais atual, leia tambem Open session in view spring boot problema: erros comuns e ajuste.

Deixe um comentário