learning-front

Nivel 6 · Introducción al testing

Integración: piezas puras trabajando juntas

Probar el pipeline completo del motor (validar → enriquecer → filtrar → rankear) como una sola unidad, y la validación en frontera con Zod del nivel anterior (que es síncrona: no necesita simular nada). Qué cubre un test de integración que los unitarios no ven: la interacción entre piezas.

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:

typescript
// 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.
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.