Dublês e Disciplina — Mockito e Testes que Não Mentem

Dublês e Disciplina — Mockito e Testes que Não Mentem

Como testar código que depende de banco, e-mail e relógio com dublês de teste: stubs, spies, mocks e fakes escritos à mão e com o Mockito, o Clock injetado para fugir de testes instáveis, verificação de estado e de comportamento, cobertura com JaCoCo e testes de mutação com o PIT.
Java

• • 19 min de leitura

Como testar código que depende de coisas difíceis de controlar, como relógio, banco de dados e serviços externos, com dublês de teste; como o Mockito os cria; e como saber se os testes realmente verificam alguma coisa.

"Provando que Funciona" testou classes que não dependiam de nada externo. Na vida real, quase todo código importante depende de algo difícil de controlar num teste: um banco de dados, um serviço de e-mail, uma API de outra empresa, o próprio relógio do sistema. Testar com as dependências reais torna os testes lentos, frágeis e às vezes perigosos, porque ninguém quer enviar e-mails de verdade a cada build. Esta aula apresenta os dublês de teste, objetos que substituem as dependências reais durante os testes, e o Mockito, a biblioteca mais usada para criá-los. Na segunda metade, trata de uma pergunta desconfortável: como saber se os testes testam de verdade? Com ela, termina a Fase 5.

Um serviço com dependências

O projeto de tarefas ganha um serviço que envia lembretes das tarefas que vencem no dia seguinte:

public interface RepositorioDeTarefas {
    List<Tarefa> pendentes();
}

public interface Notificador {
    void enviar(String destinatario, String mensagem);
}
public class ServicoDeLembretes {
    private final RepositorioDeTarefas repositorio;
    private final Notificador notificador;
    private final Clock relogio;

    public ServicoDeLembretes(RepositorioDeTarefas repositorio, Notificador notificador, Clock relogio) {
        this.repositorio = repositorio;
        this.notificador = notificador;
        this.relogio = relogio;
    }

    public int enviarLembretes(String destinatario) {
        LocalDate amanha = LocalDate.now(relogio).plusDays(1);
        List<Tarefa> vencendo = repositorio.pendentes().stream()
            .filter(t -> amanha.equals(t.prazo()))
            .toList();
        for (Tarefa t : vencendo) {
            notificador.enviar(destinatario, "Amanhã vence: " + t.titulo());
        }
        return vencendo.size();
    }
}

O serviço tem três dependências, e todas chegam pelo construtor, a injeção de dependência de "Contratos sem Implementação":

  • o repositório, que em produção leria as tarefas de um banco de dados;
  • o notificador, que em produção enviaria e-mails;
  • um Clock, a classe de java.time que representa o relógio.

A terceira é a menos óbvia e uma das mais importantes. LocalDate.now(), sem argumentos, lê o relógio do sistema, e um teste que depende dele passa ou falha conforme o dia em que roda. LocalDate.now(relogio) lê o relógio recebido. Em produção, o serviço recebe Clock.systemDefaultZone(), o relógio real. Nos testes, recebe um relógio parado numa data conhecida. O mesmo vale para qualquer código que dependa da hora: nunca chame now() sem passar um Clock em código que precise ser testado.

Dublês de teste

Um dublê de teste é qualquer objeto que substitui uma dependência real durante um teste, como um dublê substitui o ator numa cena de risco. Gerard Meszaros classificou os tipos mais comuns, e os nomes aparecem em toda a literatura:

  • Dummy: um objeto que só preenche um parâmetro e nunca é usado de fato.
  • Stub: devolve respostas prontas às chamadas. Serve para alimentar o código testado com os dados do cenário.
  • Spy: um stub que também registra as chamadas que recebeu, para serem conferidas depois.
  • Mock: um objeto programado com expectativas sobre as chamadas que deve receber, e que as verifica.
  • Fake: uma implementação que funciona de verdade, mas simplificada, como um repositório que guarda as tarefas num ArrayList em vez de num banco de dados.

Na prática, muitos programadores chamam tudo de "mock", mas a distinção ajuda a pensar: um dublê existe para fornecer dados ao código testado, ou para verificar o que o código fez com ele?

Dublês escritos à mão

