Todos os artigos
139 artigos · atualizado semanalmente Veja nossas Ferramentas
Todos os artigos
Dicas

Mocks, stubs e fakes: as diferenças que importam

Dummy, stub, fake, mock e spy não são sinônimos. Entenda o divisor real entre eles, state x behavior verification, e quando usar cada test double sem cair no over-mocking.

Mocks, stubs e fakes: as diferenças que importam
COVER · Dicas

Você abre o pull request, o CI estava verde ontem, e hoje está vermelho. Ninguém mexeu no código. O teste que quebrou chama a API de pagamento de verdade, e o gateway saiu do ar por dois minutos. Do outro lado da mesma base, tem um colega reclamando que "mockei tudo e o teste passa, mas não testa nada" — refatorou a função inteira por dentro e nenhum teste reclamou. Os dois problemas têm a mesma raiz: confusão sobre que tipo de test double usar e quando.

Este artigo separa dummy, stub, fake, mock e spy, mostra o divisor real entre eles (state verification x behavior verification) e dá uma opinião firme sobre quando usar cada um.

O termo guarda-chuva: test double

"Test double" é o termo cunhado por Gerard Meszaros e popularizado por Martin Fowler para qualquer objeto que você coloca no lugar de uma dependency real durante um teste. A analogia é com dublê de cinema: alguém que entra no lugar do ator pra cena perigosa. Mock, stub, fake — são todos test doubles. O problema é que no dia a dia a galera chama tudo de "mock", e aí a conversa vira ruído.

Vale fixar os cinco tipos antes de discutir quando usar cada um. A ordem abaixo vai do mais burro pro mais esperto: dummy, stub, fake, mock e spy.

Dummy: só preenche um parâmetro

O dummy é o mais simples. Ele existe só pra satisfazer a assinatura de um método — você precisa passar alguma coisa, mas aquele argumento nunca é usado no caminho que o teste exercita.

// um logger dummy que nao faz nada
const dummyLogger = { log() {} };
service.process(pedido, dummyLogger);

Se o process nem chega a logar nesse cenário, o dummyLogger está ali só pra compilar. Você não verifica nada nele. Na prática, null às vezes resolve, mas em linguagens que reclamam de tipo um dummy explícito é mais limpo e deixa a intenção clara pra quem lê o teste depois.

Stub: devolve resposta pronta

O stub é um test double que devolve valores fixos (canned answers) pras chamadas feitas durante o teste. Ele alimenta o objeto sob teste com dados controlados, e ponto. Você não pergunta se ele foi chamado — você só usa o que ele devolve.

// stub do repositorio: sempre devolve o mesmo usuario
const userRepoStub = {
  findById: () => ({ id: 7, plan: "free" }),
};
const result = checkout.run(userRepoStub);
expect(result.discount).toBe(0); // verifica o ESTADO

Repare no assert: ele olha o resultado (result.discount), não a interação com o stub. Isso é state verification — você pergunta "dada essa entrada, qual o estado de saída?". O stub é perfeito pra forçar caminhos: usuário sem plano, lista vazia, resposta de erro da API. Você programa a resposta e observa o que o sistema faz com ela.

Fake: implementação leve que funciona

O fake é um test double com lógica de verdade — uma implementação funcional, só que simplificada e inadequada pra produção. O exemplo clássico é o in-memory repository: ele guarda registros num array ou num Map em vez de ir ao Postgres, mas se comporta como um repositório de verdade. Você insere, busca, deleta, e ele responde coerente.

