Mock que mente: teste verde, produção vermelha e o teste de contrato que evita
Use dublê para isolar a sua regra e um teste de contrato com resposta real gravada para descobrir quando o fornecedor muda antes do cliente descobrir.
Segunda de manhã, a página de checkout mostra "prazo de entrega: undefined dias". Todos os 312 testes passaram no deploy de sexta. O fornecedor de frete renomeou o campo prazo_dias para prazo_entrega_dias na API, e o mock da sua suíte continuou devolvendo o nome antigo, porque foi você quem escreveu o mock. A resposta curta: mock verifica a conversa do seu código com o que você imagina do mundo; teste de contrato verifica se o mundo ainda fala aquilo. Mantenha os dublês para isolar a sua regra, mas guarde uma resposta real gravada, valide-a com o mesmo código de produção e, de tempos em tempos, compare com o ambiente de testes do fornecedor.
O objetivo não é abandonar mocks. É saber exatamente qual pergunta cada teste responde e ter pelo menos um teste que falha quando a fronteira muda.
Por que o mock fica velho sem ninguém perceber
Um mock é uma cópia do comportamento de outro sistema, escrita por quem consome esse sistema. No dia em que foi escrito, ele era fiel. Depois, o fornecedor evolui a API, publica uma nota de versão que ninguém leu, e a cópia continua igual. Nada no seu repositório mudou, então nenhum teste tem motivo para falhar.
O problema piora quando o mock está espalhado. Cada teste monta a sua versão da resposta, com os campos que lembrava naquele dia. Em pouco tempo existem oito respostas diferentes do mesmo fornecedor, e nenhuma é a real. Os testes passam porque estão conferindo o código contra a imaginação coletiva da equipe.
Existe uma orientação antiga, popularizada pelo livro Growing Object-Oriented Software, Guided by Tests: não faça mock de tipos que você não possui. A ideia é criar uma interface sua, pequena, na fronteira, e fazer mock dela. O cliente HTTP do fornecedor fica atrás de um adaptador, e o adaptador é testado contra respostas reais.
Como dividir o teste em duas perguntas
- Crie um adaptador para o fornecedor. Ele chama a API, lê a resposta e devolve um objeto no formato do seu domínio. É o único lugar que conhece os nomes de campo do fornecedor.
- Faça o adaptador falhar alto quando um campo essencial falta ou vem com tipo errado. Silêncio vira
undefinedna tela. - Teste a sua regra com um dublê do adaptador, não da API HTTP. Esses testes são rápidos e respondem "minha regra está certa?".
- Grave uma resposta real do ambiente de testes do fornecedor, sem dados pessoais, e versione no repositório.
- Teste o adaptador contra a resposta gravada. Esse teste responde "meu código entende o que o fornecedor manda?".
- Agende uma verificação contra o ambiente de testes real, uma vez por dia, que roda o mesmo adaptador e alerta se falhar.
O adaptador que falha alto
No exemplo, a loja consulta a cotação de frete de uma transportadora fictícia, a TransLog.
// src/translog.js
function lerCotacao(resposta) {
const prazo = resposta.prazo_entrega_dias;
const valor = resposta.valor_centavos;
if (!Number.isInteger(prazo) || !Number.isInteger(valor)) {
throw new Error('Resposta da TransLog fora do contrato: ' + JSON.stringify(resposta));
}
return { prazoDias: prazo, valorCentavos: valor };
}
async function cotar(cep, pesoGramas, opcoes = {}) {
const url = (opcoes.baseUrl || process.env.TRANSLOG_URL) + '/v2/cotacao';
const resposta = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json', Authorization: 'Bearer ' + process.env.TRANSLOG_TOKEN },
body: JSON.stringify({ cep_destino: cep, peso_gramas: pesoGramas }),
signal: AbortSignal.timeout(5000),
});
if (!resposta.ok) throw new Error('TransLog respondeu ' + resposta.status);
return lerCotacao(await resposta.json());
}
module.exports = { lerCotacao, cotar };
Repare em duas coisas. O resto do sistema só conhece prazoDias e valorCentavos, nomes seus. E a leitura confere o tipo, então uma renomeação vira um erro com a resposta inteira no log, não um "undefined dias" na tela do cliente. O limite de tempo da chamada segue a lógica de timeout antes do infinito.
O teste de regra com dublê
// test/checkout.test.js
const test = require('node:test');
const assert = require('node:assert/strict');
const { montarResumo } = require('../src/checkout');
test('resumo soma o frete ao total e mostra o prazo', async () => {
const freteFalso = { cotar: async () => ({ prazoDias: 4, valorCentavos: 1890 }) };
const resumo = await montarResumo({ subtotalCentavos: 10000, cep: '01001-000' }, freteFalso);
assert.equal(resumo.totalCentavos, 11890);
assert.equal(resumo.textoPrazo, 'Chega em até 4 dias úteis');
});
Esse dublê devolve o formato do seu domínio, não o JSON da TransLog. Se a TransLog mudar, ele não precisa mudar, e está certo assim: ele testa a sua regra.
O teste de contrato com resposta gravada
// test/contratos/translog.test.js
const test = require('node:test');
const assert = require('node:assert/strict');
const { lerCotacao } = require('../../src/translog');
const exemplo = require('./translog-cotacao-2026-10.json');
test('adaptador entende a resposta real gravada da TransLog', () => {
const cotacao = lerCotacao(exemplo);
assert.ok(cotacao.prazoDias > 0);
assert.ok(cotacao.valorCentavos > 0);
});
O arquivo translog-cotacao-2026-10.json é a resposta que o ambiente de testes devolveu, copiada sem edição manual. O nome com a data diz quando foi capturada. Quando o fornecedor anunciar mudança, você grava a nova resposta, o teste quebra de propósito e você ajusta o adaptador antes do deploy.
A verificação diária contra o ambiente real
A resposta gravada envelhece igual ao mock, só que mais devagar. Para fechar o ciclo, um script roda o adaptador contra o ambiente de testes do fornecedor:
// scripts/verificar-translog.js
const { cotar } = require('../src/translog');
cotar('01001-000', 500, { baseUrl: process.env.TRANSLOG_SANDBOX_URL })
.then((c) => console.log('Contrato TransLog ok:', c))
.catch((erro) => {
console.error('Contrato TransLog quebrou:', erro.message);
process.exit(1);
});
Agende esse script no CI uma vez por dia, separado do build principal, e faça a falha abrir um alerta para quem cuida da integração. Ele não deve bloquear o deploy de quem mexeu em outra parte do sistema; a função dele é avisar cedo. Para integrações entre serviços da própria empresa, ferramentas de contrato orientado ao consumidor, como o Pact, formalizam esse acordo dos dois lados.
Erros comuns
- Mockar o cliente HTTP em cada teste. Cada teste carrega uma versão da resposta, e nenhuma delas acompanha o fornecedor.
- Escrever a resposta de exemplo à mão. Ela reflete o que você leu na documentação, não o que a API manda; nomes, tipos e campos nulos divergem.
- Adaptador que aceita qualquer coisa. Campo ausente vira
undefinede só aparece na tela do cliente, longe do ponto onde o problema nasceu. - Gravar resposta com dado pessoal ou token. O arquivo vai para o repositório e o vazamento vai junto; limpe antes de versionar.
- Colocar a chamada ao ambiente real no build de todo commit. Quando o fornecedor cai, o seu deploy para, sem relação com o que mudou.
- Confiar só no ambiente de homologação. Ele ajuda, mas se for diferente de produção também mente; o post ambiente de teste que não parece produção mostra onde.
Checklist
- Cada fornecedor externo fica atrás de um adaptador seu.
- O adaptador valida campos essenciais e falha com a resposta no log.
- Testes de regra usam dublê do adaptador, no formato do seu domínio.
- Existe uma resposta real gravada, datada e sem dados pessoais, para cada operação usada.
- O adaptador tem teste contra essa resposta gravada.
- Uma verificação agendada roda contra o ambiente de testes do fornecedor e alerta quando falha.
- Alguém da equipe recebe as notas de versão da API do fornecedor.
Pense na última vez que a produção quebrou por causa de um fornecedor: algum teste seu poderia ter falhado antes, ou todos eles conversavam só com o mock que a equipe escreveu?
Referência: https://martinfowler.com/bliki/ContractTest.html