Mocks: quando ajudam e quando escondem o bug
Um mock serve para cortar uma dependência que não se pode ou não se deve tocar num teste: a rede, o relógio, o fornecedor de pagamentos. Dito assim parece só conveniência. É uma troca — e vale a pena saber o que se dá em troca de quê.
1. O que um mock é, de facto
É uma suposição escrita em código. Ao dizer «quando chamarem isto, devolve aquilo», está a registar-se aquilo que se acredita que o sistema de fora faz.
A partir daí o teste deixa de verificar comportamento e passa a verificar coerência interna: passa porque a suposição é consistente consigo própria. Se a suposição estiver errada, o teste fica verde na mesma.
// O que o mock diz que a API devolve
mock(buscarUtilizador).resolves({ data: { nome: 'Ana' } });
// O que a API devolve mesmo
// { results: [{ name: 'Ana' }] }Teste verde, produção partida — e a falha aparece longe dali, num sítio onde ninguém se lembra deste ficheiro.
2. A escala, do melhor para o pior
Não precisar de nenhum. Uma função que recebe valores e devolve valores testa-se sem cerimónia.
A coisa a sério, quando é barata. Um Postgres em contentor arranca em segundos e não mente sobre o que o SQL faz.
Uma implementação falsa que nos pertence — um repositório em memória que cumpre a mesma interface. Falha como a verdadeira, e é nossa para corrigir.
Um stub que devolve dados enlatados. Já é uma suposição.
Um mock que verifica chamadas — «foi chamado uma vez, com estes argumentos». É o mais frágil de todos: prende o teste à forma como o código está escrito, e passa a partir-se em cada reorganização que não muda comportamento nenhum.
3. Este site não tem nenhum
Fui ver os testes e não há um único mock. Não foi disciplina: é consequência de onde a lógica ficou.
contarPalavras(html) // -> número
primeiraMedia(html) // -> a imagem ou o vídeo
realcarCodigo(html) // -> o mesmo html, com cor
resumirHtml(html, 300) // -> texto
tomMaisLivre([0, 30, 60]) // -> númeroRecebem um valor e devolvem outro. Não sabem que existe base de dados, nem pedido, nem sessão — e por isso não há nada para fingir.
É esse o sinal a reter: quando um teste precisa de cinco mocks para arrancar, o problema não está no teste. Está no facto de aquela lógica estar entalada entre coisas de que não precisava.
4. O que tem mesmo de ser falsificado
Sobra sempre alguma coisa: o relógio, o acaso, a rede. Para esses há uma escolha melhor do que substituir a função global — recebê-los como argumento.
// Difícil de testar: o relógio vem de dentro
function expirou(sessao) {
return Date.now() > sessao.fim;
}
// Fácil: o relógio entra pela porta
function expirou(sessao, agora = Date.now()) {
return agora > sessao.fim;
}A segunda versão testa-se em qualquer instante sem biblioteca nenhuma — e continua a escrever-se igual em produção.
5. E o problema do desvio
Nada disto resolve a questão de fundo: a API lá fora pode mudar e os nossos testes continuam verdes. As duas respostas sérias são gravar respostas reais como amostras e voltar a gravá-las de tempos a tempos, ou ter um teste — um só, lento, à parte dos outros — que fala mesmo com o serviço.
Qualquer uma delas é melhor do que a alternativa habitual, que é descobrir o desvio em produção.
O resumo
Antes de escrever um mock, duas perguntas. Consigo pôr esta lógica numa função que não sabe de onde vêm os dados? Se sim, o mock desaparece com o problema. Se não, a coisa a sério é assim tão cara? Muitas vezes não é — e um contentor que arranca em três segundos vale mais do que uma suposição que ninguém revê.