learning-front

Nivel 6 · Introducción al testing

TDD y cobertura, de concepto

El ciclo rojo-verde-refactor aplicado a añadir una función nueva al motor, y qué es la cobertura de código —con su trampa: 100% de líneas no significa bien testeado—. Cierre y puente: hasta aquí, lógica pura y síncrona; con React llegan el DOM, lo asíncrono y la necesidad de simular dependencias.

Cierras el nivel con dos ideas que lo atan todo: escribir el test antes que el código (TDD) y medir cuánto cubre tu suite (la cobertura, con su trampa).

TDD: el test primero, en tres pasos#

En la T de FIRST (Timely) vimos que el test va junto al código. TDD (test-driven development) lo lleva al extremo: el test va antes. El ciclo es rojo-verde-refactor:

  1. Rojo — escribes un test de lo que quieres que pase. Falla, porque la función aún no existe o no hace lo pedido. Ese rojo confirma que el test de verdad comprueba algo.
  2. Verde — escribes la implementación más simple que lo pone en verde. Nada de adornos: solo pasar.
  3. Refactor — con los tests de red, limpias el código sin cambiar el comportamiento. Si rompes algo, el test salta al instante.

Aquí está el resultado de aplicar el ciclo a una función nueva del motor, mvp (el héroe con mayor winrate). Los tests se escribieron primero; la implementación, después; y se refactorizó a un reduce con los tests ya en verde:

Lo potente de TDD no es la ceremonia, es el cambio de orden: te obliga a decidir qué debe hacer el código antes de escribirlo, y de paso te deja la suite hecha. Lo practicas en el ejercicio: los tests de mvp ya están escritos y en rojo; tu trabajo es ponerlos en verde.

Cobertura: la foto de lo que NO has probado#

La cobertura de código mide qué porcentaje de tu código (líneas, ramas, funciones) ejecutan tus tests. Vitest la calcula con un comando, en local:

shell
# Corre la suite y, además, mide qué código tocó.
pnpm coverage

La cobertura la calcula un paquete aparte —el proveedor @vitest/coverage-v8—, que no viene con Vitest de serie. La primera vez que la lances sin tenerlo instalado, Vitest se ofrece a instalarlo por ti: aceptas y, a partir de ahí, el comando ya funciona. Hecho eso, te imprime una tabla por fichero:

text
 File        | % Stmts | % Branch | % Funcs | Uncovered Lines
-------------|---------|----------|---------|----------------
 motor.ts    |   88.5  |   75.0   |  100.0  | 42-45

Esa última columna —las líneas sin cubrir— es lo útil: te dice qué zonas no toca ningún test. La columna de ramas (% Branch) suele ser la más reveladora: un if cuyo else nunca se prueba.

La trampa del 100%#

Y aquí el «¿y qué?» importante: ejecutar una línea no es comprobarla. Puedes llamar a una función desde un test, recorrer todas sus líneas y no afirmar nada sobre el resultado: la cobertura sube al 100% y, sin embargo, no proteges nada. La cobertura te dice qué no has tocado; no te dice que lo tocado esté bien testeado. Por eso 100% no es la meta: es una brújula para encontrar agujeros, no una nota del examen. Un 80% con buenas aserciones protege más que un 100% hueco.

Comprueba lo que sabes#

Pregunta 1 de 5

¿En qué orden van los pasos del ciclo TDD?

Tu turno#

Los tests de mvp ya están escritos y en rojo (mvp lanza un TODO). Implementa mvp en motor.ts para ponerlos en verde, y luego refactoriza dejándolo limpio. 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

Implementa mvp con rojo-verde-refactor

Los tests de mvp ya están escritos (la especificación) y ahora mismo FALLAN: mvp lanza un TODO. Implementa mvp en motor.ts para ponerlos en verde —devuelve el héroe con mayor winrate, o null si la lista está vacía— y luego refactoriza la implementación dejándola limpia.

Paso 1: De rojo a verde

  • mvp devuelve el héroe con mayor winrate y null con la lista vacía: los dos tests pasan.
  • Has visto los tests en rojo antes de implementar (mvp lanzaba) y en verde después.
Ver soluciones
// motor.ts — tier OK: mvp con un bucle, pasa los dos tests.
export interface Heroe {
  nombre: string;
  rol: "Daño" | "Tanque" | "Apoyo";
  partidas: number;
  victorias: number;
}

export interface HeroeEnriquecido extends Heroe {
  winrate: number;
}

export function enriquecer(heroes: readonly Heroe[]): HeroeEnriquecido[] {
  return heroes.map((h) => ({
    ...h,
    winrate: Math.round((h.victorias / h.partidas) * 1000) / 10,
  }));
}

export function mvp(
  heroes: readonly HeroeEnriquecido[],
): HeroeEnriquecido | null {
  // Lista vacía: no hay mvp. Devolvemos null explícito.
  if (heroes.length === 0) return null;
  // Recorremos guardando el mejor hasta ahora.
  let mejor = heroes[0];
  for (let i = 1; i < heroes.length; i++) {
    if (heroes[i].winrate > mejor.winrate) {
      mejor = heroes[i];
    }
  }
  return mejor;
}

Por qué este nivel

  • Un bucle que guarda el mejor hasta ahora, con el caso vacío tratado aparte. Pasa los dos tests: el verde está conseguido, que es lo que pide la fase verde del TDD.
  • Su límite: el bucle con índice es ruidoso para algo que el lenguaje expresa mejor. Funciona, pero pide un refactor.