learning-front

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

Async y dobles de test

Esperar a que algo ocurra con findBy y waitFor, y la taxonomía de dobles —mock, stub, spy y fake— con cuándo usar cada uno: vi.fn, vi.spyOn, vi.mock para módulos y timers falsos. Simular una dependencia sin acoplarte a su implementación.

Todo lo que has probado hasta ahora era síncrono: el estado cambiaba al instante. Pero la vida real no es así: un componente pide datos y tarda, un debounce espera una pausa. Probar eso trae dos retos nuevos: esperar a que algo ocurra, y simular las dependencias que no quieres (o no puedes) llamar de verdad. Eso es este capítulo.

Esperar lo asíncrono: findBy y waitFor#

En el capítulo de componentes usaste getByRole/getByText: queries síncronas que miran la pantalla ahora. Si el dato aún no ha llegado, getBy falla al instante. Para lo que aparece después, hay otra familia: findBy, que es getBy + espera: reintenta hasta que el elemento aparece o agota su tiempo.

La regla es simple: getBy para lo que ya está (el estado de carga inicial), findBy (con await) para lo que llega más tarde. Hay también waitFor(callback), que reintenta una aserción entera hasta que pasa —útil cuando no esperas un elemento concreto, sino una condición—; findBy es, de hecho, waitFor envolviendo un getBy.

Por qué simular: los dobles de test#

El segundo reto: tu componente llama a una API, pero en un test no quieres llamar a la red real. Es lenta, puede fallar por causas ajenas (la red está caída, el servidor tarda) y no la controlas. La solución es un doble de test: un objeto falso que sustituye a la dependencia real por algo rápido, determinista y bajo tu control.

Hay una pequeña familia de dobles, y conviene distinguirlos —aunque en la práctica los nombres se mezclen—:

  • Stub: da respuestas preparadas. Te importa lo que devuelve, no cómo se le llama.
  • Spy: registra cómo se le llamó (veces, argumentos). Te importa la interacción.
  • Mock: un spy con expectativas (a menudo el término se usa para casi cualquier doble).
  • Fake: una implementación ligera pero real (el repositorio en memoria del capítulo anterior).

En Vitest, vi.fn() te da una función falsa que sirve de stub y de spy a la vez:

Fíjate en la diferencia: con toHaveBeenCalledWith lo usas de spy (compruebas la llamada); con mockReturnValue/mockResolvedValue lo usas de stub (preparas la respuesta). mockResolvedValue es el pan de cada día para fingir una API: devuelve una promesa ya resuelta, sin red.

Y para espiar un método de un objeto existente sin reemplazarlo entero está vi.spyOn:

typescript
// Envuelve api.cargar para ver cómo se llama (y, opcionalmente, cambiar lo que devuelve).
const sp = vi.spyOn(api, "cargar").mockReturnValue("falso");
expect(sp).toHaveBeenCalled();
sp.mockRestore(); // deja el método original como estaba

Controlar el tiempo: timers falsos#

¿Y el código que depende del tiempo —un debounce, un sondeo cada X segundos—? Esperar de verdad en el test sería lento y frágil. Los timers falsos congelan el reloj para que lo muevas tú:

Con vi.useFakeTimers(), los setTimeout/setInterval no corren solos: vi.advanceTimersByTime(300) avanza el reloj 300 ms de golpe, sin esperar de verdad. El test corre al instante y siempre igual. ¿El cuidado? Devolver el reloj real al terminar con vi.useRealTimers(), para que un reloj falso no se cuele en el siguiente test.

Inyectar el doble > vi.mock#

Queda una pieza. Si tu componente importa la dependencia directamente…

tsx
// En el componente: importa la función de carga directamente.
import { cargarHeroes } from "./api";

…para sustituirla en el test necesitas vi.mock, que reemplaza el módulo entero:

tsx
// Conceptual (vi.mock no corre en este playground; en tu proyecto, sí):
vi.mock("./api", () => ({
  cargarHeroes: vi.fn().mockResolvedValue([{ nombre: "Genji" }]),
}));