Antes de qualquer biblioteca, os dublês podem ser escritos à mão, e muitas vezes é a melhor opção:

class ServicoDeLembretesTest {

    private static final ZoneId SAO_PAULO = ZoneId.of("America/Sao_Paulo");
    private static final Clock RELOGIO =
        Clock.fixed(Instant.parse("2026-09-25T12:00:00Z"), SAO_PAULO);
    private static final LocalDate HOJE = LocalDate.of(2026, 9, 25);

    static class NotificadorRegistrador implements Notificador {
        final List<String> enviadas = new ArrayList<>();

        @Override
        public void enviar(String destinatario, String mensagem) {
            enviadas.add(destinatario + ": " + mensagem);
        }
    }

    @Test
    void enviaUmLembretePorTarefaQueVenceAmanha() {
        RepositorioDeTarefas repositorio = () -> List.of(
            new Tarefa(1, "Pagar conta", HOJE.plusDays(1), false),
            new Tarefa(2, "Renovar seguro", HOJE.plusDays(1), false),
            new Tarefa(3, "Estudar", HOJE.plusDays(5), false),
            new Tarefa(4, "Sem prazo", null, false));
        NotificadorRegistrador notificador = new NotificadorRegistrador();
        ServicoDeLembretes servico = new ServicoDeLembretes(repositorio, notificador, RELOGIO);

        int enviados = servico.enviarLembretes("ana@exemplo.com");

        assertEquals(2, enviados);
        assertThat(notificador.enviadas).containsExactly(
            "ana@exemplo.com: Amanhã vence: Pagar conta",
            "ana@exemplo.com: Amanhã vence: Renovar seguro");
    }

    @Test
    void semTarefasVencendoNadaEhEnviado() {
        RepositorioDeTarefas repositorio = () -> List.of(
            new Tarefa(1, "Estudar", HOJE.plusDays(5), false));
        NotificadorRegistrador notificador = new NotificadorRegistrador();

        int enviados = new ServicoDeLembretes(repositorio, notificador, RELOGIO)
            .enviarLembretes("ana@exemplo.com");

        assertEquals(0, enviados);
        assertThat(notificador.enviadas).isEmpty();
    }
}

Três dublês, três técnicas:

  • O repositório é um stub criado com uma lambda. RepositorioDeTarefas tem um único método abstrato, então é uma interface funcional, e uma lambda que devolve a lista do cenário basta. Interfaces pequenas não são só um bom projeto: elas tornam os testes triviais.
  • O notificador é um spy escrito à mão, a mesma ideia do NotificadorRegistrador do exercício 3 de "Contratos sem Implementação". Ele registra as mensagens, e o teste as confere.
  • O relógio é um stub da própria biblioteca. Clock.fixed cria um relógio parado num instante e num fuso. Ao meio-dia em UTC do dia 25, em São Paulo são 9 horas do mesmo dia, e "amanhã" é o dia 26, sempre, não importa quando o teste rode.

Os cenários cobrem as fronteiras: duas tarefas vencendo amanhã, uma vencendo depois, uma sem prazo, e nenhuma vencendo.

Mockito

Escrever dublês à mão funciona bem para interfaces pequenas. Com interfaces grandes, ou quando cada teste precisa de um comportamento diferente, o trabalho se acumula. O Mockito cria dublês automaticamente, em tempo de execução, usando por baixo a geração de classes e a reflexão de "O Programa que se Examina". As dependências no pom.xml:

<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>5.18.0</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-junit-jupiter</artifactId>
    <version>5.18.0</version>
    <scope>test</scope>
</dependency>

Os mesmos testes, com o Mockito:

import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.*;

@ExtendWith(MockitoExtension.class)
class ServicoDeLembretesMockitoTest {

    private static final Clock RELOGIO =
        Clock.fixed(Instant.parse("2026-09-25T12:00:00Z"), ZoneId.of("America/Sao_Paulo"));
    private static final LocalDate HOJE = LocalDate.of(2026, 9, 25);

    @Mock
    RepositorioDeTarefas repositorio;

    @Mock
    Notificador notificador;

