learning-front

Nivel 8 · Calidad: que no se rompa en producción

Integración de features y snapshots

Probar varios componentes, el estado y la red (con MSW) juntos siguiendo un flujo de usuario real: el corazón del trofeo de testing. Y el snapshot testing: cuándo ayuda de verdad y cuándo solo crea ruido frágil que todos terminan aprobando sin mirar.

Hasta aquí has probado piezas sueltas: un componente con Testing Library, un hook con renderHook, una dependencia con un doble, la red con MSW. Pero una persona no usa “un componente”: usa una feature —varias piezas que colaboran, con un estado compartido y datos que vienen de la red—. Probar ese conjunto, siguiendo un flujo real de usuario, es un test de integración. Y es, con diferencia, el tipo de test que más confianza da.

El trofeo de testing#

En el Nivel 6 viste la pirámide clásica: muchos unitarios abajo, algunos de integración en medio, pocos e2e arriba. El frontend moderno la recoloca en una figura distinta, el trofeo de testing (Kent C. Dodds), donde el peso baja al centro:

  • Estático (la base ancha): TypeScript y ESLint. No ejecutan tu código, pero atrapan typos y errores de tipo gratis, en cada tecla.
  • Unitario: una función o un hook aislado. Barato y rápido, pero prueba poco.
  • Integración (la banda más gorda): varias piezas juntas siguiendo un flujo. Aquí va el grueso.
  • End-to-end: la app entera en un navegador real. El más realista, pero lento y frágil.

¿Por qué la integración es la banda gorda? Porque es el punto dulce entre confianza y coste. Un unitario es baratísimo pero te asegura una pieza diminuta; un e2e te asegura todo pero cuesta mantenerlo y es lento. La integración prueba el flujo real —el usuario carga, busca, pulsa, ve el resultado— a un coste razonable. Es donde más rinde cada test que escribes.

Montar la feature entera, no las piezas#

Un test de integración hace lo contrario que un unitario: en vez de aislar una pieza, monta el conjunto a propósito. Mira el demo: una sola feature, el ConstructorEquipo, con la lista del roster y un panel de equipo dentro, compartiendo estado. El test renderiza la feature entera y recorre el flujo: espera al roster de la red, ficha a dos héroes, retira a uno desde el panel y comprueba que la cuenta y la lista del equipo se actualizan en cada paso.

Fíjate en lo que no hace el test: no prueba la lista por su cuenta, ni el botón por su cuenta, ni el panel por su cuenta. Los monta todos juntos y comprueba que el recorrido completo funciona. La única frontera que simula es la red, con MSW; todo lo de dentro es real.

La costura entre piezas: lo que un unitario no ve#

El bug que caza un test de integración vive en la costura entre componentes: ese punto donde una pieza le pasa algo a otra. Y la trampa es que cada pieza, por separado, funciona.

Imagina que el botón “Quitar” del panel borra a un héroe por su posición en la lista en vez de por su identidad. El test de la lista pasa: pinta los héroes. El test del botón pasa: dispara su evento. El test del panel pasa: muestra lo que le llega. Pero en cuanto retiras a uno, las posiciones se desplazan, y el siguiente “Quitar” borra al héroe equivocado.

¿Y qué? Que ese bug llega a producción con toda la suite en verde, porque ningún test aislado lo ve: solo aparece cuando las piezas trabajan juntas y retiras a más de uno en orden. El usuario quita a “Genji”… y se le borra “Mercy”. Un test de integración que recorra añadir → quitar → comprobar el panel revienta exactamente ahí. Por eso esta banda da tanta confianza: prueba lo que de verdad rompe.

Cuánto montar y qué simular#

La regla práctica: monta la feature, simula solo la frontera. La frontera de tu sistema es lo que no controlas —casi siempre la red—, y para eso está MSW: el componente hace su fetch real y tú decides la respuesta, sin tocar su código.

Lo que no debes hacer es doblar los componentes internos. Si sustituyes la lista, o el panel, por un doble “para aislarlos”, te quedas sin lo único que querías probar: que esas piezas colaboren de verdad. El doble fingiría que la costura está bien justo donde quieres comprobar que lo está.

Snapshots: una foto del resultado#

La segunda mitad del capítulo es otra herramienta: el snapshot testing. Un snapshot es una foto de una salida (un objeto, un mensaje de error, el HTML que pinta un componente). La primera vez, el test guarda esa foto; las siguientes, compara contra ella y falla si algo cambió.

Hay dos formas. La más útil para cosas pequeñas es el snapshot inline, que vive dentro del propio test (Vitest lo escribe entre backticks por ti):

tsx
import { it, expect } from "vitest";
import { resumenAlineacion } from "./alineacion";

it("resume la alineación en una ficha estable", () => {
  // resumenAlineacion es una función PURA: mismos héroes, mismo resumen.
  const ficha = resumenAlineacion([
    { nombre: "Genji", rol: "Daño" },
    { nombre: "Reinhardt", rol: "Tanque" },
    { nombre: "Mercy", rol: "Apoyo" },
  ]);
  // La PRIMERA vez, Vitest rellena el snapshot entre los backticks por ti.
  // Las siguientes, compara; si la ficha cambia, el test falla y enseña el diff.
  expect(ficha).toMatchInlineSnapshot(`
    {
      "porRol": {
        "Apoyo": 1,
        "Daño": 1,
        "Tanque": 1,
      },
      "tamaño": 3,
    }
  `);
});

