Ya tienes las herramientas: describe, it, expect y los matchers. Toca usarlas en serio sobre
el motor del Team Builder —el que tipaste en el Nivel 5—. Y empezamos por entender por qué ese
motor es tan fácil de probar: porque sus funciones son puras.
Por qué una función pura es fácil de testear#
Una función pura cumple dos cosas: con la misma entrada da siempre la misma salida, y
no produce efectos secundarios (no muta sus argumentos, no toca variables de fuera, ni el DOM,
ni la red). enriquecer, filtrarPorRol y rankearPorWinrate son así: entra un array, sale un
array nuevo, y nada más cambia en el mundo.
Eso las vuelve triviales de testear: las llamas y compruebas lo que devuelven. No hay que montar un navegador, ni un servidor, ni un estado global, ni limpiar nada entre tests.
// Probar una función pura: una línea de preparación, una de acción, una de afirmación.
const ranking = rankearPorWinrate(heroes);
expect(ranking[0].nombre).toBe("Genji");Compáralo con una función con efecto secundario —digamos, una que empuja el resultado a un array global en vez de devolverlo—: para testearla tendrías que preparar ese array, llamar a la función, y luego inspeccionar el array de fuera para ver qué pasó, y vaciarlo antes del siguiente test. Más montaje, más frágil. Por eso el motor se escribió puro: la testabilidad no es casualidad, es una decisión de diseño. Cuando puedas elegir, escribe funciones puras.
Tres cosas que probar de cada función#
Para cada función, no te quedes en “¿funciona con datos normales?”. Prueba tres cosas:
- El caso feliz — la entrada típica produce la salida esperada.
- Los límites — las entradas extremas: una lista vacía, un solo elemento, los valores frontera (un winrate de 0, de 100). Es donde se esconden los bugs.
- El caso que falla en silencio — un resultado que parece bien pero está mal.
Aquí está rankearPorWinrate probada así: el caso feliz, el límite de la lista vacía y la garantía
de que no muta la entrada.
El fallo que no se ve#
El caso más peligroso es el que no lanza ningún error. Mira esta versión de
rankearPorWinrate con un bug sutil: ordena de menor a mayor por una resta invertida.
No revienta, no avisa: devuelve un array perfectamente válido, solo que en el orden equivocado. Mirando dos héroes podrías no darte cuenta; en producción, el ranking del Team Builder saldría del revés y los peores héroes aparecerían arriba, sin un solo error en la consola. El test afirma el comportamiento esperado —“el de mayor winrate va primero”— y caza justo eso. Ese es el «¿y qué?»: un bug silencioso no se ve a ojo, pero un test lo convierte en rojo.
Comprueba lo que sabes#
Pregunta 1 de 5
¿Por qué una función pura es la más fácil de testear?
Tu turno#
Completa la suite del motor: para cada función, el caso feliz, los límites y el caso que falla en
silencio. Empieza por los TODO de rankearPorWinrate. 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
Prueba el motor función a función
Tienes el motor (enriquecer, filtrarPorRol, rankearPorWinrate, las tres puras) y un test con el caso feliz de rankearPorWinrate. Completa la suite cubriendo, para cada función, el caso feliz, los límites y el caso que falla en silencio.
Paso 1: Caso feliz bien comprobado
- rankearPorWinrate ordena de mayor a menor: lo compruebas con varios héroes, no solo dos.
- Comparas el resultado con toEqual (la lista de nombres o el array), no a ojo.
Paso 2: Los límites de rankearPorWinrate
- Pruebas la lista vacía: rankearPorWinrate([]) devuelve [].
- Pruebas un solo héroe: lo devuelve igual.
- Pruebas que NO muta la entrada (el array original conserva su orden).
Paso 3: Las tres funciones y sus fronteras
- enriquecer: winrate 0 (sin victorias) y 100 (gana todas), los dos valores frontera.
- filtrarPorRol: devuelve [] cuando ningún héroe tiene el rol (ni null, ni undefined, ni error).
- rankearPorWinrate: orden correcto y sin mutar. Cada función prueba su caso típico Y sus bordes.
Ver soluciones
// motor.test.ts — tier OK: el caso feliz de rankearPorWinrate, bien comprobado.
import { describe, it, expect } from "vitest";
import { rankearPorWinrate, type HeroeEnriquecido } from "./motor";
describe("rankearPorWinrate", () => {
it("ordena de mayor a menor winrate", () => {
const heroes: HeroeEnriquecido[] = [
{ nombre: "Reinhardt", rol: "Tanque", partidas: 8, victorias: 2, winrate: 25 },
{ nombre: "Genji", rol: "Daño", partidas: 10, victorias: 6, winrate: 60 },
{ nombre: "Mercy", rol: "Apoyo", partidas: 20, victorias: 15, winrate: 75 },
];
// Comparamos la lista de nombres: refleja el orden por winrate (75, 60, 25).
const nombres = rankearPorWinrate(heroes).map((h) => h.nombre);
expect(nombres).toEqual(["Mercy", "Genji", "Reinhardt"]);
});
}); Por qué este nivel
- Comprueba el caso feliz con TRES héroes y compara la lista de nombres con toEqual: si el orden se rompe, el test lo dice. Suficiente para "correcto".
- Su límite: solo prueba el caso típico de una función. Con una lista vacía o un solo héroe no sabemos qué pasa; ahí es donde se esconden los bugs.
// motor.test.ts — tier mejor: el caso feliz Y los límites (vacío, uno, no mutar).
import { describe, it, expect } from "vitest";
import { rankearPorWinrate, type HeroeEnriquecido } from "./motor";
const equipo: HeroeEnriquecido[] = [
{ nombre: "Reinhardt", rol: "Tanque", partidas: 8, victorias: 2, winrate: 25 },
{ nombre: "Genji", rol: "Daño", partidas: 10, victorias: 6, winrate: 60 },
];
describe("rankearPorWinrate", () => {
it("ordena de mayor a menor winrate (caso feliz)", () => {
const nombres = rankearPorWinrate(equipo).map((h) => h.nombre);
expect(nombres).toEqual(["Genji", "Reinhardt"]);
});
it("con una lista vacía devuelve una lista vacía (límite)", () => {
expect(rankearPorWinrate([])).toEqual([]);
});
it("con un solo héroe lo devuelve igual (límite)", () => {
expect(rankearPorWinrate([equipo[1]])).toHaveLength(1);
});
it("no muta la entrada (devuelve una copia ordenada)", () => {
rankearPorWinrate(equipo);
// El original conserva su orden de partida: Reinhardt seguía primero.
expect(equipo[0].nombre).toBe("Reinhardt");
});
}); Por qué es mejor que el anterior
- Añade los límites de rankearPorWinrate: vacío, un elemento y la garantía de que no muta la entrada. Eso ya cubre los bordes donde más se rompe.
- El test de "no muta" es el que un humano olvida: comprueba una promesa de la función (devolver una copia) que no se ve en el resultado de un único caso.
// motor.test.ts — tier excelente: las tres funciones, con sus fronteras y el
// caso que falla en silencio si nadie lo vigila.
import { describe, it, expect } from "vitest";
import {
enriquecer,
filtrarPorRol,
rankearPorWinrate,
type Heroe,
type HeroeEnriquecido,
} from "./motor";
describe("enriquecer", () => {
it("calcula winrate 0 cuando no hay victorias (frontera)", () => {
const heroes: Heroe[] = [
{ nombre: "Bastion", rol: "Daño", partidas: 8, victorias: 0 },
];
expect(enriquecer(heroes)[0].winrate).toBe(0);
});
it("calcula winrate 100 cuando gana todas (frontera)", () => {
const heroes: Heroe[] = [
{ nombre: "Mercy", rol: "Apoyo", partidas: 8, victorias: 8 },
];
expect(enriquecer(heroes)[0].winrate).toBe(100);
});
});
describe("filtrarPorRol", () => {
it("devuelve [] cuando ningún héroe tiene el rol (ni null ni error)", () => {
const heroes = enriquecer([
{ nombre: "Genji", rol: "Daño", partidas: 10, victorias: 6 },
]);
// El fallo silencioso a evitar: devolver undefined/null en vez de una lista vacía.
expect(filtrarPorRol(heroes, "Tanque")).toEqual([]);
});
});
describe("rankearPorWinrate", () => {
const equipo: HeroeEnriquecido[] = [
{ nombre: "Reinhardt", rol: "Tanque", partidas: 8, victorias: 2, winrate: 25 },
{ nombre: "Genji", rol: "Daño", partidas: 10, victorias: 6, winrate: 60 },
];
it("pone primero al de mayor winrate", () => {
expect(rankearPorWinrate(equipo).map((h) => h.nombre)).toEqual([
"Genji",
"Reinhardt",
]);
});
it("no muta la entrada", () => {
rankearPorWinrate(equipo);
expect(equipo[0].nombre).toBe("Reinhardt");
});
}); Por qué es mejor que el anterior
- Cubre las TRES funciones, cada una con sus fronteras: enriquecer en 0 y 100, filtrarPorRol con cero coincidencias, rankearPorWinrate con orden e inmutabilidad.
- El test de filtrarPorRol con [] vigila un fallo silencioso clásico: que en vez de una lista vacía devolvieras null o undefined, y reventara más adelante quien hiciera .map sobre el resultado.
- La suite entera se lee como el contrato del motor: qué hace cada función con datos normales y qué hace en los extremos. Eso es lo que protege un refactor.