    @Test
    void enviaUmLembretePorTarefaQueVenceAmanha() {
        when(repositorio.pendentes()).thenReturn(List.of(
            new Tarefa(1, "Pagar conta", HOJE.plusDays(1), false),
            new Tarefa(2, "Estudar", HOJE.plusDays(5), false)));
        ServicoDeLembretes servico = new ServicoDeLembretes(repositorio, notificador, RELOGIO);

        int enviados = servico.enviarLembretes("ana@exemplo.com");

        assertEquals(1, enviados);
        verify(notificador).enviar("ana@exemplo.com", "Amanhã vence: Pagar conta");
        verifyNoMoreInteractions(notificador);
    }

    @Test
    void semTarefasPendentesNenhumaNotificacaoEhEnviada() {
        when(repositorio.pendentes()).thenReturn(List.of());

        new ServicoDeLembretes(repositorio, notificador, RELOGIO).enviarLembretes("ana@exemplo.com");

        verify(notificador, never()).enviar(anyString(), anyString());
    }
}
  • @ExtendWith(MockitoExtension.class) integra o Mockito ao JUnit. Com ela, cada campo anotado com @Mock recebe, antes de cada teste, um dublê novo que implementa a interface. Sem a anotação, o mesmo se faz com Notificador notificador = mock(Notificador.class);.
  • when(...).thenReturn(...) programa uma resposta: "quando pendentes() for chamado, devolva esta lista". Métodos não programados devolvem valores neutros: null, zero, false ou uma coleção vazia.
  • verify(dublê).metodo(argumentos) confere que o método foi chamado com aqueles argumentos. verify(dublê, times(2)) confere o número de chamadas, e never(), que não houve nenhuma.
  • verifyNoMoreInteractions confere que não houve nenhuma outra chamada além das verificadas.
  • Matchers como anyString(), any() e eq(valor) aceitam qualquer argumento de um tipo, ou um valor específico. Quando um argumento usa matcher, todos os argumentos da mesma chamada precisam usar.

Quando o argumento de uma chamada é um objeto construído dentro do código testado, e o teste precisa examiná-lo em detalhe, o Mockito oferece o ArgumentCaptor, que captura o valor passado:

ArgumentCaptor<String> mensagem = ArgumentCaptor.forClass(String.class);
verify(notificador).enviar(eq("ana@exemplo.com"), mensagem.capture());
assertThat(mensagem.getValue()).startsWith("Amanhã vence:");

Um aviso prático. Desde o Java 21, a JVM avisa quando uma ferramenta carrega dinamicamente um agente de instrumentação, que é o que o Mockito faz para criar os dublês, e versões futuras devem bloquear isso por padrão. A documentação do Mockito recomenda registrá-lo explicitamente como agente na configuração do Surefire, obtendo o caminho do JAR com o objetivo properties do maven-dependency-plugin:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <configuration>
        <argLine>-javaagent:${org.mockito:mockito-core:jar}</argLine>
    </configuration>
</plugin>

Confira a seção sobre o Java 21 na documentação da versão do Mockito que você usar, porque os detalhes dessa configuração vêm mudando.

Stubs ou mocks? Estado ou comportamento

As duas versões dos testes verificam coisas parecidas de formas diferentes. A escrita à mão verifica o estado final: o spy guardou as mensagens, e o teste confere a lista. A do Mockito verifica o comportamento: o teste confere que certas chamadas aconteceram.

A verificação de comportamento é a ferramenta certa quando a chamada é o efeito que interessa, como enviar uma notificação, gravar um registro ou cobrar um cartão. Nesses casos, não há outro resultado para conferir. O perigo está em usá-la para tudo. Compare:

verify(repositorio).pendentes();                 // verifica COMO o serviço trabalha
verify(notificador).enviar("ana@exemplo.com", "Amanhã vence: Pagar conta");   // verifica O QUE ele faz

A primeira verificação não diz nada sobre o que o serviço deve fazer. Ela prende o teste à implementação: se amanhã o serviço passar a usar um método vencendoEm(data) do repositório, mais eficiente, o comportamento visível continua idêntico, e mesmo assim o teste quebra. Testes presos à implementação atrapalham exatamente o que os testes deveriam facilitar, a melhoria do código. A regra prática: use stubs para fornecer dados e verifique chamadas só quando a chamada for o próprio resultado.