La otra forma, toMatchSnapshot(), guarda la foto en un fichero aparte (__snapshots__/…​.snap) en vez de inline. Útil cuando la salida es grande, pero tiene un coste: la foto vive en otro fichero que es fácil de no mirar al revisar un cambio.

Los snapshots se escriben así en tu proyecto con Vitest, pero dependen de ficheros (.snap) o de reescribir el propio test, y eso el playground no lo guarda entre corridas. Por eso aquí los ves como código, no como un demo ejecutable: lo importante no es correrlos, es entender cuándo usarlos.

Cuándo el snapshot ayuda y cuándo solo hace ruido#

Un snapshot ayuda cuando la salida es pequeña, estable y serializable: una función pura que devuelve un objeto, el mensaje de un Error, la configuración que genera un helper. Cambia solo cuando cambia algo de verdad, y el diff es lo bastante corto para leerlo en cada revisión.

El snapshot se vuelve ruido cuando es una foto enorme —el HTML de un componente entero—:

tsx
// El anti-patrón: una foto del componente ENTERO.
it("el panel se ve bien", () => {
  const { container } = render(<PanelFichajes />);
  // Cientos de líneas de HTML serializado en un .snap que nadie revisa.
  expect(container).toMatchSnapshot();
});

¿Y qué? Que cualquier cambio legítimo —una clase, un espacio, un texto— rompe esa foto. El equipo aprende a actualizarla a ciegas con vitest -u, sin leer el diff, porque “seguro que es el cambio de antes”. Y el día que se cuela una regresión real, también la aprueban sin mirar. Llegado ese punto, el snapshot pasa en verde aunque la UI esté rota y falla en rojo aunque esté bien: un test que miente y solo añade fricción.

Seamos honestos: un snapshot de algo pequeño y estable (un Badge, una ficha de datos) puede valer perfectamente. La línea es esta pregunta: ¿lo revisarías de verdad, entero, cada vez que cambie? Si la respuesta es no, no es un test, es ruido; cámbialo por una aserción explícita (toHaveTextContent, toBeInTheDocument) que diga qué compruebas.

Pruébalo#

Abre el demo de arriba y rompe la costura a propósito: en añadir, ignora el héroe recibido y añade siempre roster[0]. Tras pulsar “Añadir Reinhardt”, en el equipo aparecerá “Genji” (y su “Quitar Genji”): el héroe equivocado. El test del flujo se pondrá rojo —la cuenta sube, pero del que no es—, justo el tipo de bug que ningún test de pieza aislada cazaría. Devuélvelo a su sitio y vuelve a verde.

Comprueba lo que sabes#

Pregunta 1 de 6

En los capítulos anteriores probabas un componente solo, o un hook solo. ¿Qué prueba un test de INTEGRACIÓN?

Tu turno#

Prueba PanelFichajes de integración: monta la feature entera, simula la red con MSW y recorre el flujo del usuario (cargar, fichar, ver el equipo). Cuando lo tengas (o si te atascas), despliega las soluciones y fíjate en el salto de un tier al siguiente: de comprobar que carga, a recorrer el flujo y su costura, y de ahí al contrato completo con el error y el pool vacío.

Ejercicio · en esta página

Prueba PanelFichajes de integración

PanelFichajes es una feature: carga candidatos de una API y, al fichar uno, lo saca del pool y lo mete en el equipo subiendo la cuenta. No tocas el componente: escribe un test de INTEGRACIÓN que monte la feature entera, simule la red con MSW y compruebe el flujo del usuario (cargar -> fichar -> ver el equipo), no las piezas por separado.

Paso 1: La feature monta y carga

  • Montas un servidor MSW con un handler para GET de los candidatos.
  • Gestionas su ciclo de vida con beforeAll(listen), afterEach(resetHandlers) y afterAll(close).
  • Renderizas <PanelFichajes /> y compruebas con findBy que pinta los candidatos que "devolvió la API".
Ver soluciones
import { describe, it, expect, beforeAll, afterEach, afterAll } from "vitest";
import { render, screen } from "@testing-library/react";
import { http, HttpResponse } from "msw";
import { setupServer } from "msw/node";
import { PanelFichajes } from "./PanelFichajes";

// El backend de mentira: los candidatos que "devuelve la API".
const server = setupServer(
  http.get("https://api.fichajes.test/candidatos", () =>
    HttpResponse.json([
      { nombre: "Genji", rol: "Daño" },
      { nombre: "Mercy", rol: "Apoyo" },
    ]),
  ),
);

// Enciende la intercepción antes de todo, límpiala entre tests, apágala al final.
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

describe("PanelFichajes (integración)", () => {
  it("carga los candidatos y los pinta", async () => {
    // Monta la feature entera; el componente hace su fetch real y MSW responde.
    render(<PanelFichajes />);
    // findBy espera a que la red responda y la lista se pinte.
    expect(await screen.findByText("Genji")).toBeInTheDocument();
    expect(screen.getByText("Mercy")).toBeInTheDocument();
  });
});

Por qué este nivel

  • Monta la feature entera y la prueba como un todo: un servidor MSW responde la red y findBy espera a que la lista se pinte. No aísla ninguna pieza: ese es el punto de un test de integración.
  • Su límite: solo comprueba que carga. El valor de verdad —que el flujo del usuario (fichar, mover al equipo, subir la cuenta) funcione de cabo a rabo— se queda fuera.