ConstructiCat Logo
CodeBust.
Browse section ▾

Assertion Roulette.

Una prueba agrupa muchas aserciones sin documentar en un mismo método, de modo que cuando falla no puedes saber qué aserción se disparó ni por qué: te toca jugar a la ruleta.

##Signs and Symptoms

Una sola prueba contiene una serie de aserciones desnudas, sin mensajes explicativos y sin un único objetivo claro. Cuando se pone en rojo, el informe de fallo (sobre todo una línea de resumen de CI o una función auxiliar de aserción compartida) te dice que algo se rompió, pero no qué comprobación ni por qué: juegas a la "ruleta" para encontrar al culpable.

Señales reveladoras:

  • Muchas llamadas a expect(...) / assert(...) en una misma prueba, ninguna con un mensaje o etiqueta descriptivos.
  • El nombre de la prueba es genérico ('works', 'user profile', 'test register'), de modo que un fallo no comunica nada por sí mismo.
  • Las aserciones abarcan varias responsabilidades sin relación entre sí (un Eager Test que reutiliza un único fixture para comprobarlo todo a la vez).
  • Un fallo te obliga a recurrir al depurador o a los números de línea para averiguar qué se estaba verificando en realidad.
  • Como la mayoría de las aserciones fallan rápido, el primer fallo cortocircuita el resto, así que nunca ves las comprobaciones posteriores en una misma ejecución.
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);   // si ESTA es la que falla,
  expect(user.slug).toBe('ada');     // el informe solo dice
  expect(user.roles).toContain('member'); // "expected false to be true"
  expect(user.createdAt).toBeInstanceOf(Date);
});

Nota: los runners modernos (Jest, Vitest) imprimen la línea que falla y un diff, lo que atenúa el problema de "¿qué línea?". El olor sigue molestando cuando la prueba mezcla objetivos, tiene un nombre vago, itera sobre aserciones, las oculta tras funciones auxiliares compartidas o se ejecuta donde solo sobrevive un mensaje de resumen.

##Reasons for the Problem

Por qué ocurre

  • Eager Test / reutilización de fixtures. Una preparación costosa tienta a acumular comprobaciones del tipo "ya que estoy aquí" en lugar de escribir una segunda prueba.
  • Verificación de objeto completo hecha a mano. Comprobar un objeto campo por campo produce de forma natural una larga serie de aserciones sin documentar.
  • Crecimiento por copiar y pegar. Las pruebas acumulan aserciones con el tiempo sin que nadie las divida.
  • Herramientas históricas. Las aserciones clásicas de xUnit solo informaban éxito/fallo, así que una aserción sin etiqueta no daba ningún contexto al fallar: el origen del olor original de Meszaros.
  • Presión por los plazos. Una sola prueba grande parece más rápida de escribir que varias enfocadas.

Por qué es perjudicial

  • Diagnosticabilidad / fiabilidad. Según testsmells.org, "varias sentencias de aserción en un método de prueba sin un mensaje descriptivo perjudican la legibilidad/comprensibilidad/mantenibilidad, ya que no es posible entender el motivo del fallo". Pierdes tiempo localizando qué aserción se disparó.
  • Defectos ocultos (falsa confianza). Las aserciones de fallo rápido se detienen en el primer fallo, por lo que las aserciones posteriores nunca se ejecutan. Arreglas una, vuelves a ejecutar, encuentras la siguiente: se enmascaran varios errores y un "verde tras un único arreglo" transmite más seguridad de la que realmente da.
  • Legibilidad. La prueba deja de documentar un único comportamiento; su intención queda enterrada en una lista, y un nombre de prueba genérico no aporta nada.
  • Mantenibilidad. No queda claro si una nueva comprobación pertenece a esta prueba o a una nueva, así que el montón sigue creciendo y se acoplan responsabilidades sin relación entre sí.

##Treatment

Apunta al principio que hay detrás de una Single-Condition Test: cada prueba verifica un comportamiento y puede fallar por exactamente un motivo.

  1. Divide por objetivo. Descompón la prueba ómnibus en pruebas enfocadas con nombres descriptivos. El nombre pasa entonces a ser el mensaje de fallo.
  2. Condensa las comprobaciones campo por campo en una sola aserción. Usa toEqual / expect.objectContaining / un snapshot revisado para que N aserciones se conviertan en un único diff con sentido.
  3. Cuando la agrupación es realmente cohesiva, etiqueta las aserciones. El expect de Jest/Vitest no tiene argumento de mensaje, así que usa el parámetro de mensaje de node:assert, el expect(actual, message) de Vitest o jest-expect-message.
  4. ¿Quieres que todas las comprobaciones se ejecuten e informen juntas? Mejor divídelas; si no, usa aserciones suaves (expect.soft en Vitest) para que un fallo no oculte el resto.
  5. Protégete frente a regresiones limitando las aserciones por prueba (consulta los 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);
});

Después: un comportamiento por prueba, con una aserción de objeto completo:

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);
  });
});

Si debes mantener una sola prueba, haz que cada aserción se describa a sí misma:

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-expectsMarca el indicador basado en recuento de Assertion Roulette: informa cuando una prueba supera N llamadas a expect() (5 por defecto). La documentación señala que un mayor número de aserciones tiende a mezclar varios objetivos.
  • eslint-vitest vitest/max-expectsEquivalente en Vitest: impone un número máximo de aserciones expect() por prueba, detectando las pruebas que acumulan demasiadas comprobaciones.
  • sonar java:S5961SonarSource 'Test methods should not contain too many assertions': limita las aserciones por prueba (25 por defecto para JUnit/AssertJ), el indicador estándar de análisis estático para este olor.