SkillsTecnológicas
Menu
Back-end

Testes Unitários em Java com JUnit e Mockito: Guia Prático

Aprenda testes unitários em Java com JUnit e Mockito: configuração, assertions, mocks, testes parametrizados, boas práticas e exemplos completos.

Skills TecnológicasPublicado em 6 de março de 2025Atualizado em 19 de agosto de 202611 min de leitura
Componente de software Java passa por diferentes verificações automatizadas em um ambiente controlado de testes unitários

Testes unitários em Java verificam pequenas unidades de comportamento — normalmente métodos ou classes — de forma rápida, isolada e repetível.

O JUnit fornece a estrutura de execução e as assertions; o Mockito ajuda a substituir dependências externas quando o objeto testado não deve acessar banco, rede ou outro serviço real.

Um bom teste não existe para aumentar uma porcentagem no relatório. Ele documenta uma regra importante, falha por um motivo compreensível e dá segurança para refatorar.

Isso exige escolher cenários relevantes, controlar dependências e evitar testes tão acoplados à implementação que qualquer mudança interna os quebra.

Neste guia, você configurará JUnit e Mockito, escreverá testes com Arrange–Act–Assert, cobrirá exceções e casos parametrizados, simulará um repositório e verificará interações.

Os exemplos usam Java moderno e Maven, com observações para Gradle e projetos que ainda permanecem no JUnit 5.

O que é um teste unitário?

Um teste unitário executa uma pequena parte do sistema sob condições controladas e compara o comportamento observado com o esperado.

A “unidade” não precisa ser literalmente um método: pode ser uma classe ou um conjunto muito pequeno de objetos que representa uma regra coesa.

O teste deve ser determinístico. Dado o mesmo código e a mesma entrada, precisa produzir o mesmo resultado. Relógio do sistema, aleatoriedade, rede, banco e arquivos são fontes comuns de variação. Quando fazem parte da regra, abstraia-os para que o teste controle o cenário.

Teste unitário, integração e ponta a ponta

TipoO que validaVelocidadeDependências reais
UnitárioRegra isoladaMuito altaPoucas ou nenhuma
IntegraçãoColaboração entre componentesMédiaBanco, fila ou framework
Ponta a pontaFluxo visto pelo usuárioMais baixaSistema quase completo

Essas camadas se complementam. Um mock pode provar que o serviço tentou salvar um pedido, mas somente um teste de integração confirma que o mapeamento realmente persiste no banco.

Use testes unitários para feedback rápido e acrescente testes reais nos limites mais importantes.

JUnit e Mockito: responsabilidades diferentes

JUnit é a base de execução: descobre métodos de teste, controla o ciclo de vida, oferece assertions, parametrização e extensões. A documentação do JUnit separa a plataforma de execução da API Jupiter usada para escrever testes.

Mockito é um framework de mocking. Ele cria objetos simulados, programa respostas e verifica chamadas. O projeto recomenda o ciclo simples “stub, use, verify” e também alerta para não simular tudo. Consulte a documentação oficial do Mockito antes de recorrer a recursos mais complexos.

Qual versão usar?

Em agosto de 2026, JUnit 6.1.2 é uma versão estável atual e exige Java 17 ou superior. Projetos presos ao Java 8 ou 11 podem usar uma linha compatível do JUnit 5, como 5.14.x. Não atualize apenas o número: leia as notas de versão e migração do JUnit 6. Para Mockito, a linha estável 5.23.0 está publicada no repositório central oficial.

Configure JUnit e Mockito no Maven

<dependencies>
  <dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>6.1.2</version>
    <scope>test</scope>
  </dependency>

  <dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-junit-jupiter</artifactId>
    <version>5.23.0</version>
    <scope>test</scope>
  </dependency>
</dependencies>

O escopo test impede que as bibliotecas sejam empacotadas como dependências de produção. O Maven Surefire executa os testes na fase test; versões atuais trabalham com JUnit Platform. A documentação do Surefire recomenda fixar a versão do plugin no projeto ou no POM pai para evitar mudanças inesperadas do ambiente.

Configuração equivalente no Gradle

dependencies {
    testImplementation("org.junit.jupiter:junit-jupiter:6.1.2")
    testImplementation("org.mockito:mockito-junit-jupiter:5.23.0")
}

tasks.test {
    useJUnitPlatform()
}

