Conditional Test Logic.
Una prueba que usa `if`/`switch`/ternarios, bucles o `try`/`catch` para decidir qué ejecutar o aseverar, de modo que su comportamiento (y si verifica algo siquiera) depende de qué rama se ejecute en tiempo de ejecución.
##Signs and Symptoms
Una prueba se lee como un pequeño programa en lugar de como un guion lineal "preparar → actuar → aseverar". Busca flujo de control dentro del cuerpo de la prueba:
if/else,switch, ternarios o cortocircuitos&&/||que controlan qué aserciones se ejecutan.- Bucles
for/while/forEachque construyen entradas o iteran sobre aserciones. try/catchusado para "probar" una ruta de error, con las llamadas aexpectescondidas dentro delcatch.- Una misma prueba reutilizada para varios casos ramificando según un flag o el estado del entorno.
- Valores esperados calculados en la prueba (a menudo con un bucle o una fórmula) en lugar de codificados de forma literal.
// Olor: las aserciones viven dentro de código condicional/ramificado
test('user discount', () => {
const user = getUser();
if (user.isPremium) {
expect(price(user)).toBe(80); // puede no ejecutarse nunca
} else {
expect(price(user)).toBe(100); // puede no ejecutarse nunca
}
});
test('throws on bad input', () => {
try {
parse('!!!');
// si parse() NO lanza, caemos hasta aquí y no aseveramos nada → la prueba pasa
} catch (err) {
expect(err.message).toMatch(/invalid/);
}
});
El modo de fallo revelador: la prueba sigue en verde aunque el código esté roto, porque nunca se tomó la rama que contenía la aserción.
##Reasons for the Problem
Por qué ocurre
- DRY llevado al extremo. Los autores intentan cubrir varios escenarios con una única prueba "flexible", ramificando según las entradas o un flag en lugar de escribir una prueba por caso (Meszaros: Flexible Test).
- Acoplamiento al entorno. El SUT no se desacopló de sus dependencias, así que la prueba se adapta a cualquier estado que encuentre en tiempo de ejecución.
- Teardown defensivo. Aparece
if (resource) resource.close()para evitar desmontar fixtures que quizá no existan (Complex Teardown). - Expectativas calculadas. El resultado esperado se deriva con el mismo algoritmo que el código de producción, arrastrando esa lógica (bucles incluidos) hasta la prueba (Production Logic in Test).
- Prueba manual de la ruta de error. Se usa
try/catchpara aseverar sobre un error lanzado en lugar de un matcher integrado.
Por qué es perjudicial
- Falsa confianza (lo peor). La mayoría de los runners solo hacen fallar una prueba cuando una aserción lanza. Si se omite la rama que asevera, o el código bajo prueba no lanza dentro del
try, la prueba pasa sin haber verificado nada. - Código de prueba no probado. Las ramas y los bucles dentro de una prueba son lógica que, a su vez, no tiene pruebas; un error en el propio flujo de control de la prueba pasa desapercibido.
- Diagnósticos pobres. Cuando una prueba con ramificaciones falla, primero debes averiguar qué camino se ejecutó antes de poder interpretar el fallo.
- Menor legibilidad y mantenibilidad. Una prueba lineal documenta un comportamiento con un único resultado esperado; una prueba con ramificaciones obliga al lector a simular la ejecución para saber qué se garantiza en realidad.
- Fragilidad. Las pruebas que dependen del estado del entorno o de la ejecución pasan o fallan de forma no determinista.
##Treatment
Haz que cada prueba sea un único camino lineal e incondicional. En concreto:
- Un escenario por prueba. Divide una prueba con ramificaciones en pruebas separadas, o usa la API basada en datos del framework (
test.each,it.each, pruebas parametrizadas) para que cada caso sea su propia ejecución, con nombre claro e informada de forma independiente. - Saca la ramificación fuera de la prueba. Si un caso solo aplica bajo cierta condición, decídelo en tiempo de definición (por ejemplo, eligiendo
describe/itsegún la configuración), no dentro del cuerpo de la prueba; las aserciones en sí permanecen incondicionales. - Codifica los valores esperados de forma literal. Sustituye las expectativas calculadas por resultados esperados literales (o un Expected Object / matcher personalizado). No reimplementes la lógica de producción en la prueba.
- Prueba las rutas de error con matchers, no con
try/catch:expect(fn).toThrow(...),await expect(p).rejects.toThrow(...). Estos fallan de forma sonora cuando no se lanza ningún error. - Si una aserción condicional es realmente inevitable, fija el recuento con
expect.assertions(n)/expect.hasAssertions()para que una rama omitida falle en lugar de pasar en silencio. - Sustituye el teardown condicional por los hooks de ciclo de vida del framework (
afterEach) y una limpieza automática/idempotente, de modo que no haga falta ningúnifpara proteger el desmontaje.
// Antes: prueba con ramificaciones, las aserciones pueden omitirse
test('user discount', () => {
const user = getUser();
if (user.isPremium) expect(price(user)).toBe(80);
else expect(price(user)).toBe(100);
});
// Después: un caso explícito por fila, cada aserción se ejecuta siempre
test.each([
['premium', { isPremium: true }, 80],
['regular', { isPremium: false }, 100],
])('price for %s user', (_label, user, expected) => {
expect(price(user)).toBe(expected);
});
// Antes: try/catch que pasa cuando no se lanza nada
try { parse('!!!'); } catch (e) { expect(e.message).toMatch(/invalid/); }
// Después: falla si parse() no lanza
expect(() => parse('!!!')).toThrow(/invalid/);
##Detected by
- eslint-jest jest/no-conditional-expect — no-conditional-expect
- eslint-jest jest/no-conditional-in-test — no-conditional-in-test
- eslint-vitest vitest/no-conditional-expect — no-conditional-expect
- eslint-vitest vitest/no-conditional-in-test — no-conditional-in-test
- eslint-vitest vitest/no-conditional-tests — no-conditional-tests