O excesso de mocks também é um sinal de projeto. Se um teste precisa de seis dublês programados para rodar, a classe testada provavelmente tem responsabilidades demais. E há coisas que nunca devem ser substituídas por dublês: valores, como records, String, List e LocalDate. Crie-os de verdade. Um mock de List para testar código que recebe uma lista é pura complicação.

Testes que mentem

Um teste pode passar sem verificar nada. Os casos mais comuns:

O teste sem asserção. Um teste que chama o código e não confere o resultado passa sempre, a menos que haja uma exceção. Ele dá a sensação de cobertura sem nenhuma proteção.

O teste que testa o dublê. when(repositorio.pendentes()).thenReturn(lista); assertEquals(lista, repositorio.pendentes()); verifica que o Mockito funciona, e nada sobre o seu código.

O teste instável (flaky). Um teste que às vezes passa e às vezes falha, sem mudança no código, por depender do relógio, da ordem dos testes, de estado compartilhado ou de uma rede. Ele ensina a equipe a ignorar falhas ("roda de novo que passa"), e no dia em que um defeito real aparecer, será ignorado também. Um teste instável deve ser consertado ou removido, nunca tolerado.

O teste que repete a implementação. Um teste que calcula o resultado esperado com a mesma fórmula do código testado concorda com ele até nos erros. O esperado deve vir de fora: um valor calculado à mão, uma especificação, um exemplo conhecido.

Cobertura: medir o que foi executado

A cobertura de código mede quanto do código foi executado pelos testes: que linhas, que ramos de cada if. A ferramenta padrão no Java é o JaCoCo, integrado ao Maven por um plugin com os objetivos prepare-agent, que instrumenta a execução dos testes, e report, que gera um relatório em HTML (em target/site/jacoco/index.html), com cada linha pintada de verde, amarelo ou vermelho. Use uma versão do JaCoCo que suporte o Java 25, porque versões antigas falham ao instrumentar classes compiladas para versões novas.

O relatório é útil para encontrar o que não foi testado: um ramo vermelho é um comportamento sem nenhuma verificação. Mas cobertura alta não significa testes bons. Um teste sem asserções executa as linhas, pinta tudo de verde e não verifica nada. Metas rígidas de cobertura, como "90% ou o build falha", costumam produzir exatamente esse tipo de teste. Use a cobertura como lanterna, e não como meta.

Testes de mutação: testando os testes

Se a cobertura não diz se os testes verificam alguma coisa, o que diz? Os testes de mutação. Uma ferramenta, como o PIT (org.pitest:pitest-maven), modifica o código de propósito, criando mutantes: troca < por <=, plusDays(1) por plusDays(0), um return vencendo.size() por return 0, remove uma chamada. Para cada mutante, roda os testes. Se algum teste falha, o mutante foi morto, porque os testes perceberam a alteração. Se todos passam, o mutante sobreviveu, e isso revela um comportamento que os testes não verificam de verdade.

No serviço de lembretes, o mutante que troca return vencendo.size() por return 0 é morto pela asserção assertEquals(2, enviados). Sem essa asserção, com os testes verificando só as mensagens, ele sobreviveria, e o relatório mostraria que o valor devolvido pelo método nunca é conferido. A análise de mutação é mais lenta que a cobertura, porque roda os testes muitas vezes, mas é a medida mais honesta da qualidade de uma coleção de testes.

Além dos testes, ferramentas de análise estática examinam o código sem executá-lo, em busca de padrões de erro conhecidos: o Error Prone, do Google, que roda como plugin do compilador, e o SpotBugs encontram coisas como comparações de String com ==, equals sem hashCode e exceções ignoradas, vários dos erros que este curso vem apontando. São verificações baratas, que rodam em todo build.

Com esta aula termina a quinta fase. O código saiu dos arquivos soltos e virou um projeto: pacotes que organizam e escondem, JARs que empacotam, Maven e Gradle que baixam as dependências e constroem tudo com um comando, módulos que declaram as fronteiras entre as partes, e testes que verificam automaticamente, a cada build, que o projeto continua funcionando. É a infraestrutura de qualquer projeto Java profissional, e tudo o que vem daqui em diante no curso assume que ela existe.

