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.
- Divide por objetivo. Descompón la prueba ómnibus en pruebas enfocadas con nombres descriptivos. El nombre pasa entonces a ser el mensaje de fallo.
- 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. - Cuando la agrupación es realmente cohesiva, etiqueta las aserciones. El
expectde Jest/Vitest no tiene argumento de mensaje, así que usa el parámetro de mensaje denode:assert, elexpect(actual, message)de Vitest ojest-expect-message. - ¿Quieres que todas las comprobaciones se ejecuten e informen juntas? Mejor divídelas; si no, usa aserciones suaves (
expect.soften Vitest) para que un fallo no oculte el resto. - 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-expects — Marca 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-expects — Equivalente en Vitest: impone un número máximo de aserciones expect() por prueba, detectando las pruebas que acumulan demasiadas comprobaciones.
- sonar java:S5961 — SonarSource '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.