Logotipo de ConstructiCat
CodeBust.
Explorar la sección ▾

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.

##Signos y síntomas

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. Se trata en detalle en Prueba verbosa.
  • 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. Véase Número mágico en tests.
  • 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.

##Causas del problema

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 beforeEach se 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.

##Tratamiento

No hay un único remedio para una prueba oscura, porque "oscura" es un síntoma con varias causas. Diagnostica primero y aplica después el remedio de la causa que encuentres: cada una tiene su propio artículo con las refactorizaciones detalladas.

  1. Haz una lectura en frío. Pide a alguien que no haya visto el código que diga, solo a partir del cuerpo de la prueba, cuál es la preparación, la acción y el resultado esperado. Allí donde tenga que abrir otro archivo o adivinar, está la causa.
  2. ¿Varios comportamientos no relacionados en una prueba? Es una prueba ansiosa: divídela en pruebas enfocadas, cada una con el nombre del comportamiento que verifica. Así, además, un fallo temprano deja de ocultar las comprobaciones posteriores.
  3. ¿Valores esperados que se justifican con un archivo externo, una semilla o un registro compartido? Es un invitado misterioso: lleva a la prueba los datos de los que depende la aserción.
  4. ¿Un beforeEach compartido y amplio construye mucho más de lo que la prueba necesita? Es un fixture general: pasa a un fixture mínimo y nuevo para cada prueba.
  5. ¿Páginas de preparación irrelevante o aserciones campo por campo? Es una prueba verbosa: oculta el ruido tras Creation Methods y builders y compara con un objeto esperado; allí se explica la mecánica.
  6. ¿Literales sin explicar? Consulta Número mágico en tests: pon nombre a los valores para que la aserción se explique sola.
  7. ¿Al sistema bajo prueba solo se llega a través de varios otros objetos (prueba indirecta)? Pruébalo a través de su propia interfaz, o extrae la lógica para poder probarla directamente.
// 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: '[email protected]' }); // el constructor asigna por defecto el ruido

  const created = await svc.create(newUser);

  expect(created.email).toBe('[email protected]');
  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.

##Detectado por