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:
- 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.
- Verde — escribes la implementación más simple que lo pone en verde. Nada de adornos: solo pasar.
- 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:
# Corre la suite y, además, mide qué código tocó.
pnpm coverageLa 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:
File | % Stmts | % Branch | % Funcs | Uncovered Lines
-------------|---------|----------|---------|----------------
motor.ts | 88.5 | 75.0 | 100.0 | 42-45Esa ú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.
Paso 2: El refactor
- Tras pasar a verde, dejas la implementación limpia (por ejemplo, un reduce en vez de un bucle con índice a mano).
- Los tests siguen en verde tras refactorizar: el comportamiento no ha cambiado.
Paso 3: Decisiones explícitas
- El caso vacío se trata primero y devuelve null explícito (no undefined).
- No muta la entrada (parámetro readonly) y recorre la lista una sola vez.
- El comportamiento ante un empate de winrate es intencionado y está comentado (p. ej. se queda el primero).
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.
// motor.ts — tier mejor: el refactor. Misma conducta, expresada con reduce.
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 {
if (heroes.length === 0) return null;
// reduce arrastra "el mejor hasta ahora": más declarativo que el bucle a mano.
return heroes.reduce((mejor, h) => (h.winrate > mejor.winrate ? h : mejor));
} Por qué es mejor que el anterior
- El refactor: reduce arrastra "el mejor hasta ahora" en una línea, más declarativo. Como los tests siguen en verde, sabemos que el comportamiento no ha cambiado: eso es refactorizar con red.
- Esa confianza para reescribir sin miedo es justo lo que el TDD te regala: el test fija el qué, tú cambias el cómo.
// motor.ts — tier excelente: el mismo reduce, con las decisiones explícitas.
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 {
// Caso vacío primero: sin héroes no hay mvp. null explícito, no undefined.
if (heroes.length === 0) return null;
// reduce sin valor inicial: arranca con el primero y compara contra el resto.
// En EMPATE usamos > (no >=), así que se queda el PRIMERO: resultado determinista.
// No muta la entrada (readonly) y recorre la lista una sola vez.
return heroes.reduce((mejor, h) => (h.winrate > mejor.winrate ? h : mejor));
} Por qué es mejor que el anterior
- Las decisiones quedan explícitas: vacío → null primero, sin mutar la entrada, una sola pasada. Cualquiera que lea la función entiende qué hace en los bordes.
- El empate está pensado y comentado: con > (no >=) se queda el primero, así el resultado es determinista (la R de FIRST: repetible). Un detalle que un test de empate fijaría del todo.
- Mismo comportamiento que el tier OK, pero el código cuenta su propia historia. Ese es el destino del refactor: no más listo, más claro.