learning-front

Nivel 6 · Introducción al testing

Qué hace bueno a un test

Los cinco principios FIRST (rápido, aislado, repetible, autovalidado, a tiempo) y, más allá de la sigla, probar comportamiento en vez de implementación. El aislamiento con datos frescos en beforeEach, los anti-patrones que producen tests frágiles o que solo testean el lenguaje, y un helper sencillo para no duplicar datos de prueba.

Hasta aquí, “el test pasa” ha sido sinónimo de “bien”. No siempre lo es: un test puede pasar y aun así ser malo —frágil, acoplado al de al lado, o que en realidad no comprueba nada tuyo—. Este capítulo separa los tests que protegen de los que solo dan una falsa sensación de seguridad.

Los cinco principios FIRST#

Un buen test cumple cinco propiedades, que se recuerdan con el acrónimo FIRST:

  • Fast (rápido). Corre en milisegundos, para que lo lances a cada cambio sin pereza. Las funciones puras del motor lo son por naturaleza.
  • Isolated (aislado). No depende de otros tests ni del orden en que corran. Cada uno parte de cero.
  • Repeatable (repetible). Mismo resultado siempre. Nada de azar, de la fecha de hoy o de un estado que cambie entre ejecuciones.
  • Self-validating (autovalidado). El propio test dice si pasa o falla con un expect. Si tienes que leer la salida para saber si fue bien, no es un test, es un script.
  • Timely (a tiempo). Escrito junto al código, no “algún día”. (En el próximo capítulo verás una forma de escribirlo incluso antes.)

Dos son las que más se incumplen: el aislamiento (la I de FIRST) y probar comportamiento en vez de implementación —que no es una letra de FIRST, sino la regla de oro que viste en Anatomía de un test—. Vamos a por el aislamiento.

Aislamiento: cada test, datos frescos#

El síntoma clásico de tests mal aislados: uno pasa cuando lo corres solo, pero falla con toda la suite (o al revés). Eso pasa cuando un test deja un estado —un array mutado, una variable compartida— que otro hereda. El orden los acopla, y un test que depende del orden no es fiable.

La cura es dar a cada test datos frescos, y para eso está beforeEach: una función que corre antes de cada it.

El primer test hace pop() sobre heroes; el segundo, aun así, los ve completos. ¿Por qué? Porque beforeEach reconstruye el array antes de cada uno. Quita el beforeEach mentalmente: el segundo test vería un solo héroe (el que dejó el primero) y fallaría. Ese es el «¿y qué?» del aislamiento: sin él, añadir o reordenar un test puede romper otro que no has tocado, y persiguiendo ese fallo “fantasma” se va una tarde entera.

Menos ruido, menos repetición: un builder#

Repetir el objeto de prueba entero en cada test (cinco campos del héroe) tiene dos costes: ruido —cuesta ver qué mira cada test entre tanto campo— y mantenimiento —si el héroe cambia de forma, tocas todos los tests—. Un builder lo arregla: una función que da un dato válido por defecto y deja sobreescribir solo lo relevante.

heroe({ victorias: 0 }) grita “este test va de SIN victorias”; el resto de campos son ruido que el builder esconde. Y si mañana el héroe gana un campo nuevo, lo añades en un solo sitio. Es DRY aplicado a los datos de prueba, sin sacrificar legibilidad.

Anti-patrones: tests que no protegen#

Reconócelos para no escribirlos:

typescript
// 1) Probar el LENGUAJE, no tu código. Que 2 + 2 sea 4 no es cosa tuya:
//    este test no protege nada de la aplicación.
it("suma", () => {
  expect(2 + 2).toBe(4);
});
typescript
// 2) Acoplarse a la IMPLEMENTACIÓN. Afirmar CÓMO se hizo (que se llamó a map)
//    en vez de QUÉ se obtuvo: el test se rompe en un refactor que no cambia nada
//    observable. Prueba el resultado, no el camino.
typescript
// 3) Test frágil por datos volátiles. Si afirmas un objeto entero que incluye
//    un campo que cambia (una fecha, un id aleatorio), el test fallará mañana
//    sin que nada esté roto. Afirma solo lo estable, o controla ese campo.

El hilo común: un buen test comprueba comportamiento observable y estable de tu código. Si prueba el lenguaje, la implementación interna o un dato que cambia solo, estorba más que protege.

Comprueba lo que sabes#

Pregunta 1 de 5

¿Qué resume el acrónimo FIRST sobre un buen test?

Tu turno#

Coge la suite de partida (funciona, pero repite y nombra mal) y conviértela en una buena: aíslala con beforeEach, quita la repetición con un builder y nombra por comportamiento. Cuando lo tengas (o si te atascas), despliega las soluciones y fíjate en el salto de un nivel al siguiente.

Ejercicio · en esta página

Convierte una suite que funciona en una buena

La suite de partida funciona, pero repite el mismo array en cada test y se llama "filtra Daño" / "filtra Tanque". Mejórala: aíslala con beforeEach, quita la repetición con un builder heroe(overrides) y nombra los tests por su comportamiento.

Paso 1: Independientes y bien nombrados

  • Cada test prepara sus propios datos (ninguno depende de otro) y los nombres describen el comportamiento, no "filtra Daño".
  • Las afirmaciones comparan el resultado real (toEqual / toHaveLength), no a ojo.
Ver soluciones
// motor.test.ts — tier OK: tests independientes y nombres por comportamiento.
import { describe, it, expect } from "vitest";
import { filtrarPorRol, type Heroe } from "./motor";

describe("filtrarPorRol", () => {
  it("devuelve solo los héroes del rol pedido", () => {
    const heroes: Heroe[] = [
      { nombre: "Genji", rol: "Daño", partidas: 10, victorias: 6 },
      { nombre: "Reinhardt", rol: "Tanque", partidas: 8, victorias: 5 },
    ];
    expect(filtrarPorRol(heroes, "Daño").map((h) => h.nombre)).toEqual(["Genji"]);
  });

  it("con 'todos' no descarta a nadie", () => {
    const heroes: Heroe[] = [
      { nombre: "Genji", rol: "Daño", partidas: 10, victorias: 6 },
      { nombre: "Reinhardt", rol: "Tanque", partidas: 8, victorias: 5 },
    ];
    expect(filtrarPorRol(heroes, "todos")).toHaveLength(2);
  });
});

Por qué este nivel

  • Renombra los tests por comportamiento ("devuelve solo los héroes del rol pedido") y compara el resultado de verdad. Ya es una suite legible.
  • Su límite: sigue copiando el array entero en cada test. Funciona, pero si cambia la forma del héroe hay que tocar todos los tests.