Há uma parte do Java que, até aqui, foi tratada como caixa-preta: a própria JVM. O curso mencionou o JIT, o coletor de lixo, a pilha e o heap, sempre com a promessa de explicações futuras. Mesmo os testes desta aula dependem dela, com o Mockito gerando classes durante a execução, e as medições de desempenho de "Agrupar, Achatar, Reduzir" já esbarravam no aquecimento da JVM. Entender como a máquina virtual carrega, executa e otimiza o seu código, e como ela gerencia a memória por você, explica boa parte do que o curso mostrou até aqui sem explicar.

Fontes e leituras recomendadas

Exercícios

Exercício 1

Classifique cada dublê usado nesta aula segundo a taxonomia de Meszaros: o repositório criado com lambda, o NotificadorRegistrador, o Clock.fixed, o @Mock Notificador usado com verify, e um repositório que guardasse as tarefas num ArrayList com métodos de adicionar e buscar.

Ver resposta

✓ Resposta: O repositório com lambda é um stub: devolve uma resposta pronta e não registra nada. O NotificadorRegistrador é um spy: aceita as chamadas e as registra para verificação posterior, que o teste faz inspecionando a lista. O Clock.fixed é um stub: fornece sempre o mesmo instante (alguns o chamariam de fake, por ser uma implementação completa e funcional de Clock). O @Mock Notificador usado com verify é um mock: o teste verifica as chamadas que ele recebeu. O mesmo objeto do Mockito, usado só com when(...).thenReturn(...), seria um stub. O papel é definido pelo uso, e não pela ferramenta. O repositório com ArrayList seria um fake: uma implementação que funciona de verdade, de forma simplificada, útil quando vários testes precisam de um repositório que se comporte como o real, com gravações e leituras coerentes entre si.

Exercício 2

O relógio dos testes está parado às 12h de 25/09/2026 em UTC. O que o serviço calcularia como "amanhã" se o relógio estivesse às 02h UTC do mesmo dia, com o fuso de São Paulo? E com o fuso UTC? Por que um teste desse cenário é valioso?

Ver resposta

✓ Resposta: Às 02h UTC de 25/09, em São Paulo (UTC−3) ainda são 23h do dia 24. Com o fuso de São Paulo, LocalDate.now(relogio) devolve 24/09, e "amanhã" é 25/09. Com o fuso UTC, a data é 25/09, e "amanhã" é 26/09. O mesmo instante produz lembretes para dias diferentes conforme o fuso. Um teste com esse cenário, perto da meia-noite, documenta e protege uma regra de negócio que costuma ser decidida por acidente: "amanhã" é o amanhã do usuário, no fuso dele, e não o do servidor. Se o serviço rodar num servidor configurado em UTC, como é comum em nuvem, e usar o fuso do sistema, os usuários brasileiros recebem, entre 21h e meia-noite, lembretes das tarefas de depois de amanhã. Com o Clock injetado, esse cenário é testável em milissegundos, e sem ele seria preciso rodar o teste de madrugada.

Exercício 3

Um colega escreveu o teste abaixo. Explique por que ele é frágil e reescreva a verificação.

@Test
void enviaLembretes() {
    when(repositorio.pendentes()).thenReturn(List.of(
        new Tarefa(1, "Pagar conta", HOJE.plusDays(1), false)));

    new ServicoDeLembretes(repositorio, notificador, RELOGIO).enviarLembretes("ana@exemplo.com");

    verify(repositorio, times(1)).pendentes();
    verify(notificador, times(1)).enviar(anyString(), anyString());
}
Ver resposta

✓ Resposta: A primeira verificação testa a implementação: que o serviço chama pendentes() exatamente uma vez. Não é um requisito do serviço, e se ele passar a usar outro método do repositório, ou a fazer duas consultas por algum motivo legítimo, o teste quebra sem que o comportamento tenha mudado. A segunda verificação é fraca demais: aceita qualquer destinatário e qualquer mensagem, e continuaria passando se o serviço enviasse a mensagem errada, para a pessoa errada. Reescrita:

verify(notificador).enviar("ana@exemplo.com", "Amanhã vence: Pagar conta");
verifyNoMoreInteractions(notificador);