Em projetos Spring Boot, o gerenciamento de dependências pode definir versões compatíveis. Verifique o BOM da versão do framework antes de sobrescrever números manualmente. Se ainda está estruturando seu aprendizado, revise as dicas essenciais para iniciantes em Java.

Organize o projeto de testes

src/
├── main/java/com/exemplo/frete/CalculadoraFrete.java
└── test/java/com/exemplo/frete/CalculadoraFreteTest.java

Mantenha a classe de teste no mesmo pacote lógico da classe de produção. Isso facilita localização e permite testar membros com visibilidade de pacote sem torná-los públicos só para o teste. Arquivos JSON, CSV ou fixtures pertencem normalmente a src/test/resources.

Seu primeiro teste com JUnit

class CalculadoraFrete {
    BigDecimal calcular(BigDecimal subtotal) {
        if (subtotal.signum() < 0) {
            throw new IllegalArgumentException("Subtotal inválido");
        }
        return subtotal.compareTo(new BigDecimal("200.00")) >= 0
                ? BigDecimal.ZERO
                : new BigDecimal("19.90");
    }
}
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.math.BigDecimal;
import org.junit.jupiter.api.Test;

class CalculadoraFreteTest {
    @Test
    void deveOferecerFreteGratisQuandoSubtotalAtingeDuzentosReais() {
        CalculadoraFrete calculadora = new CalculadoraFrete();

        BigDecimal frete = calculadora.calcular(new BigDecimal("200.00"));

        assertEquals(BigDecimal.ZERO, frete);
    }
}
Três etapas visuais representam preparação dos dados, execução da unidade e comparação do resultado esperado em um teste
Arrange prepara o cenário, Act executa uma única ação e Assert verifica o resultado observável do comportamento testado.

Arrange, Act e Assert

No exemplo, a criação da calculadora é Arrange; a chamada a calcular é Act; e assertEquals é Assert. Nem todo teste precisa de comentários separando as etapas, mas a leitura deve revelar um cenário, uma ação principal e uma consequência.

Assertions que comunicam intenção

Prefira a assertion mais específica: assertEquals para valores, assertTrue para uma condição legítima, assertNull ou assertNotNull quando a nulidade é o contrato e assertIterableEquals para sequências. Uma mensagem opcional deve explicar o significado da falha, não repetir valores que o JUnit já mostra.

Teste exceções com assertThrows

@Test
void deveRejeitarSubtotalNegativo() {
    CalculadoraFrete calculadora = new CalculadoraFrete();

    IllegalArgumentException erro = assertThrows(
            IllegalArgumentException.class,
            () -> calculadora.calcular(new BigDecimal("-0.01"))
    );

    assertEquals("Subtotal inválido", erro.getMessage());
}

Não capture Exception de forma ampla se o contrato prevê um tipo específico. O teste poderia passar por causa de um NullPointerException acidental e esconder o defeito real.

Agrupe verificações relacionadas

assertAll executa várias verificações relacionadas e apresenta todas as falhas do grupo. É útil para validar o estado coerente de um objeto retornado. Não o use para juntar comportamentos independentes em um teste enorme.

Ciclo de vida do teste

Por padrão, o JUnit cria uma nova instância da classe de teste para cada método. @BeforeEach roda antes de cada caso e @AfterEach depois. Já @BeforeAll e @AfterAll servem para recursos compartilhados mais caros, com cuidado para não introduzir estado mutável entre testes.

Quando usar BeforeEach

Extraia uma preparação apenas quando ela for comum e clara. Se o leitor precisa subir até o topo da classe para descobrir um valor crucial, manter o dado no próprio teste pode ser mais legível. Fixtures ocultas e mutáveis tornam a suíte difícil de entender.

Testes parametrizados

Quando a mesma regra deve ser validada com várias entradas, @ParameterizedTest reduz repetição sem esconder os casos. Cada conjunto é uma execução independente e aparece separadamente no relatório.

ValueSource e CsvSource

@ParameterizedTest
@CsvSource({
    "0.00, 19.90",
    "199.99, 19.90",
    "200.00, 0.00",
    "350.50, 0.00"
})
void deveCalcularFretePorFaixa(String subtotal, String esperado) {
    CalculadoraFrete calculadora = new CalculadoraFrete();

    BigDecimal resultado = calculadora.calcular(new BigDecimal(subtotal));

    assertEquals(new BigDecimal(esperado), resultado);
}

