ConstructiCat Logo
CodeBust.
Browse section ▾

Invitado misterioso.

Una prueba cuyas entradas o resultados esperados residen en un recurso externo —un archivo, una semilla de base de datos o una fixture compartida— de modo que no puedes entender ni confiar en la prueba leyéndola por sí sola.

##Signs and Symptoms

No puedes entender una prueba leyéndola: los datos que la impulsan (y que justifican sus aserciones) residen en algún lugar fuera de la vista —un archivo CSV/JSON, un script de semilla de base de datos, un módulo de fixture compartido o una anotación de fixture de datos del framework—. La prueba hace referencia a un recurso opaco y luego hace aserciones sobre valores «mágicos» cuyo significado está oculto en ese recurso.

Señales reveladoras:

  • El cuerpo llama a algo como readFileSync('fixtures/subscribers.csv'), loadSeed('invoices.sql'), getResource(...), o un beforeAll global que siembra una base de datos compartida.
  • Los valores esperados parecen arbitrarios (toHaveLength(3), un id/email concreto) y debes abrir otro archivo para averiguar por qué.
  • Una fixture compartida/«general» se reutiliza en muchas pruebas, de modo que las entradas relevantes para esta prueba quedan enterradas entre datos que no le importan.
  • Las pruebas se rompen cuando un compañero edita una fixture compartida para una prueba no relacionada.
test('returns active subscribers', async () => {
  // Invitado misterioso: ¿qué filas hay en este archivo? ¿por qué 3 es correcto?
  const subscribers = await loadSubscribersFromCsv('./fixtures/subscribers.csv');
  const result = filterActive(subscribers);
  expect(result).toHaveLength(3); // la justificación reside fuera de la prueba
});

El ejemplo original del catálogo de Meszaros/Test Smells tiene la misma forma: loadAirportsAndFlightsFromFile("test-flights.csv") seguido de assertEquals(1, flightsAtOrigin.size()) —el «1» solo tiene sentido si lees test-flights.csv—.

##Reasons for the Problem

Por qué ocurre

  • DRY llevado demasiado lejos. Los datos de prueba se extraen a un archivo compartido o a una «fixture general» para evitar la duplicación, sacrificando legibilidad a cambio de reutilización.
  • Comodidad con datos del mundo real. Volcar un CSV/JSON de producción o un seed.sql parece más fácil que construir objetos en línea.
  • Configuración heredada/de integración. Las suites que arrancan una base de datos compartida o que dependen de anotaciones de fixtures de datos del framework (@magentoDataFixture …) heredan estado oculto por defecto.

Por qué es perjudicial

  • Legibilidad / causa y efecto. El vínculo entre la entrada y la salida esperada se rompe. Un lector no puede ver por qué la aserción es correcta sin salir de la prueba, lo que anula el papel de la prueba como documentación ejecutable.
  • Falsa confianza. En realidad no sabes qué ejercita la prueba; el archivo puede contener más (o menos) de lo que supones, de modo que una ejecución en verde demuestra menos de lo que parece.
  • Fiabilidad / determinismo. El recurso externo puede faltar, haber sido renombrado, reformateado o diferir según el entorno, el sistema operativo, la codificación o la configuración regional; los fallos reflejan entonces el estado de la fixture, no un defecto real (pruebas inestables).
  • Mantenibilidad / acoplamiento. Cuando varias pruebas comparten un mismo recurso, cualquiera que lo edite para una prueba puede romper las demás de forma silenciosa, y nadie sabe de qué campos depende cada prueba.

##Treatment

Haz visible la entrada relevante dentro de la prueba, junto a la aserción que depende de ella (una Fresh Fixture / configuración en línea). El objetivo es que el valor esperado resulte evidente por sí mismo.

Pasos concretos:

  1. Incorpora en línea los datos que importan. Construye en el cuerpo de la prueba los pocos objetos/filas que le importan, de modo que el valor esperado de la aserción sea evidentemente correcto.
  2. Si realmente necesitas un archivo, constrúyelo en la prueba. Usa un helper que reciba solo los parámetros relevantes y escriba en una ruta temporal, y luego limpia; así los valores significativos aparecen en la prueba, no en un bloque versionado.
  3. Sustituye las fixtures compartidas/«generales» por fixtures por prueba, o expón creadores/buscadores que revelen la intención (createProductWithName('Simple Product'), getRecentlyAddedProduct()) para que los atributos bajo prueba sean explícitos mientras la configuración irrelevante permanece oculta tras un builder bien nombrado, no tras un recurso opaco.

Antes → después:

// Antes — Invitado misterioso: los datos y el «2» están ocultos en un archivo de semilla
test('flags overdue invoices', async () => {
  await seedDatabaseFromFixture('invoices.sql');
  const overdue = await findOverdueInvoices();
  expect(overdue).toHaveLength(2);
});

// Después — las entradas son visibles; el resultado esperado es obvio
test('flags overdue invoices', async () => {
  await insertInvoice({ id: 1, dueDate: '2020-01-01', paid: false }); // vencida
  await insertInvoice({ id: 2, dueDate: '2099-01-01', paid: false }); // aún no vencida
  const overdue = await findOverdueInvoices();
  expect(overdue.map(i => i.id)).toEqual([1]);
});

Para el caso del archivo, prefiere un helper específico antes que un archivo versionado:

const csv = makeSubscriberCsv(tmpFile, 'active@x.com', 'active@y.com', 'active@z.com');
// ahora «3 activos» se justifica por lo que ves en la prueba, no por un archivo oculto