ConstructiCat Logo
CodeBust.
Browse section ▾

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 um beforeAll global 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.sql parece 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:

  1. 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.
  2. 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.
  3. 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