Funciona, pero hay una alternativa más limpia que ya viste en el capítulo de arquitectura: inyectar la dependencia. Si el componente la recibe por props en vez de importarla, el test solo tiene que pasarle el doble, sin vi.mock:

tsx
// El componente recibe cargarHeroes por props (inyección).
render(<ListaHeroes cargarHeroes={vi.fn().mockResolvedValue([{ nombre: "Genji" }])} />);

¿Y por qué importa la diferencia? Porque vi.mock te ata al camino del módulo ("./api") y a detalles de cómo está cableado el código; la inyección, no. Diseñar para inyectar dependencias hace el código desacoplado y los tests simples. vi.mock queda para cuando no puedes cambiar el código y no hay forma de inyectar: una salida, no la primera opción.

Y un último principio, transversal a todo esto: un doble sustituye al contrato de la dependencia, no a sus tripas. Si tu test imita demasiados detalles internos de lo que simula, se vuelve frágil — se rompe cuando cambias la implementación aunque el comportamiento siga bien—. Simula lo justo: la forma de la respuesta, no el cómo.

Pruébalo#

En el playground de dobles de arriba, añade un test que use vi.fn() como spy para comprobar que notificarAltas llama a enviar en orden: usa toHaveBeenCalledWith dos veces, o investiga enviar.mock.calls (el registro de todas las llamadas). Verás que el doble no solo finge: te deja inspeccionar cómo lo usaron.

Comprueba lo que sabes#

Pregunta 1 de 5

Tu componente carga datos y, justo tras render(), el dato aún no está. ¿Qué query usas para comprobarlo cuando llegue?

Tu turno#

Prueba ListaHeroes pasándole un doble de cargarHeroes. Espera a lo asíncrono con findBy, comprueba el estado de carga y usa el doble como spy. Cuando lo tengas (o si te atascas), despliega las soluciones y fíjate en el salto de un tier al siguiente.

Ejercicio · en esta página

Prueba ListaHeroes: async + dobles

Tienes ListaHeroes, un componente que carga héroes de forma asíncrona y recibe la función de carga por props (cargarHeroes). No tocas el componente: pruébalo pasándole un doble (vi.fn) en vez de la red real. Espera a lo asíncrono con findBy, comprueba el estado de carga, y usa el doble como spy para verificar cómo se llamó.

Paso 1: El caso feliz asíncrono

  • Inyectas un stub (vi.fn().mockResolvedValue) que resuelve con héroes de ejemplo.
  • Esperas con findByText a que los héroes aparezcan y lo compruebas.
Ver soluciones
// Tier OK: el caso feliz asíncrono. Un stub resuelve los datos y findBy espera a que se pinten.
import { describe, it, expect, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import { ListaHeroes, type Heroe } from "./ListaHeroes";

const heroes: Heroe[] = [
  { nombre: "Genji", rol: "Daño" },
  { nombre: "Mercy", rol: "Apoyo" },
];

describe("ListaHeroes", () => {
  it("muestra los héroes que llegan", async () => {
    // STUB: una función falsa que resuelve con datos preparados, sin tocar la red.
    const cargarHeroes = vi.fn().mockResolvedValue(heroes);
    render(<ListaHeroes cargarHeroes={cargarHeroes} />);
    // findByText es asíncrono: espera a que el dato llegue y se pinte (no falla al instante).
    expect(await screen.findByText("Genji")).toBeInTheDocument();
    // Una vez cargado, el resto ya está; getByText vale (síncrono).
    expect(screen.getByText("Mercy")).toBeInTheDocument();
  });
});

Por qué este nivel

  • Inyecta un STUB de cargarHeroes (vi.fn().mockResolvedValue): el test controla la respuesta sin red ni esperas reales. Y usa findByText, que espera a que el dato asíncrono llegue y se pinte.
  • Su límite: no comprueba el estado de CARGA inicial ni que la dependencia se llamó. Solo el final feliz.