Test Doubles - Stub, Spy, Mock e Fake
Por que existem dublês
Seção intitulada “Por que existem dublês”Um teste unitário precisa isolar a unidade sob teste de suas dependências — para ser rápido (Fast), determinístico (Repeatable) e falhar por um único motivo. Substituímos o colaborador real (banco, gateway HTTP, serviço de e-mail) por um dublê controlável. Isso só é possível quando a dependência é uma abstração injetável — ponto que amarra tudo à inversão de dependência, retomado ao final.
A taxonomia de Meszaros
Seção intitulada “A taxonomia de Meszaros”Meszaros classifica os dublês por como participam do teste, não por biblioteca. Martin Fowler resume a divisão em Mocks Aren’t Stubs.
graph TD
TD["Test Double"] --> DU["Dummy<br/>preenche parâmetro, nunca usado"]
TD --> ST["Stub<br/>responde com dados fixos"]
TD --> SP["Spy<br/>stub que registra o que recebeu"]
TD --> MO["Mock<br/>tem expectativas verificadas"]
TD --> FA["Fake<br/>implementação leve e funcional"]
- Dummy — objeto passado só para satisfazer a assinatura; nunca é de fato usado. Ex.: um
nullou um objeto vazio num parâmetro irrelevante para o caso. - Stub — fornece respostas pré-programadas a chamadas feitas durante o teste. Não tem lógica; só devolve o que você mandou devolver. Serve à verificação de estado.
- Spy — um stub que, além de responder, grava informações sobre como foi chamado (argumentos, número de vezes), permitindo inspeção posterior.
- Mock — objeto pré-programado com expectativas: uma especificação das chamadas que deve receber. A verificação dessas expectativas é o próprio critério de sucesso — verificação de comportamento.
- Fake — uma implementação real, porém simplificada, que funciona de verdade mas não serve para produção. O exemplo canônico é um repositório em memória.
Stub (estado) vs Mock (comportamento): a distinção que importa
Seção intitulada “Stub (estado) vs Mock (comportamento): a distinção que importa”Essa é a linha divisória conceitual. Com um stub, você prepara dados e no fim verifica o resultado/estado do sistema. Com um mock, você verifica que as interações certas aconteceram — quais métodos foram chamados, com quais argumentos, quantas vezes.
- Verificação de estado (stub): “dado que o repositório retorna este usuário, o método devolve o nome em maiúsculas?” O foco é a saída.
- Verificação de comportamento (mock): “ao concluir o pedido, o serviço de e-mail foi chamado exatamente uma vez com o endereço certo?” O foco é a colaboração.
Regra prática: prefira verificação de estado sempre que houver estado observável — é mais resistente a refactoring. Use verificação de comportamento quando o efeito relevante é justamente uma interação sem retorno observável (enviar e-mail, publicar evento, gravar em log de auditoria).
Exemplos com Mockito
Seção intitulada “Exemplos com Mockito”Considere um caso de uso que confirma um pedido e notifica o cliente:
public interface PedidoRepository { Optional<Pedido> buscar(PedidoId id); void salvar(Pedido pedido);}
public interface ServicoEmail { void enviarConfirmacao(String destinatario, PedidoId id);}
public class ConfirmarPedido {
private final PedidoRepository repository; private final ServicoEmail email;
public ConfirmarPedido(PedidoRepository repository, ServicoEmail email) { this.repository = repository; this.email = email; }
public void executar(PedidoId id) { Pedido pedido = repository.buscar(id) .orElseThrow(() -> new PedidoInexistenteException(id)); pedido.confirmar(); repository.salvar(pedido); email.enviarConfirmacao(pedido.emailCliente(), id); }}Stub + verificação de estado — when(...).thenReturn(...) prepara a resposta; o assert olha o estado do pedido:
@ExtendWith(MockitoExtension.class)class ConfirmarPedidoTest {
@Mock PedidoRepository repository; @Mock ServicoEmail email; @InjectMocks ConfirmarPedido useCase;
@Test void confirmaPedidoExistente() { var pedido = new Pedido(new PedidoId("42"), "cliente@x.com"); when(repository.buscar(new PedidoId("42"))).thenReturn(Optional.of(pedido));
useCase.executar(new PedidoId("42"));
assertThat(pedido.status()).isEqualTo(StatusPedido.CONFIRMADO); }}Mock + verificação de comportamento — verify(...) afirma que a interação ocorreu como esperado:
@Testvoid notificaClienteExatamenteUmaVez() { var pedido = new Pedido(new PedidoId("42"), "cliente@x.com"); when(repository.buscar(new PedidoId("42"))).thenReturn(Optional.of(pedido));
useCase.executar(new PedidoId("42"));
verify(email).enviarConfirmacao(eq("cliente@x.com"), eq(new PedidoId("42"))); verify(repository).salvar(pedido); verifyNoMoreInteractions(email);}Spy — atenção à colisão de nomes: o spy() do Mockito é um partial mock que envolve um objeto real, deixando-o funcionar de verdade e permitindo verificar/interceptar chamadas — conceito diferente do spy de Meszaros da taxonomia acima (um stub que grava como foi chamado). Mesmo nome, ideias distintas:
@Testvoid spyMantemComportamentoRealMasPermiteVerificar() { List<String> lista = spy(new ArrayList<>());
lista.add("café"); lista.add("filtro");
verify(lista).add("café"); assertThat(lista).hasSize(2);}Um Fake escrito à mão: InMemoryPedidoRepository
Seção intitulada “Um Fake escrito à mão: InMemoryPedidoRepository”Um Fake é uma implementação funcional e leve da mesma interface que o adapter real implementa. Aqui, um repositório em memória que substitui o de banco:
public class InMemoryPedidoRepository implements PedidoRepository {
private final Map<PedidoId, Pedido> dados = new ConcurrentHashMap<>();
@Override public Optional<Pedido> buscar(PedidoId id) { return Optional.ofNullable(dados.get(id)); }
@Override public void salvar(Pedido pedido) { dados.put(pedido.id(), pedido); }}Usado no teste, ele elimina a necessidade de mockar cada chamada — o comportamento de persistência “funciona”:
@Testvoid confirmaEPersistePedido() { var repository = new InMemoryPedidoRepository(); var email = mock(ServicoEmail.class); repository.salvar(new Pedido(new PedidoId("42"), "cliente@x.com")); var useCase = new ConfirmarPedido(repository, email);
useCase.executar(new PedidoId("42"));
assertThat(repository.buscar(new PedidoId("42")).orElseThrow().status()) .isEqualTo(StatusPedido.CONFIRMADO);}Fakes brilham quando há muita interação com a dependência: mockar cada when/verify viraria ruído, enquanto o Fake se comporta como o real de forma verificável por estado.
Ponto-chave: um Fake/Stub é um adapter de teste para uma porta
Seção intitulada “Ponto-chave: um Fake/Stub é um adapter de teste para uma porta”Aqui a peça se encaixa na arquitetura. PedidoRepository é uma porta — uma abstração pertencente ao domínio. Em produção, um JpaPedidoRepository é o adapter que a implementa. InMemoryPedidoRepository é outro adapter da mesma porta, só que de teste.
graph LR
UC["ConfirmarPedido<br/>(núcleo)"] --> P["PedidoRepository<br/>(porta)"]
A1["JpaPedidoRepository<br/>(adapter de produção)"] -.implementa.-> P
A2["InMemoryPedidoRepository<br/>(adapter de teste / Fake)"] -.implementa.-> P
Ou seja: a testabilidade não é um truque de biblioteca de mocking — ela vem da inversão de dependência (DIP). O núcleo depende da porta, não da implementação; por isso podemos plugar um dublê. Sem essa inversão, não haveria costura onde inserir o dublê. É a mesma simetria da Arquitetura Hexagonal e a razão de Repositórios serem definidos como interfaces no domínio. Aprofundamento em Testabilidade e Arquitetura.
Anti-padrões e erros comuns
Seção intitulada “Anti-padrões e erros comuns”- Supermocking: mockar tudo, inclusive objetos de valor e entidades de domínio. Objetos sem dependências externas devem ser usados reais; mockar domínio testa a fiação, não a regra.
- Testar o mock: quando o único assert é
verifyde uma chamada trivial, o teste replica a implementação e quebra a cada refatoração. - Verificação de comportamento onde estado bastaria: prende o teste à mecânica interna; prefira
assertsobre resultado. - Mock de tipo que você não possui (classes de framework/lib externa): frágil a mudanças da lib; envolva atrás de uma porta sua e mocke a porta.
- Stubs que mentem: retornar dados que a implementação real nunca retornaria dá falsa confiança; um Fake fiel evita isso.
- Confundir Mockito
mockcom “mock” de Meszaros:mock()do Mockito cria um objeto que serve tanto de stub (when) quanto de mock (verify) — o papel depende de como você o usa no teste.
Relações
Seção intitulada “Relações”- Testes e TDD
- Fundamentos da Automação de Testes
- Princípio FIRST
- Tipos de Teste - Unit, Integration e E2E
- Test-Driven Development
- Ports · Adapters · Arquitetura Hexagonal
- DIP - Dependency Inversion Principle · Repositórios
- Testabilidade e Arquitetura · fakes em ação: Testando o Núcleo
Perguntas de revisão
Seção intitulada “Perguntas de revisão”1. Qual a diferença semântica entre stub e mock?
Resposta
Um stub fornece respostas fixas e serve à verificação de estado — você checa o resultado observável no fim. Um mock carrega expectativas de interação e serve à verificação de comportamento — o critério de sucesso é que as chamadas certas ocorreram (via verify). Estado versus interação.
2. Por que um Fake de repositório é conceitualmente um adapter?
Resposta
Porque implementa a mesma porta (interface de domínio) que o adapter de produção. InMemoryPedidoRepository e JpaPedidoRepository são dois adapters da porta PedidoRepository; um serve testes, o outro produção. O núcleo não distingue qual está plugado.
3. De onde vem a testabilidade que permite inserir um dublê?
Resposta
Da inversão de dependência (DIP): o núcleo depende de uma abstração (porta), não de uma implementação concreta. Essa costura é o que permite plugar um dublê no lugar do adapter real. Sem a inversão, não haveria ponto de injeção — a testabilidade é uma propriedade do design, não da ferramenta de mocking.
4. O que é supermocking e por que é prejudicial?
Resposta
É mockar dependências que não precisavam ser mockadas, inclusive objetos de valor e entidades de domínio sem I/O. Isso substitui lógica real por comportamento simulado, de modo que o teste passa a verificar a fiação em vez da regra, ganha fragilidade a refactoring e perde poder de detecção de bugs reais.