class InMemoryUserRepo {
  #users = new Map();
  save(u) { this.#users.set(u.id, u); }
  findById(id) { return this.#users.get(id) ?? null; }
}

A diferença pro stub: o stub devolve sempre a mesma coisa, sem estado interno; o fake tem comportamento real, com estado que evolui. Se você salvar e depois buscar, o fake devolve o que você salvou. Isso o torna ótimo pra testar fluxos com várias operações encadeadas sem subir um banco. A verificação continua sendo de estado: você age e depois inspeciona o resultado.

Mock: pré-programado com expectativas

Aqui muda a filosofia. O mock é um objeto pré-programado com expectativas sobre as chamadas que ele deveria receber. Ele não está ali só pra devolver dado — ele está ali pra verificar que foi usado do jeito certo. Se a chamada esperada não acontecer, ou acontecer com argumentos errados, o teste falha.

const mailerMock = createMock();
mailerMock.expects("send")
  .once()
  .with({ to: "ana@x.com", template: "welcome" });

signup.register({ email: "ana@x.com" });

mailerMock.verify(); // falha se "send" nao foi chamado certo

Esse verify() no fim é a assinatura do mock. Você não olha o estado do sistema — você afirma que uma interação específica aconteceu. Isso é behavior verification: "o objeto sob teste conversou com a dependency da forma combinada?". Útil quando o efeito do código é justamente a chamada externa (mandar email, publicar evento, debitar cartão) e não há estado de retorno relevante pra inspecionar.

Spy: registra as chamadas

O spy fica no meio do caminho. Ele grava o que aconteceu — quantas vezes foi chamado, com quais argumentos — e deixa você afirmar isso depois, no corpo do teste, em vez de pré-programar expectativas como o mock faz.

const spy = createSpy();
notifier.onError = spy;
run();
expect(spy.calls.length).toBe(1);
expect(spy.calls[0].args[0]).toMatch(/timeout/);

Muitos frameworks misturam spy e mock no mesmo objeto (o jest.fn(), o Mock do unittest.mock), e por isso a fronteira fica borrada na prática. Conceitualmente: o spy observa e você faz o assert depois; o mock já vem com a expectativa embutida e cobra no verify.

O divisor real: estado x comportamento

Esqueça por um segundo os cinco nomes. Só existem duas escolas de verificação:

  • State verification (stub, fake): você roda a ação e inspeciona o resultado ou o estado final. Não importa como o objeto chegou lá, importa onde chegou.
  • Behavior verification (mock, spy): você afirma que certas chamadas aconteceram, com certos argumentos, numa certa ordem. Importa como o objeto interagiu com as dependencies.

Essa é a única distinção que muda o desenho do seu teste. Dummy, stub e fake servem state verification. Mock e spy servem behavior verification. Decorar os nomes não adianta nada se você não decidir primeiro qual tipo de verificação aquele teste precisa. Se você ainda está na dúvida do que cada teste deveria afirmar, o irmão deste artigo, testes unitários: o que realmente testar, ataca exatamente isso.

A armadilha do over-mocking

Aqui mora a maioria das dores. Quando você mocka cada colaborador e afirma cada chamada, o teste vira um espelho da implementação. Trocou a ordem de duas chamadas inofensivas? Teste vermelho. Extraiu um método privado? Teste vermelho. Você não testou comportamento — congelou a estrutura interna do código. Isso é o teste acoplado à implementação: frágil, caro de manter, e que dá uma falsa sensação de cobertura. O colega que "mockou tudo e o teste não testa nada" caiu nisso.

O sintoma clássico: você refatora sem mudar o comportamento observável e metade da suite quebra. Teste bom quebra quando o comportamento muda, não quando a estrutura muda.

Minha opinião depois de muito CI vermelho

Prefira fake e stub. Eles testam comportamento observável e sobrevivem a refactor. Use mock estrito com parcimônia — só quando a interação é o comportamento (efeitos colaterais sem retorno: publicar evento, enviar notificação, debitar). E não mocke o que você não possui: embrulhe a API externa num adapter e dubla o adapter, nunca a lib de terceiros direto.

Pra montar um fake ou stub que valha alguma coisa, você precisa de dados de exemplo realistas — gerar um JSON de mock com a forma certa acelera essa parte (gerador de JSON de mock).

Perguntas frequentes

Como saber se estou mockando demais?

Olhe pros asserts. Se a maioria deles é verify(chamada X aconteceu) e quase nenhum olha resultado ou estado, você está amarrado à implementação. Tente reescrever o teste pra afirmar só o resultado observável — se não der, talvez aquela dependency devesse ser um fake, não um mock. A regra prática: um teste que quebra quando você refatora sem mudar comportamento está testando estrutura, não comportamento.

Devo usar mock ou stub pra essa dependency?

Pergunte: o efeito que importa é o retorno (então stub ou fake, com state verification) ou a chamada em si (então mock ou spy, com behavior verification)? Mandar email só importa porque a chamada acontece — mock cabe. Calcular desconto importa pelo número que sai — stub cabe. Quando os dois parecem caber, fique com o stub: ele acopla menos o teste à implementação.

Posso mockar a biblioteca de terceiros direto?

Pode, mas evite. Mockar o SDK da Stripe ou o client HTTP te prende ao formato deles, que você não controla e que muda sem te avisar. Quando eles mudam, seu mock continua verde mentindo enquanto a produção quebra. Embrulhe a dependency num adapter seu e use fake ou stub no adapter — você passa a dublar uma interface que você é dono.

Qual a diferença entre fake e stub na prática?

O stub devolve sempre a mesma resposta pronta, sem memória; o fake tem estado interno e comportamento real, só que simplificado. Se o teste salva um registro e depois busca esperando recuperá-lo, você precisa de um fake — um stub não lembraria do que foi salvo. Para um único valor de retorno num caminho específico, o stub é mais simples e suficiente.

Pra fechar

Test double é o guarda-chuva; embaixo dele, o dummy preenche, o stub responde, o fake funciona, o mock cobra e o spy observa. O que de fato decide o desenho do teste é a escolha entre state verification (stub/fake) e behavior verification (mock/spy) — escolha isso primeiro, o nome vem depois. Na dúvida, vá de fake ou stub, reserve mocks estritos pros efeitos colaterais, e jamais mocke uma dependency que você não controla sem embrulhar antes.

RD
Autor
Rafael Duarte
Desenvolvedor backend com passagem por fintech e SaaS B2B — trabalhou em times que escalaram APIs de zero a milhões de requisições. Carrega cicatrizes de produção suficientes para ter opiniões fortes sobre ferramentas, padrões e decisões de arquitetura. Não é acadêmico: leu a RFC do UUID quando precisou escolher entre v4 e v7 para uma tabela de alta escrita.
Ver perfil