Use @ValueSource para um único parâmetro simples e @CsvSource para pequenas combinações. Para cenários complexos, @MethodSource permite criar objetos. Se a matriz cresce demais, talvez os casos representem comportamentos diferentes e mereçam testes separados.

Quando usar Mockito

Use Mockito quando a unidade depende de uma colaboração que não deve executar de verdade: repositório, gateway de pagamento, cliente HTTP, relógio ou publicador de eventos. Se o objeto é um valor simples, como endereço ou dinheiro, geralmente é melhor criar uma instância real.

Mock, stub, spy e fake

  • Stub: devolve respostas programadas para orientar o cenário.
  • Mock: objeto simulado cujas interações podem ser verificadas.
  • Spy: envolve uma instância real e permite substituir partes; use com moderação.
  • Fake: implementação funcional simplificada, como repositório em memória.

A distinção ajuda a escolher o menor instrumento. Um fake em memória pode expressar melhor o comportamento que dezenas de stubbings. Muitos mocks também podem indicar que a classe possui responsabilidades demais, tema ligado à orientação a objetos em Java.

Serviço em teste utiliza dependências simuladas enquanto banco de dados e serviço externo reais permanecem isolados
Mocks substituem dependências externas para que o teste avalie somente o comportamento da unidade em condições controladas.

Exemplo completo com Mockito

class PedidoService {
    private final PedidoRepository repository;
    private final Notificador notificador;

    PedidoService(PedidoRepository repository, Notificador notificador) {
        this.repository = repository;
        this.notificador = notificador;
    }

    Pedido criar(String email, BigDecimal total) {
        if (repository.existeAbertoPara(email)) {
            throw new IllegalStateException("Pedido aberto já existe");
        }

        Pedido salvo = repository.salvar(new Pedido(email, total));
        notificador.enviarConfirmacao(salvo);
        return salvo;
    }
}

Mock e InjectMocks

@ExtendWith(MockitoExtension.class)
class PedidoServiceTest {
    @Mock PedidoRepository repository;
    @Mock Notificador notificador;
    @InjectMocks PedidoService service;

    @Test
    void deveSalvarENotificarNovoPedido() {
        Pedido salvo = new Pedido("ana@exemplo.com", new BigDecimal("150.00"));
        when(repository.existeAbertoPara("ana@exemplo.com")).thenReturn(false);
        when(repository.salvar(any(Pedido.class))).thenReturn(salvo);

        Pedido resultado = service.criar(
                "ana@exemplo.com", new BigDecimal("150.00"));

        assertSame(salvo, resultado);
        verify(repository).salvar(any(Pedido.class));
        verify(notificador).enviarConfirmacao(salvo);
    }
}

@ExtendWith(MockitoExtension.class) integra Mockito ao ciclo do JUnit. @Mock cria as dependências simuladas e @InjectMocks tenta construir o objeto testado. A injeção por construtor continua preferível no código de produção, pois torna as dependências explícitas e o objeto fácil de criar sem framework.

Stubbing com when e thenReturn

O when(...).thenReturn(...) programa a resposta necessária ao caminho testado. Não configure métodos que o cenário não usa. Stubbings desnecessários aumentam ruído e podem esconder uma alteração de fluxo.

Verifique interações importantes

verify é valioso quando a interação é parte do resultado: salvar, publicar evento, enviar notificação ou impedir uma chamada. No cenário de pedido duplicado, por exemplo, o teste deveria verificar que nem o salvamento nem a notificação ocorreram.

verify(repository, never()).salvar(any());
verifyNoInteractions(notificador);

Capture argumentos quando necessário

ArgumentCaptor permite inspecionar o objeto enviado a uma dependência quando ele é criado internamente. Use-o para validar dados relevantes, não para reproduzir toda a estrutura privada do objeto.

@Captor ArgumentCaptor<Pedido> pedidoCaptor;

verify(repository).salvar(pedidoCaptor.capture());
assertEquals("ana@exemplo.com", pedidoCaptor.getValue().email());

Não verifique detalhes irrelevantes

Verificar cada getter, ordem de chamadas sem significado ou número exato de consultas torna o teste dependente da implementação. Priorize saída, mudança de estado e interações que representam o contrato. Uma refatoração interna deveria manter os testes verdes quando o comportamento não mudou.

Como escrever testes que não ficam frágeis

  • teste comportamento público, não linhas internas;
  • use dados pequenos que deixem o limite da regra evidente;
  • evite relógio, rede e aleatoriedade sem controle;
  • mantenha um motivo principal para a falha;
  • não compartilhe estado mutável entre casos;
  • elimine espera com Thread.sleep de testes unitários;
  • refatore o teste com o mesmo cuidado do código de produção.