Agora o teste verifica o efeito que interessa, uma notificação específica, para a pessoa certa, e nenhuma outra, e não diz nada sobre como o serviço obtém as tarefas. O nome também poderia ser mais preciso, como enviaUmLembreteParaATarefaQueVenceAmanha.

Exercício 4

Um teste de mutação cria o mutante que troca plusDays(1) por plusDays(0) em enviarLembretes. Esse mutante é morto pelos testes da aula? E o mutante que remove a linha notificador.enviar(...)? Qual asserção mata cada um?

Ver resposta

✓ Resposta: Com plusDays(0), o serviço procura tarefas que vencem hoje. No teste enviaUmLembretePorTarefaQueVenceAmanha, nenhuma tarefa do cenário vence hoje (o dia 25): duas vencem no dia 26, uma no dia 30 e uma não tem prazo. O mutante devolve 0 e não envia nada, e morre em assertEquals(2, enviados) e na verificação das mensagens. O mutante que remove a chamada ao notificador devolve 2, porque a contagem continua certa, e sobreviveria a um teste que verificasse só o retorno. Ele morre em assertThat(notificador.enviadas).containsExactly(...), que exige as duas mensagens. O exercício mostra por que o teste confere os dois efeitos do método: o valor devolvido e as notificações. Cada asserção mata mutantes que a outra deixaria passar.

Exercício 5

O teste abaixo passou por meses e, numa madrugada, falhou no servidor de build. Ninguém mexeu no código. Explique o provável motivo e corrija o serviço e o teste.

@Test
void lembreteParaTarefaDeAmanha() {
    RepositorioDeTarefas repo = () -> List.of(
        new Tarefa(1, "X", LocalDate.now().plusDays(1), false));
    NotificadorRegistrador n = new NotificadorRegistrador();

    new ServicoDeLembretes(repo, n, Clock.systemDefaultZone()).enviarLembretes("a@b.c");

    assertThat(n.enviadas).hasSize(1);
}
Ver resposta

✓ Resposta: O teste lê o relógio real duas vezes: ao montar o cenário, com LocalDate.now(), e dentro do serviço, com LocalDate.now(Clock.systemDefaultZone()). Se o teste começar às 23:59:59,999 e o serviço ler o relógio já na meia-noite, o cenário diz "amanhã é o dia 26" e o serviço diz "amanhã é o dia 27", e nenhum lembrete é enviado. É um teste instável clássico: falha raramente, sem padrão aparente, só quando roda na virada do dia. Servidores de build rodam muito de madrugada. O serviço já está correto, porque recebe o Clock. O defeito está no teste, que entrega o relógio real. A correção usa um relógio fixo nos dois lugares:

private static final Clock RELOGIO =
    Clock.fixed(Instant.parse("2026-09-25T12:00:00Z"), ZoneId.of("America/Sao_Paulo"));

@Test
void lembreteParaTarefaDeAmanha() {
    LocalDate hoje = LocalDate.now(RELOGIO);
    RepositorioDeTarefas repo = () -> List.of(new Tarefa(1, "X", hoje.plusDays(1), false));
    NotificadorRegistrador n = new NotificadorRegistrador();

    new ServicoDeLembretes(repo, n, RELOGIO).enviarLembretes("a@b.c");

    assertThat(n.enviadas).containsExactly("a@b.c: Amanhã vence: X");
}

Agora o teste dá o mesmo resultado a qualquer hora, em qualquer fuso de servidor. A asserção também ficou mais precisa, conferindo a mensagem, e não só a quantidade. A lição: qualquer dependência não controlada, seja o relógio, a ordem de execução ou a rede, é uma fonte de instabilidade, e deve ser trocada por um dublê.

Comentários

Mais em Java

Construção Automatizada — Maven e Gradle
Construção Automatizada — Maven e Gradle

Como Maven e Gradle baixam dependências, compilam, testam e empacotam um…

Da Primeira Classe à JVM: o Mapa da Jornada em Java
Da Primeira Classe à JVM: o Mapa da Jornada em Java

O mapa do curso de Java: setenta e oito aulas em nove fases e três trilhas, do…

Dividir para Resolver — Métodos, Parâmetros e Recursão
Dividir para Resolver — Métodos, Parâmetros e Recursão

Como escrever métodos em Java: tipo de retorno, parâmetros, return, passagem…