En el capítulo anterior copiaste un esqueleto describe / it / expect sin entrar en qué era
cada pieza. Vamos a diseccionarlo. Y a partir de aquí cambia una cosa: las demos corren dentro
de esta página, con los matchers reales de Vitest, para que no tengas que saltar a la
terminal en cada ejemplo. Lo que ves en verde o rojo aquí es lo mismo que verías con pnpm test.
Las tres piezas: describe, it, expect#
Todo test se construye con tres funciones, cada una con un trabajo distinto:
describe(título, fn)— agrupa tests relacionados bajo un título. No comprueba nada por sí misma: organiza. En el informe, su título encabeza sus casos.it(nombre, fn)— declara un caso de test: un nombre en lenguaje llano y una función con el cuerpo de la prueba. (testes su sinónimo exacto; usa el que lea mejor.)expect(valor)— afirma el resultado. Encadenado con un matcher (toBe, y muchos más que verás en el próximo capítulo), declara qué debería cumplirse. Si no se cumple, lanza un error y el test se pone en rojo.
Aquí están las tres en acción. Pulsa «Correr tests» (o edita y vuelve a correr) y mira el panel de resultados: verde es que pasa.
Cada expect(...).toBe(...) es una aserción: una comprobación concreta. Un test puede tener
una o varias; si cualquiera falla, el test entero falla.
El patrón Arrange-Act-Assert#
¿Te has fijado en los comentarios Arrange, Act, Assert del ejemplo? No son decoración: son
el patrón con el que se estructura cualquier test, en tres pasos siempre en el mismo orden:
- Arrange (preparar) — montas los datos de entrada: el héroe, el array, lo que la función necesita.
- Act (actuar) — ejecutas la función bajo prueba, una sola vez, y guardas el resultado.
- Assert (afirmar) — compruebas ese resultado con
expect.
it("calcula el winrate de un héroe", () => {
// Arrange: el escenario.
const heroes: Heroe[] = [{ nombre: "Genji", partidas: 10, victorias: 6 }];
// Act: la acción que se prueba.
const resultado = enriquecer(heroes);
// Assert: la comprobación.
expect(resultado[0].winrate).toBe(60);
});Seguir siempre este orden hace que cualquier test —tuyo o de un compañero— se lea igual: primero
el escenario, luego la acción, luego la afirmación. Cuando la preparación es trivial, Act y Assert
pueden ir en la misma línea (expect(winrate(6, 10)).toBe(60)), pero el orden mental es el mismo.
El nombre del test es documentación#
El nombre que le pones a un it no es un detalle: es una frase que aparece en el informe y que
cuenta qué debe hacer el código. La regla de oro: describe el comportamiento, no la
implementación.
// MAL: no dice nada, o describe el CÓMO interno.
it("test1", ...);
it("funciona", ...);
it("llama a map y a Math.round", ...);
// BIEN: describe el QUÉ, el comportamiento observable.
it("calcula el winrate como victorias entre partidas", ...);¿Y qué tiene de malo “llama a map y a Math.round”? Que si mañana reescribes enriquecer con un
for en vez de un map —mismo resultado, otra implementación—, ese nombre miente: describe un
interior que ya no existe. El nombre que habla de comportamiento sigue siendo cierto. Por eso una
suite bien nombrada se lee como una especificación: pasas la vista por los nombres y entiendes qué
hace el motor sin leer una línea de su código.
Cuando un test falla#
Un test en verde tranquiliza, pero el que enseña es el que se pone en rojo. Mira qué pasa cuando una aserción no se cumple: este test afirma a propósito un valor equivocado.
Verás algo como ✗ winrate › convierte 6 de 10 en 60% y, debajo, expected 60 to be 70. Eso es lo
que hace útil un fallo: te dice qué caso se rompió y qué esperaba frente a qué recibió. No
es un misterio, es una pista. Cambia el 70 por 60 en el editor, vuelve a correr y míralo pasar
a verde.
Acostúmbrate a ver tus tests fallar al menos una vez. Una suite que solo has visto en verde no demuestra que proteja nada: hasta que no la ves saltar cuando rompes algo, no sabes si de verdad vigila.
Comprueba lo que sabes#
Pregunta 1 de 5
¿Cuál es el papel de `describe`?
Tu turno#
Tienes motor.ts con enriquecer y filtrarPorRol, y un motor.test.ts con un test de ejemplo
y dos TODO. Completa la suite siguiendo el esqueleto y el patrón AAA. Recuerda: de momento solo
conoces toBe; para tamaños, expect(algo.length).toBe(n). 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
Escribe la anatomía de los tests del motor
Tienes motor.ts (enriquecer y filtrarPorRol, ya escritas) y un motor.test.ts con un test de ejemplo. Completa la suite: añade casos siguiendo el esqueleto describe → it → expect y el patrón Arrange-Act-Assert. Solo conoces el matcher toBe; para tamaños usa expect(x.length).toBe(n).
Paso 1: Más casos, en verde
- Añades al menos un segundo it dentro del describe de enriquecer (otro héroe, otro winrate).
- Cada test sigue el esqueleto describe → it → expect y pasa (todo en verde).
- Los nombres de los it describen el caso, no son "test1" ni "funciona".
Paso 2: AAA y una segunda función
- Añades un describe("filtrarPorRol", ...) con al menos un test que compruebe que filtra por un rol concreto.
- En los tests se distingue el Arrange (preparar datos), el Act (llamar a la función) y el Assert (expect).
- Los nombres describen comportamiento ("devuelve solo los héroes del rol pedido"), no implementación.
Paso 3: La suite se lee como una especificación
- Cada función tiene su describe, y dentro varios it que cubren casos distintos (incluido el "todos" de filtrarPorRol).
- Leer los nombres de los tests, sin mirar el código, cuenta lo que hace el motor.
- Cada test prepara sus propios datos y afirma una cosa clara; ningún test depende de otro.
Ver soluciones
// motor.test.ts — tier OK: dos casos de enriquecer, esqueleto correcto.
import { describe, it, expect } from "vitest";
import { enriquecer, type Heroe } from "./motor";
describe("enriquecer", () => {
it("calcula el winrate de un héroe", () => {
// Arrange: 6 victorias de 10 partidas.
const heroes: Heroe[] = [
{ nombre: "Genji", rol: "Daño", partidas: 10, victorias: 6 },
];
// Act
const resultado = enriquecer(heroes);
// Assert: 6/10 = 60%.
expect(resultado[0].winrate).toBe(60);
});
it("calcula el winrate de otro héroe", () => {
// Arrange: 2 victorias de 8 partidas.
const heroes: Heroe[] = [
{ nombre: "Reinhardt", rol: "Tanque", partidas: 8, victorias: 2 },
];
// Act
const resultado = enriquecer(heroes);
// Assert: 2/8 = 25%.
expect(resultado[0].winrate).toBe(25);
});
}); Por qué este nivel
- Dos casos de enriquecer con nombres que describen el caso: cumple el esqueleto y pasa. Suficiente para "correcto".
- Su límite: solo prueba una función, y los dos casos son casi iguales (mismo camino, distinto número). No toca filtrarPorRol ni el patrón AAA de forma explícita.
// motor.test.ts — tier mejor: AAA explícito y un segundo describe para filtrarPorRol.
import { describe, it, expect } from "vitest";
import { enriquecer, filtrarPorRol, type Heroe } from "./motor";
describe("enriquecer", () => {
it("añade el winrate como porcentaje de victorias sobre partidas", () => {
// Arrange: preparamos el dato de entrada.
const heroes: Heroe[] = [
{ nombre: "Genji", rol: "Daño", partidas: 10, victorias: 6 },
];
// Act: ejecutamos la función bajo prueba.
const resultado = enriquecer(heroes);
// Assert: afirmamos el resultado.
expect(resultado[0].winrate).toBe(60);
});
});
describe("filtrarPorRol", () => {
it("devuelve solo los héroes del rol pedido", () => {
// Arrange: un equipo con dos roles distintos.
const heroes: Heroe[] = [
{ nombre: "Genji", rol: "Daño", partidas: 10, victorias: 6 },
{ nombre: "Reinhardt", rol: "Tanque", partidas: 8, victorias: 5 },
];
// Act: filtramos por Tanque.
const tanques = filtrarPorRol(heroes, "Tanque");
// Assert: hay uno solo, y es Reinhardt. (length + toBe: aún no conoces toHaveLength.)
expect(tanques.length).toBe(1);
expect(tanques[0].nombre).toBe("Reinhardt");
});
}); Por qué es mejor que el anterior
- Aparece un segundo describe para filtrarPorRol: la suite empieza a cubrir el motor, no una sola función.
- Los comentarios Arrange / Act / Assert hacen explícito el patrón. No es obligatorio comentarlos siempre, pero aquí ayudan a fijar el orden mental.
- Comprueba el tamaño con length + toBe (todavía no conoces toHaveLength): se puede testear de sobra con lo que ya sabes.
// motor.test.ts — tier excelente: la suite se lee como una especificación.
// Cada it describe un COMPORTAMIENTO; cada test prepara sus propios datos.
import { describe, it, expect } from "vitest";
import { enriquecer, filtrarPorRol, type Heroe } from "./motor";
describe("enriquecer", () => {
it("calcula el winrate como porcentaje de victorias sobre partidas", () => {
// Arrange
const heroes: Heroe[] = [
{ nombre: "Genji", rol: "Daño", partidas: 10, victorias: 6 },
];
// Act
const resultado = enriquecer(heroes);
// Assert: 6/10 = 60%.
expect(resultado[0].winrate).toBe(60);
});
it("deja intactos los héroes de entrada (devuelve copias nuevas)", () => {
// Arrange
const heroes: Heroe[] = [
{ nombre: "Genji", rol: "Daño", partidas: 10, victorias: 6 },
];
// Act
enriquecer(heroes);
// Assert: el original NO debería tener winrate; enriquecer no muta su entrada.
expect("winrate" in heroes[0]).toBe(false);
});
});
describe("filtrarPorRol", () => {
it("devuelve solo los héroes del rol pedido", () => {
// Arrange
const heroes: Heroe[] = [
{ nombre: "Genji", rol: "Daño", partidas: 10, victorias: 6 },
{ nombre: "Reinhardt", rol: "Tanque", partidas: 8, victorias: 5 },
];
// Act
const tanques = filtrarPorRol(heroes, "Tanque");
// Assert
expect(tanques.length).toBe(1);
expect(tanques[0].nombre).toBe("Reinhardt");
});
it("con 'todos' devuelve el equipo entero sin filtrar", () => {
// Arrange
const heroes: Heroe[] = [
{ nombre: "Genji", rol: "Daño", partidas: 10, victorias: 6 },
{ nombre: "Reinhardt", rol: "Tanque", partidas: 8, victorias: 5 },
];
// Act + Assert: "todos" no descarta a nadie.
expect(filtrarPorRol(heroes, "todos").length).toBe(2);
});
}); Por qué es mejor que el anterior
- La suite se lee como una especificación: "enriquecer calcula el winrate", "deja intactos los héroes de entrada", "filtrarPorRol devuelve solo los del rol", "con todos devuelve el equipo entero". Cuatro frases que cuentan qué hace el motor.
- El test de "no muta la entrada" comprueba un comportamiento que un humano olvidaría a mano: que enriquecer devuelve copias. Eso es lo que hace valiosa una suite, no repetir el caso feliz.
- Cada test prepara sus propios datos: ninguno depende del orden ni de lo que dejó otro. Esa independencia es lo que veremos a fondo en "Qué hace bueno a un test".