Nomeie pelo comportamento esperado

Nomes como deveRejeitarSubtotalNegativo informam cenário e expectativa. Evite teste1 ou repetir apenas o nome do método. @DisplayName aceita uma descrição mais livre, mas nomes claros no código continuam úteis em buscas e relatórios.

Cobertura não é qualidade

Cobertura mostra quais instruções ou ramificações foram executadas, não se as verificações são boas. Um teste sem assertion pode elevar o número sem proteger regra alguma. Use o relatório para encontrar áreas esquecidas, mas priorize regras financeiras, permissões, transformações, limites e falhas com maior impacto.

Se sua equipe trabalha com ciclos orientados por teste, o artigo sobre TDD na prática explica Red, Green e Refactor. Aqui, o foco permanece nas ferramentas e no desenho dos testes, independentemente de terem sido escritos antes ou depois do código.

Erros comuns com JUnit e Mockito

  • Simular a classe testada: você termina validando Mockito, não sua regra.
  • Mockar objetos de valor: instâncias reais são mais simples e confiáveis.
  • Usar qualquer matcher em tudo: entradas incorretas podem passar sem serem percebidas.
  • Compartilhar mocks entre testes: interações anteriores contaminam o resultado.
  • Testar método privado por reflexão: cria acoplamento direto à implementação.
  • Esperar ordem sem necessidade: uma reorganização legítima quebra a suíte.
  • Ignorar testes intermitentes: um teste não determinístico reduz a confiança de toda a equipe.
  • Executar somente na IDE: diferenças do build oficial aparecem tarde.

Execute testes no Maven, Gradle e CI

# Maven
mvn test
mvn -Dtest=PedidoServiceTest test

# Gradle
./gradlew test
./gradlew test --tests PedidoServiceTest

Execute o mesmo comando localmente e no pipeline. Falha de teste deve interromper o build antes do deploy. Para organizar essa etapa com revisão, artefatos e ambientes, consulte o guia de CI/CD com testes e deploy seguro. Em aplicações de framework, como as descritas no artigo sobre Spring Boot para desenvolvedores Java, separe testes unitários rápidos dos que inicializam o contexto completo.

Checklist de uma suíte saudável

  • cada teste descreve um comportamento relevante;
  • resultados independem de ordem e horário;
  • falhas apresentam mensagem e localização úteis;
  • mocks representam limites externos, não todos os objetos;
  • casos felizes, limites e erros estão cobertos;
  • testes unitários rodam em poucos segundos;
  • integrações críticas possuem testes reais complementares;
  • versões de JUnit, Mockito e plugin de build estão fixadas;
  • a suíte roda no pipeline em cada alteração;
  • testes instáveis são corrigidos, não apenas repetidos.

Perguntas frequentes

Mockito é obrigatório para testar Java?

Não. Classes puras podem ser testadas somente com JUnit. Adicione Mockito quando precisar controlar ou observar uma dependência. Menos infraestrutura costuma produzir testes mais legíveis.

Devo testar métodos privados?

Normalmente, teste-os por meio do comportamento público. Se um método privado concentra lógica complexa, talvez essa lógica mereça uma classe própria com contrato testável, em vez de reflexão no teste.

Quantas assertions um teste pode ter?

Não existe limite fixo. Várias assertions são adequadas quando descrevem um único resultado coerente. Separe o teste quando as verificações representam comportamentos independentes ou quando uma falha impede entender as demais.

Teste unitário substitui teste de integração?

Não. Mocks não confirmam SQL, serialização, configuração do framework ou contrato real de uma API. Mantenha muitos testes unitários rápidos e uma camada menor de testes de integração para os limites críticos.

Conclusão

JUnit e Mockito formam uma combinação poderosa quando cada ferramenta é usada para sua responsabilidade.

JUnit organiza cenários e verifica resultados; Mockito isola colaborações externas. O objetivo não é simular o sistema inteiro, mas criar um ambiente pequeno em que uma regra possa ser exercitada com clareza.

Comece pelas regras mais importantes, nomeie testes pelo comportamento, controle fontes de variação e execute a suíte no build oficial.

Conforme o projeto cresce, combine testes unitários com integração e ponta a ponta.

Essa estratégia oferece feedback rápido sem criar uma falsa sensação de segurança.