Pular para o conteúdo

Test Doubles - Stub, Spy, Mock e Fake

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.

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 null ou 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).

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 estadowhen(...).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 comportamentoverify(...) afirma que a interação ocorreu como esperado:

@Test
void 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:

@Test
void spyMantemComportamentoRealMasPermiteVerificar() {
List<String> lista = spy(new ArrayList<>());
lista.add("café");
lista.add("filtro");
verify(lista).add("café");
assertThat(lista).hasSize(2);
}

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”:

@Test
void 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.

  • 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 é verify de 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 assert sobre 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 mock com “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.

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.