Convidado Misterioso.
Um teste cujas entradas ou resultados esperados ficam em um recurso externo — um arquivo, um seed de banco de dados ou uma fixture compartilhada — de modo que você não consegue entender nem confiar no teste apenas lendo-o.
##Signs and Symptoms
Você não consegue entender um teste apenas lendo-o: os dados que o conduzem (e que justificam suas asserções) ficam em algum lugar fora da tela — um arquivo CSV/JSON, um script de seed de banco de dados, um módulo de fixture compartilhada ou uma anotação de data-fixture do framework. O teste faz referência a um recurso opaco e então faz asserções sobre valores "mágicos" cujo significado está oculto nesse recurso.
Sinais reveladores:
- O corpo chama algo como
readFileSync('fixtures/subscribers.csv'),loadSeed('invoices.sql'),getResource(...)ou umbeforeAllglobal que popula um banco de dados compartilhado. - Os valores esperados parecem arbitrários (
toHaveLength(3), um id/email específico) e você precisa abrir outro arquivo para descobrir por quê. - Uma fixture compartilhada/"geral" é reutilizada em muitos testes, de modo que as entradas relevantes para este teste ficam soterradas em meio a dados que não lhe interessam.
- Os testes quebram quando um colega edita uma fixture compartilhada para um teste não relacionado.
test('returns active subscribers', async () => {
// Convidado Misterioso: que linhas há neste arquivo? por que 3 está correto?
const subscribers = await loadSubscribersFromCsv('./fixtures/subscribers.csv');
const result = filterActive(subscribers);
expect(result).toHaveLength(3); // a justificativa fica fora do teste
});
O exemplo original do catálogo de Meszaros/Test Smells tem o mesmo formato: loadAirportsAndFlightsFromFile("test-flights.csv") seguido de assertEquals(1, flightsAtOrigin.size()) — o "1" só faz sentido se você ler test-flights.csv.
##Reasons for the Problem
Por que acontece
- DRY levado longe demais. Os dados de teste são extraídos para um arquivo compartilhado ou "fixture geral" a fim de evitar duplicação, trocando legibilidade por reúso.
- Conveniência com dados do mundo real. Despejar um CSV/JSON de produção ou um
seed.sqlparece mais fácil do que construir objetos inline. - Configuração legada/de integração. Suítes que sobem um banco de dados compartilhado ou dependem de anotações de data-fixture do framework (
@magentoDataFixture …) herdam estado oculto por padrão.
Por que prejudica
- Legibilidade / causa e efeito. O elo entre a entrada e a saída esperada é rompido. Um leitor não consegue ver por que a asserção está correta sem sair do teste, o que anula o papel do teste como documentação executável.
- Falsa confiança. Você na verdade não sabe o que o teste exercita; o arquivo pode conter mais (ou menos) do que você supõe, então uma execução verde prova menos do que parece.
- Confiabilidade / determinismo. O recurso externo pode estar ausente, ter sido renomeado, reformatado ou diferir conforme o ambiente, o sistema operacional, a codificação ou o locale — as falhas então refletem o estado da fixture, e não um defeito real (testes instáveis).
- Manutenibilidade / acoplamento. Quando vários testes compartilham um mesmo recurso, qualquer pessoa que o edite para um teste pode quebrar silenciosamente os outros, e ninguém sabe de quais campos cada teste depende.
##Treatment
Torne a entrada relevante visível dentro do teste, ao lado da asserção que depende dela (uma Fresh Fixture / configuração inline). O objetivo é que o valor esperado se torne autoevidente.
Passos concretos:
- Coloque inline os dados que importam. Construa, no corpo do teste, os poucos objetos/linhas que interessam ao teste, de modo que o valor esperado da asserção seja obviamente correto.
- Se você realmente precisar de um arquivo, construa-o no teste. Use um helper que receba apenas os parâmetros relevantes e grave em um caminho temporário, e depois faça a limpeza — assim os valores significativos aparecem no teste, não em um blob versionado.
- Substitua fixtures compartilhadas/"gerais" por fixtures específicas de cada teste, ou exponha criadores/buscadores que revelem a intenção (
createProductWithName('Simple Product'),getRecentlyAddedProduct()), para que os atributos em teste fiquem explícitos enquanto a configuração irrelevante permanece escondida atrás de um builder bem nomeado — e não atrás de um recurso opaco.
Antes → depois:
// Antes — Convidado Misterioso: os dados e o "2" estão ocultos em um arquivo de seed
test('flags overdue invoices', async () => {
await seedDatabaseFromFixture('invoices.sql');
const overdue = await findOverdueInvoices();
expect(overdue).toHaveLength(2);
});
// Depois — as entradas estão visíveis; o resultado esperado é óbvio
test('flags overdue invoices', async () => {
await insertInvoice({ id: 1, dueDate: '2020-01-01', paid: false }); // vencida
await insertInvoice({ id: 2, dueDate: '2099-01-01', paid: false }); // ainda não vencida
const overdue = await findOverdueInvoices();
expect(overdue.map(i => i.id)).toEqual([1]);
});
Para o caso do arquivo, prefira um helper focado a um arquivo versionado:
const csv = makeSubscriberCsv(tmpFile, 'active@x.com', 'active@y.com', 'active@z.com');
// agora os "3 ativos" se justificam pelo que você vê no teste, não por um arquivo oculto