ConstructiCat Logo
CodeBust.
Browse section ▾

Assertion Roulette.

Um teste empacota muitas asserções não documentadas em um único método, então, quando ele falha, você não consegue saber qual asserção disparou nem por quê — é preciso apostar.

##Signs and Symptoms

Um único teste contém uma sequência de asserções nuas, sem mensagens explicativas e sem um objetivo único claro. Quando ele fica vermelho, o relatório de falha (especialmente uma linha de resumo de CI ou um helper de asserção compartilhado) diz que algo quebrou, mas não qual verificação nem por quê — você joga na "roleta" para achar o culpado.

Sinais reveladores:

  • Muitas chamadas expect(...) / assert(...) em um único teste, nenhuma carregando uma mensagem ou rótulo descritivo.
  • O nome do teste é genérico ('works', 'user profile', 'test register'), então uma falha não comunica nada por si só.
  • As asserções abrangem várias preocupações não relacionadas (um Eager Test reutilizando uma única fixture para verificar tudo de uma vez).
  • Uma falha faz você recorrer ao depurador ou aos números de linha para descobrir o que estava de fato sendo verificado.
  • Como a maioria das asserções é fail-fast, a primeira falha interrompe o restante, então você nunca vê as verificações posteriores em uma única execução.
test('user profile', () => {
  const user = createUser({ name: 'Ada', age: 36 });
  expect(user.name).toBe('Ada');
  expect(user.age).toBe(36);
  expect(user.isAdult).toBe(true);   // se ESTA for a vermelha,
  expect(user.slug).toBe('ada');     // o relatório apenas diz
  expect(user.roles).toContain('member'); // "expected false to be true"
  expect(user.createdAt).toBeInstanceOf(Date);
});

Observação: executores modernos (Jest, Vitest) imprimem a linha que falhou e um diff, o que ameniza o problema do "qual linha?". O smell ainda incomoda quando o teste mistura objetivos, tem um nome vago, itera sobre asserções em laço, as esconde atrás de helpers compartilhados ou roda em um ambiente onde só uma mensagem de resumo sobrevive.

##Reasons for the Problem

Por que acontece

  • Eager Test / reúso de fixture. Uma configuração custosa tenta você a acumular verificações do tipo "já que estou aqui" em vez de escrever um segundo teste.
  • Verificação de objeto inteiro feita do jeito difícil. Verificar um objeto campo a campo naturalmente produz uma longa sequência de asserções não documentadas.
  • Crescimento por copiar e colar. Os testes acumulam asserções ao longo do tempo sem que ninguém os divida.
  • Ferramental histórico. As asserções clássicas do xUnit reportavam apenas passou/falhou, então uma asserção sem rótulo não dava contexto algum na falha — a origem do smell original de Meszaros.
  • Pressão de prazo. Um teste grande parece mais rápido de escrever do que vários testes focados.

Por que prejudica

  • Diagnosticabilidade / confiabilidade. Segundo o testsmells.org, "múltiplas instruções de asserção em um método de teste sem uma mensagem descritiva impactam a legibilidade/compreensibilidade/manutenibilidade, pois não é possível entender o motivo da falha." Você gasta tempo localizando qual asserção disparou.
  • Defeitos ocultos (falsa confiança). Asserções fail-fast param na primeira falha, então as asserções posteriores nunca são executadas. Você corrige uma, roda de novo, encontra a próxima — vários bugs ficam mascarados, e um "verde depois de uma correção" parece mais seguro do que de fato é.
  • Legibilidade. O teste deixa de documentar um único comportamento; sua intenção fica enterrada em uma lista, e um nome de teste genérico não acrescenta nada.
  • Manutenibilidade. Não fica claro se uma nova verificação pertence a este teste ou a um novo, então a pilha continua crescendo e preocupações não relacionadas acabam acopladas.

##Treatment

Busque o princípio por trás de um Single-Condition Test: cada teste verifica um comportamento e pode falhar por exatamente um motivo.

  1. Divida por objetivo. Quebre o teste abrangente em testes focados com nomes descritivos. O nome então é a mensagem de falha.
  2. Condense verificações campo a campo em uma única asserção. Use toEqual / expect.objectContaining / um snapshot revisado para que N asserções virem um único diff significativo.
  3. Quando o agrupamento for genuinamente coeso, rotule as asserções. O expect do Jest/Vitest não tem argumento de mensagem, então use o parâmetro de mensagem do node:assert, o expect(actual, message) do Vitest ou o jest-expect-message.
  4. Quer que toda verificação rode e reporte em conjunto? Prefira dividir; caso contrário, use asserções leves (expect.soft no Vitest) para que uma falha não esconda as demais.
  5. Proteja-se contra regressões limitando o número de asserções por teste (veja os detectores).

Antes — assertion roulette:

test('register user', () => {
  const res = register({ email: 'a@b.com', age: 36 });
  expect(res.ok).toBe(true);
  expect(res.user.email).toBe('a@b.com');
  expect(res.user.isAdult).toBe(true);
  expect(res.welcomeEmailSent).toBe(true);
});

Depois — um comportamento por teste, com uma asserção de objeto inteiro:

describe('register', () => {
  it('accepts a valid adult signup', () => {
    expect(register({ email: 'a@b.com', age: 36 }).ok).toBe(true);
  });

  it('stores the normalized user record', () => {
    const { user } = register({ email: 'a@b.com', age: 36 });
    expect(user).toEqual(
      expect.objectContaining({ email: 'a@b.com', isAdult: true }),
    );
  });

  it('sends a welcome email on signup', () => {
    expect(register({ email: 'a@b.com', age: 36 }).welcomeEmailSent).toBe(true);
  });
});

Se você precisar manter um único teste, faça cada asserção ser autoexplicativa:

import { strict as assert } from 'node:assert';
assert.equal(res.ok, true, 'registration should succeed');
assert.equal(res.welcomeEmailSent, true, 'welcome email should be sent');

##Detected by

  • eslint-jest jest/max-expectsSinaliza o indicador baseado em contagem do Assertion Roulette: reporta quando um teste excede N chamadas expect() (padrão 5). A documentação observa que mais asserções tendem a misturar múltiplos objetivos.
  • eslint-vitest vitest/max-expectsEquivalente no Vitest: impõe um número máximo de asserções expect() por teste, capturando testes que acumulam verificações em excesso.
  • sonar java:S5961SonarSource 'Métodos de teste não devem conter asserções demais' — limita as asserções por teste (padrão 25 para JUnit/AssertJ), o indicador padrão de análise estática para esse smell.