Prueba oscura.
Una Prueba oscura es aquella en la que, solo a partir del método de prueba, un lector no puede saber qué escenario se prepara, qué comportamiento se ejercita ni qué resultado se espera, porque la intención queda sepultada bajo demasiado detalle, demasiado poco contexto o lógica oculta en otra parte.
##Signs and Symptoms
Lees una prueba y aún no puedes responder a tres preguntas: ¿cuál es la preparación, qué se está ejercitando y cuál es el resultado esperado? La cadena de causa y efecto está oculta. Meszaros agrupa las causas en «demasiada información» y «demasiado poca información». Indicios habituales:
- Prueba ansiosa (Eager Test) — un único método de prueba verifica muchos comportamientos no relacionados, así que ninguna intención concreta queda clara.
- Invitado misterioso (Mystery Guest) — el fixture o los valores esperados viven fuera de la prueba (un archivo, un registro compartido de la base de datos, un fixture cargado por nombre), de modo que no puedes ver por qué debería pasar la aserción.
- Fixture general (General Fixture) — un gran
beforeEach/factoría compartido construye mucho más de lo que esta prueba necesita; los pocos campos relevantes se pierden entre el ruido. - Información irrelevante (Irrelevant Information) — páginas de datos de preparación o un snapshot gigante en los que solo importan de verdad uno o dos campos.
- Datos de prueba codificados a mano (Hard-Coded Test Data) — literales mágicos (
42,"a3f9-...",userId=7) sin significado nombrado, repetidos por la preparación y las aserciones. - Prueba indirecta (Indirect Testing) — la prueba hurga en el sistema bajo prueba a través de varios otros objetos, oscureciendo qué se está probando en realidad.
// Oscura: Ansiosa + Invitado misterioso + Información irrelevante
test('user', async () => {
const data = loadFixture('users.json'); // invitado misterioso: los valores viven en un archivo
const svc = new UserService(data, cfg, clock); // fixture general: la mayor parte no se usa
const u = await svc.create({ ...data[0], roles: ['a','b'], flags: 0x1f });
expect(u.id).toBeDefined();
expect(u.email).toContain('@');
expect(await svc.count()).toBe(data.length + 1); // ¿por qué este número? la respuesta está en el archivo
expect(await svc.login(u.email, 'p@ss')).toBe(true); // segundo comportamiento no relacionado
});
Aquí no puedes saber qué demuestra la prueba sin abrir users.json, y en realidad comprueba la creación y el inicio de sesión a la vez.
##Reasons for the Problem
Por qué ocurre
- Las pruebas crecen por acumulación: un desarrollador añade «una aserción más» a una prueba existente en lugar de escribir una nueva, produciendo una Prueba ansiosa (Eager Test).
- Compartir la preparación parece eficiente, así que un único fixture amplio o un
beforeEachse reutiliza por todas partes (Fixture general), y los archivos externos o las semillas de la base de datos se cargan por nombre (Invitado misterioso). - Copiar y pegar datos de aspecto realista arrastra docenas de campos irrelevantes y números mágicos.
- Abusar de los mocks o pasar por muchos colaboradores convierte una prueba unitaria en una Prueba indirecta (Indirect Testing).
Por qué resulta perjudicial
- Legibilidad: una prueba es documentación del comportamiento previsto. Si el lector no puede reconstruir Preparar → Ejercitar → Verificar, se pierde ese valor documental.
- Mantenibilidad: cuando falla una prueba oscura no sabes qué comportamiento se rompió ni si lo que está mal es la prueba o el código, así que los cambios son lentos y arriesgados.
- Fiabilidad: el Invitado misterioso y el Fixture general acoplan la prueba a un estado externo o compartido, provocando fallos intermitentes o dependientes del orden (y rompiendo la propiedad «fresca y determinista» de una buena prueba unitaria).
- Falsa confianza: las Pruebas ansiosas enmascaran la cobertura: un fallo al principio del método cortocircuita las comprobaciones posteriores, de modo que comportamientos que crees probados quizá nunca se ejecuten. Y, como señala Meszaros, los errores de programación son más fáciles de ocultar en una prueba oscura, produciendo Pruebas con errores (Buggy Tests) que pasan por las razones equivocadas.
##Treatment
Haz que cada prueba cuente una historia autocontenida cuya intención sea visible en el cuerpo de la prueba.
- Verifica una sola condición por prueba. Divide una Prueba ansiosa en pruebas enfocadas, cada una nombrada según el comportamiento que comprueba. Esto también corrige el enmascaramiento de cobertura.
- Incorpora el fixture relevante en línea (elimina el Invitado misterioso). Construye los datos de los que depende la prueba dentro de la prueba, o mediante un método de creación/construcción (Creation/Builder) explícito y nombrado; no cargues archivos anónimos ni filas compartidas de la base de datos.
- Usa un fixture mínimo / fresco. Construye solo lo que esta prueba necesita; sustituye un
beforeEachamplio y compartido por un constructor (builder) que asigne por defecto el ruido y deje que cada prueba fije únicamente el campo bajo prueba. - Nombra tus datos. Sustituye los literales mágicos por constantes/variables que revelen la intención, de modo que la aserción se explique por sí sola.
- Afirma sobre el significado, no sobre todo. Prefiere aserciones específicas a un snapshot gigante; si haces un snapshot, mantenlo pequeño y revisable para que los hechos relevantes no queden ahogados en una salida irrelevante.
- Oculta la mecánica, no la intención. Empuja el cableado incidental hacia métodos de utilidad/ayudantes de prueba bien nombrados, de modo que el cuerpo de la prueba se lea como preparación → acción → expectativa.
// Antes (oscura)
test('user', async () => {
const data = loadFixture('users.json');
const svc = new UserService(data, cfg, clock);
const u = await svc.create({ ...data[0], roles: ['a','b'], flags: 0x1f });
expect(await svc.count()).toBe(data.length + 1);
});
// Después: una sola intención, fixture en línea, datos nombrados
test('create() persists a new user', async () => {
const svc = userServiceWith([]); // fixture mínimo y fresco
const newUser = aUser({ email: 'ada@example.com' }); // el constructor asigna por defecto el ruido
const created = await svc.create(newUser);
expect(created.email).toBe('ada@example.com');
expect(await svc.count()).toBe(1); // autoexplicativo, sin archivo externo
});
Apunta a la forma AAA (Arrange-Act-Assert, preparar-actuar-afirmar): un lector debería captar el escenario sin salir del método de prueba.
##Detected by
- eslint-plugin-jest max-expects — jest/max-expects
- eslint-plugin-vitest max-expects — vitest/max-expects
- eslint-plugin-jest no-large-snapshots — jest/no-large-snapshots
- eslint-plugin-vitest no-large-snapshots — vitest/no-large-snapshots
- eslint-plugin-jest max-nested-describe — jest/max-nested-describe