Llegaste al final del nivel. Toca el encargo que lo junta todo: dotar al motor del Team Builder —el que tipaste en el Nivel 5— de una suite de tests completa.
No hay nada nuevo que aprender aquí. Es síntesis: coger lo de cada capítulo y aplicarlo, de una vez, sobre código que ya conoces.
- Unitarios de cada función, con su caso típico y sus límites (vacío, un elemento, fronteras como winrate 0 y 100, filtros sin coincidencias).
- Los fallos silenciosos: que las funciones no muten su entrada, que
validarHeroesdescarte cada tipo de dato corrupto. - Parametrizados con
it.eachpara las tablas, aislados conbeforeEach, y un builder para no repetir datos. - El matcher correcto en cada sitio:
toEqualpara objetos y arrays,toBeCloseTopara decimales,toThrowpara los errores,toHaveLengthpara los tamaños. - La integración con
procesar: el pipeline entero, de datos crudos a ranking. - Y una mirada a la cobertura (
pnpm coverage) para encontrar agujeros —sin perseguir el 100% a ciegas—.
Recuerda que este proyecto se construye sobre el anterior: el motor tipado del Nivel 5 sigue intacto, y tú le añades la red de tests encima. Acumulas una capa más —HTML, JS, tipos, y ahora tests— sin descartar nada. Es exactamente como crece un proyecto de verdad.
Cuando tu suite corra en verde y cubra el motor de punta a punta, habrás cerrado el Nivel 6: sabes escribir tests que protegen sobre lógica pura. Lo siguiente es React (Nivel 7); y en el Nivel 8, el testing vuelve para acompañarlo en el DOM, lo asíncrono y la red.
Comprueba lo que sabes#
Pregunta 1 de 4
Una suite completa del motor, ¿qué debería cubrir de cada función?
Tu turno#
Este es un proyecto en local: el testing vive en tu terminal. Clona la carpeta, escribe la suite
completa y déjala en verde con pnpm test. Mira la cobertura con pnpm coverage (la primera vez te
pedirá instalar el proveedor @vitest/coverage-v8; acepta y listo). Cuando lo tengas
(o si te atascas), despliega las soluciones y fíjate en el salto de un nivel al siguiente: de los
unitarios del caso feliz a una suite que cubre el motor entero, integración incluida.
Ejercicio · hazlo en local
La suite de tests completa del motor
Coge el motor del Team Builder ya tipado (validarHeroes con Zod, enriquecer, filtrarPorRol, rankearPorWinrate, agruparPorRol y procesar) y dótalo de una suite de tests completa que corra en verde. Cubre cada función por su caso típico, sus límites y su fallo silencioso, prueba la integración del pipeline, y mira la cobertura. No hay concepto nuevo: junta todo lo del nivel.
Paso 1: Unitarios de las funciones núcleo
- enriquecer, filtrarPorRol y rankearPorWinrate tienen tests del caso típico que pasan en verde.
- Comparas los resultados con el matcher adecuado (toEqual, toHaveLength), no a ojo.
- Los nombres de los tests describen el comportamiento, no la implementación.
Paso 2: Límites, parametrizados y la frontera
- Cubres los límites: lista vacía, winrate 0 y 100, y que las funciones NO mutan su entrada.
- Usas it.each para las tablas (winrate, datos corruptos) y beforeEach para datos frescos.
- validarHeroes se prueba: descarta cada tipo de dato corrupto (rol inválido, campo ausente, tipo equivocado).
Paso 3: Suite completa, con integración y cobertura
- Cubres TODAS las funciones, incluida agruparPorRol (agrupa por rol, omite los roles vacíos).
- Pruebas la integración con procesar: el pipeline entero, de datos crudos a ranking, y que lo corrupto nunca llega al resultado.
- Usas un builder para los datos de prueba, la suite se lee como la especificación del motor, y has mirado la cobertura (pnpm coverage) para encontrar agujeros, sin perseguir el 100% a ciegas.
Cómo hacerlo en local
Clona el repositorio del curso, entra en la carpeta del ejercicio y abre el
index.html en tu navegador. Toda tu solución va en
solucion.js.
git clone <repo>
cd exercises/nivel-6/proyecto-suite-del-motor
# abre index.html en el navegador y edita solucion.js Ver soluciones
// motor.test.ts — tier OK: tests unitarios de las funciones núcleo, caso feliz.
import {
enriquecer,
filtrarPorRol,
rankearPorWinrate,
type Heroe,
} from "./motor";
import { describe, it, expect } from "vitest";
describe("enriquecer", () => {
it("calcula el winrate como victorias entre partidas", () => {
const heroes: Heroe[] = [
{ nombre: "Genji", rol: "Daño", partidas: 10, victorias: 6 },
];
expect(enriquecer(heroes)[0].winrate).toBe(60);
});
});
describe("filtrarPorRol", () => {
it("devuelve solo los héroes del rol pedido", () => {
const heroes = enriquecer([
{ nombre: "Genji", rol: "Daño", partidas: 10, victorias: 6 },
{ nombre: "Reinhardt", rol: "Tanque", partidas: 8, victorias: 5 },
]);
expect(filtrarPorRol(heroes, "Tanque").map((h) => h.nombre)).toEqual([
"Reinhardt",
]);
});
it("con 'todos' no descarta a nadie", () => {
const heroes = enriquecer([
{ nombre: "Genji", rol: "Daño", partidas: 10, victorias: 6 },
{ nombre: "Reinhardt", rol: "Tanque", partidas: 8, victorias: 5 },
]);
expect(filtrarPorRol(heroes, "todos")).toHaveLength(2);
});
});
describe("rankearPorWinrate", () => {
it("ordena de mayor a menor winrate", () => {
const heroes = enriquecer([
{ nombre: "Reinhardt", rol: "Tanque", partidas: 8, victorias: 2 },
{ nombre: "Genji", rol: "Daño", partidas: 10, victorias: 6 },
]);
expect(rankearPorWinrate(heroes).map((h) => h.nombre)).toEqual([
"Genji",
"Reinhardt",
]);
});
}); Por qué este nivel
- Tests unitarios del caso feliz de las tres funciones núcleo, con matchers y nombres correctos. Es una base sólida: si alguien rompe el cálculo del winrate o el orden del ranking, salta.
- Su límite: ni límites ni integración ni validación. El caso típico está cubierto; los bordes —donde viven los bugs— todavía no.
// motor.test.ts — tier mejor: fronteras con it.each, aislamiento con beforeEach,
// inmutabilidad y la validación que descarta lo corrupto.
import {
enriquecer,
filtrarPorRol,
rankearPorWinrate,
validarHeroes,
type Heroe,
} from "./motor";
import { describe, it, expect, beforeEach } from "vitest";
describe("enriquecer", () => {
it.each([
[6, 10, 60],
[0, 8, 0],
[8, 8, 100],
])("%i victorias de %i partidas da winrate %i", (victorias, partidas, esperado) => {
const heroes: Heroe[] = [{ nombre: "X", rol: "Daño", partidas, victorias }];
expect(enriquecer(heroes)[0].winrate).toBe(esperado);
});
it("no muta la entrada", () => {
const heroes: Heroe[] = [
{ nombre: "Genji", rol: "Daño", partidas: 10, victorias: 6 },
];
enriquecer(heroes);
expect("winrate" in heroes[0]).toBe(false);
});
});
describe("filtrarPorRol", () => {
let heroes: ReturnType<typeof enriquecer>;
beforeEach(() => {
heroes = enriquecer([
{ nombre: "Genji", rol: "Daño", partidas: 10, victorias: 6 },
{ nombre: "Reinhardt", rol: "Tanque", partidas: 8, victorias: 5 },
]);
});
it("devuelve solo los héroes del rol pedido", () => {
expect(filtrarPorRol(heroes, "Daño").map((h) => h.nombre)).toEqual(["Genji"]);
});
it("devuelve [] cuando ningún héroe tiene el rol", () => {
expect(filtrarPorRol(heroes, "Apoyo")).toEqual([]);
});
});
describe("rankearPorWinrate", () => {
it("con lista vacía devuelve []", () => {
expect(rankearPorWinrate([])).toEqual([]);
});
it("no muta la entrada", () => {
const heroes = enriquecer([
{ nombre: "Reinhardt", rol: "Tanque", partidas: 8, victorias: 2 },
{ nombre: "Genji", rol: "Daño", partidas: 10, victorias: 6 },
]);
rankearPorWinrate(heroes);
expect(heroes[0].nombre).toBe("Reinhardt");
});
});
describe("validarHeroes", () => {
it.each([
[{ rol: "Daño", partidas: 5, victorias: 3 }], // falta el nombre
[{ nombre: "X", rol: "Healer", partidas: 5, victorias: 3 }], // rol inválido
[{ nombre: "X", rol: "Daño", partidas: "5", victorias: 3 }], // tipo erróneo
])("descarta el dato corrupto %o", (corrupto) => {
expect(validarHeroes([corrupto])).toEqual([]);
});
}); Por qué es mejor que el anterior
- Añade lo que más protege: las fronteras (vacío, 0, 100), la inmutabilidad y la validación que descarta lo corrupto, todo con it.each y beforeEach para no repetirse.
- La tabla de datos corruptos de validarHeroes es media suite por sí sola: comprueba que la frontera rechaza cada forma de basura, que es justo su trabajo.
// motor.test.ts — tier excelente: la suite COMPLETA. Builder, aislamiento, las
// fronteras de cada función, agruparPorRol y la integración del pipeline.
import {
enriquecer,
rankearPorWinrate,
agruparPorRol,
validarHeroes,
procesar,
type Heroe,
} from "./motor";
import { describe, it, expect, beforeEach } from "vitest";
// Builder: un héroe de prueba válido; cada test sobreescribe solo lo relevante.
function heroe(overrides: Partial<Heroe> = {}): Heroe {
return { nombre: "Genji", rol: "Daño", partidas: 10, victorias: 6, ...overrides };
}
describe("enriquecer", () => {
it.each([
[6, 10, 60],
[0, 8, 0],
[8, 8, 100],
])("%i victorias de %i partidas da winrate %i", (victorias, partidas, esperado) => {
expect(enriquecer([heroe({ partidas, victorias })])[0].winrate).toBe(esperado);
});
it("no muta la entrada", () => {
const heroes = [heroe()];
enriquecer(heroes);
expect("winrate" in heroes[0]).toBe(false);
});
});
describe("rankearPorWinrate", () => {
it("ordena de mayor a menor sin mutar la entrada", () => {
const heroes = enriquecer([
heroe({ nombre: "Bajo", partidas: 8, victorias: 2 }),
heroe({ nombre: "Alto", partidas: 10, victorias: 9 }),
]);
expect(rankearPorWinrate(heroes).map((h) => h.nombre)).toEqual(["Alto", "Bajo"]);
// El original conserva su orden: rankear devolvió una copia.
expect(heroes[0].nombre).toBe("Bajo");
});
it("aguanta los límites (vacío y un elemento)", () => {
expect(rankearPorWinrate([])).toEqual([]);
expect(rankearPorWinrate(enriquecer([heroe()]))).toHaveLength(1);
});
});
describe("agruparPorRol", () => {
it("reparte por rol y omite los roles sin héroes", () => {
const heroes = enriquecer([
heroe({ nombre: "Mercy", rol: "Apoyo" }),
heroe({ nombre: "Ana", rol: "Apoyo" }),
heroe({ nombre: "Genji", rol: "Daño" }),
]);
const grupos = agruparPorRol(heroes);
expect(grupos.Apoyo?.map((h) => h.nombre)).toEqual(["Mercy", "Ana"]);
// Ningún Tanque en la entrada: ese rol no aparece en el resultado.
expect(grupos.Tanque).toBeUndefined();
});
});
describe("validarHeroes", () => {
it.each([
[{ rol: "Daño", partidas: 5, victorias: 3 }], // falta nombre
[{ nombre: "X", rol: "Healer", partidas: 5, victorias: 3 }], // rol inexistente
[{ nombre: "X", rol: "Daño", partidas: 0, victorias: 3 }], // partidas no positivo
])("descarta el dato corrupto %o", (corrupto) => {
expect(validarHeroes([corrupto])).toEqual([]);
});
it("conserva los datos válidos", () => {
const ok = { nombre: "Genji", rol: "Daño", partidas: 10, victorias: 6 };
expect(validarHeroes([ok])).toHaveLength(1);
});
});
describe("procesar (integración del pipeline)", () => {
let crudos: unknown[];
beforeEach(() => {
crudos = [
{ nombre: "Reinhardt", rol: "Tanque", partidas: 8, victorias: 2 },
{ nombre: "Genji", rol: "Daño", partidas: 10, victorias: 6 },
{ nombre: "Roto", rol: "Healer", partidas: 5, victorias: 3 }, // corrupto
];
});
it("valida, enriquece, filtra y rankea de una tirada", () => {
const ranking = procesar(crudos, "todos");
expect(ranking.map((h) => h.nombre)).toEqual(["Genji", "Reinhardt"]);
expect(ranking[0].winrate).toBe(60);
});
it("el dato corrupto nunca llega al resultado", () => {
expect(procesar(crudos, "todos").map((h) => h.nombre)).not.toContain("Roto");
});
}); Por qué es mejor que el anterior
- La suite completa: todas las funciones (incluida agruparPorRol), la integración del pipeline con procesar, y un builder que mantiene cada test enfocado en lo que prueba.
- El test de integración —"el dato corrupto nunca llega al resultado"— es el que ata el nivel: demuestra que las piezas, juntas, hacen lo correcto y que la frontera protege a las de abajo.
- Leída de arriba abajo, la suite cuenta qué hace el motor y qué garantiza en los bordes. Eso es lo que de verdad protege un refactor: no la cobertura, sino tests que fijan el comportamiento que importa.