Hasta ahora has probado piezas sueltas: enriquecer sola, filtrarPorRol sola. Pero la app no
usa las piezas por separado: las encadena. Un dato crudo entra, se valida, se enriquece, se
filtra y se rankea, todo seguido. Que cada función pase sus tests no garantiza que encajen bien
juntas: ahí entra el test de integración.
Qué cubre la integración que los unitarios no ven#
Un test unitario dice “esta pieza, sola, funciona”. Un test de integración dice “estas piezas,
juntas, hacen lo correcto”. Y eso es distinto: el bug puede estar en la costura —un campo que
una función espera y la anterior no garantiza, un orden de pasos equivocado—, no dentro de ninguna
pieza. El motor expone procesar, que es el pipeline completo:
// El camino real de un dato en la app: de crudo a ranking listo.
export function procesar(crudos: unknown[], rol: Rol | "todos") {
const validos = validarHeroes(crudos); // 1) frontera: descarta basura
const enriquecidos = enriquecer(validos); // 2) añade winrate
const filtrados = filtrarPorRol(enriquecidos, rol); // 3) por rol
return rankearPorWinrate(filtrados); // 4) por winrate
}Probarlo entero, con datos crudos que incluyen alguno corrupto, comprueba la cadena completa:
El héroe con rol "Healer" (que no existe) se cae en la validación y nunca llega al ranking; los
válidos salen ordenados y enriquecidos. Ningún test unitario de una sola pieza habría
comprobado que todo eso ocurre en una tirada.
La frontera con Zod: síncrona, sin nada que simular#
La primera pieza del pipeline es la validación en frontera que montaste en el Nivel 5 con
Zod: validarHeroes usa safeParse para descartar lo que no cumple el schema. Y aquí va algo
importante para este nivel: esa validación es síncrona y pura. Le pasas datos, te devuelve qué
pasó, al momento. No hay red, ni tiempo de espera, ni dependencia externa que fingir.
Por eso se prueba igual que cualquier función pura: datos de entrada, afirmación sobre la salida.
El «¿y qué?» de poner la validación primero en el pipeline: si un dato con partidas: 0 (o
partidas: "8") se colara hasta enriquecer, el winrate saldría NaN o Infinity y contaminaría
el ranking sin un error visible. La frontera lo frena antes. El test de integración demuestra
que esa protección está en su sitio.
Cuando en el Nivel 8 aparezcan la red y lo asíncrono —pedir datos a una API de verdad—, ahí sí harás falta simular dependencias (los dobles de test). En este nivel todo es síncrono y puro, así que no hace falta: es deliberado.
Comprueba lo que sabes#
Pregunta 1 de 5
¿Qué prueba un test de integración que los unitarios no ven?
Tu turno#
Prueba el pipeline procesar como una unidad y la frontera validarHeroes por su cuenta. 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 pipeline y su frontera
Tienes el motor con validarHeroes (Zod, síncrona) y procesar (el pipeline: validar → enriquecer → filtrar → rankear). Prueba el pipeline completo como una unidad y la validación en frontera, que es lo que protege a las piezas de abajo de los datos corruptos.
Paso 1: El pipeline, de crudo a ranking
- procesar descarta lo corrupto y devuelve los válidos rankeados por winrate.
- Compruebas que además ENRIQUECE: el primero del ranking tiene su winrate calculado.
Paso 2: Filtro por rol y descarte dentro del pipeline
- procesar con un rol concreto devuelve solo los héroes de ese rol.
- Compruebas explícitamente que el héroe con rol inválido NO aparece en el resultado.
- Usas el matcher que mejor expresa cada caso (toEqual, toHaveLength, .not.toContain).
Paso 3: La frontera, probada por su cuenta
- Pruebas validarHeroes directamente: descarta cada tipo de dato corrupto (rol inexistente, campo ausente, tipo equivocado) y deja solo lo válido.
- Cubres el caso que protege a las piezas de abajo: un dato que haría dividir entre cero (partidas: 0) se descarta en la frontera, no llega a enriquecer.
- Queda claro, leyendo los tests, qué garantiza el pipeline entero y qué garantiza su validación de entrada.
Ver soluciones
// motor.test.ts — tier OK: el pipeline descarta lo corrupto, rankea y enriquece.
import { describe, it, expect } from "vitest";
import { procesar } from "./motor";
describe("procesar (pipeline completo)", () => {
const 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 },
];
it("descarta lo corrupto y rankea por winrate", () => {
expect(procesar(crudos, "todos").map((h) => h.nombre)).toEqual([
"Genji",
"Reinhardt",
]);
});
it("enriquece por el camino (calcula el winrate)", () => {
// No solo ordena: el resultado lleva el winrate añadido por enriquecer.
expect(procesar(crudos, "todos")[0].winrate).toBe(60);
});
}); Por qué este nivel
- Prueba procesar de punta a punta: que descarta lo corrupto, rankea, y además enriquece. Eso ya cubre el camino real que recorre un dato en la app.
- Su límite: no comprueba el filtro por rol ni mira la frontera (validarHeroes) por separado; si la validación se rompiera, este test podría no notarlo.
// motor.test.ts — tier mejor: el pipeline con filtro por rol y descarte explícito.
import { describe, it, expect } from "vitest";
import { procesar } from "./motor";
const crudos = [
{ nombre: "Reinhardt", rol: "Tanque", partidas: 8, victorias: 2 },
{ nombre: "Genji", rol: "Daño", partidas: 10, victorias: 6 },
{ nombre: "Mercy", rol: "Apoyo", partidas: 20, victorias: 15 },
{ nombre: "Roto", rol: "Healer", partidas: 5, victorias: 3 },
];
describe("procesar (pipeline completo)", () => {
it("con 'todos' devuelve los válidos rankeados por winrate", () => {
expect(procesar(crudos, "todos").map((h) => h.nombre)).toEqual([
"Mercy",
"Genji",
"Reinhardt",
]);
});
it("filtra por rol dentro del pipeline", () => {
const danos = procesar(crudos, "Daño");
expect(danos).toHaveLength(1);
expect(danos[0].nombre).toBe("Genji");
});
it("descarta el héroe con rol inválido (Healer)", () => {
const nombres = procesar(crudos, "todos").map((h) => h.nombre);
expect(nombres).not.toContain("Roto");
});
}); Por qué es mejor que el anterior
- Añade el filtro por rol dentro del pipeline y un test explícito de que el héroe inválido NO aparece. Comprueba la interacción, no solo el caso feliz del conjunto.
- .not.toContain("Roto") es una afirmación sobre el RESULTADO del pipeline: el dato corrupto no llega al final. Es el tipo de cosa que un unitario de una sola pieza no ve.
// motor.test.ts — tier excelente: el pipeline Y la frontera de validación, que
// es lo que une las piezas y lo que protege a las de abajo de la basura.
import { describe, it, expect } from "vitest";
import { procesar, validarHeroes } from "./motor";
describe("procesar (pipeline completo)", () => {
it("valida → enriquece → filtra → rankea de una tirada", () => {
const crudos = [
{ nombre: "Reinhardt", rol: "Tanque", partidas: 8, victorias: 2 },
{ nombre: "Genji", rol: "Daño", partidas: 10, victorias: 6 },
];
const resultado = procesar(crudos, "todos");
expect(resultado.map((h) => h.nombre)).toEqual(["Genji", "Reinhardt"]);
// La integración: el ranking además trae el winrate, no solo el orden.
expect(resultado[0].winrate).toBe(60);
});
});
describe("validarHeroes (la frontera, síncrona con Zod)", () => {
it("descarta cada tipo de dato corrupto y deja solo lo válido", () => {
const datos = [
{ nombre: "Genji", rol: "Daño", partidas: 10, victorias: 6 }, // válido
{ nombre: "Roto", rol: "Healer", partidas: 5, victorias: 3 }, // rol inexistente
{ rol: "Daño", partidas: 5, victorias: 3 }, // falta el nombre
{ nombre: "Mal", rol: "Tanque", partidas: "8", victorias: 2 }, // partidas no es número
];
// Solo el primero cumple el schema; los otros tres se caen.
expect(validarHeroes(datos)).toHaveLength(1);
});
it("frena la basura ANTES de enriquecer (un 0 partidas no llega a dividir)", () => {
// partidas debe ser positive (>0): 0 no pasa el schema, así que enriquecer
// nunca calcula 5/0 = Infinity. La frontera protege a las piezas de abajo.
const sucios = [{ nombre: "Mal", rol: "Tanque", partidas: 0, victorias: 5 }];
expect(validarHeroes(sucios)).toEqual([]);
});
}); Por qué es mejor que el anterior
- Separa lo que prueba: procesar (el pipeline entero) y validarHeroes (la frontera). Cada describe documenta una garantía distinta del sistema.
- El test de "frena la basura antes de enriquecer" es el corazón de la integración: demuestra que la validación protege a las piezas de abajo de un dato que las rompería (partidas: 0 → división entre cero). Esa interacción es justo lo que un test de integración existe para vigilar.
- Validar es síncrono y puro, así que todo esto se prueba sin simular nada. Cuando en el Nivel 8 aparezcan la red y lo asíncrono, harán falta dobles de test; aquí todavía no.