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:
// 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 estabaControlar 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…
// 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:
// 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:
// 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.
Paso 2: La transición y el spy
- Compruebas el "Cargando héroes…" inicial con getByText (síncrono) y luego los héroes con findByText.
- Usas el doble como spy: compruebas que cargarHeroes se llamó (toHaveBeenCalledTimes).
Paso 3: El contrato completo, incluido el caso vacío
- Cubres carga, datos y la llamada a la dependencia en un test claro.
- Añades el caso de respuesta VACÍA (mockResolvedValue([])): el componente avisa y no se queda colgado en "Cargando…".
- Todo con el doble inyectado: sin red, determinista, y sin atarte a cómo carga el componente por dentro.
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.
// Tier mejor: la transición carga → datos, y el doble usado como SPY (cómo se llamó).
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 'Cargando…' y luego los héroes", async () => {
const cargarHeroes = vi.fn().mockResolvedValue(heroes);
render(<ListaHeroes cargarHeroes={cargarHeroes} />);
// Justo tras render, aún no hay datos: el cargando se ve (getBy, SÍNCRONO, sin esperar).
expect(screen.getByText("Cargando héroes…")).toBeInTheDocument();
// Luego llegan: findBy ESPERA a que la transición ocurra.
expect(await screen.findByText("Genji")).toBeInTheDocument();
});
it("pide los héroes una sola vez", async () => {
const cargarHeroes = vi.fn().mockResolvedValue(heroes);
render(<ListaHeroes cargarHeroes={cargarHeroes} />);
// Esperamos a que termine de cargar antes de afirmar sobre la dependencia.
await screen.findByText("Genji");
// SPY: afirmamos sobre CÓMO se usó la dependencia, no solo sobre el resultado.
expect(cargarHeroes).toHaveBeenCalledTimes(1);
});
}); Por qué es mejor que el anterior
- Cubre la transición carga → datos: el "Cargando…" con getBy (síncrono, antes de esperar) y los héroes con findBy (espera). Esa es la diferencia clave entre getBy y findBy.
- El segundo test usa el doble como SPY: toHaveBeenCalledTimes(1) afirma sobre CÓMO se usó la dependencia (se pidió una vez, no en bucle), algo que el resultado por sí solo no garantiza.
// Tier excelente: el contrato completo por su comportamiento (carga, datos, llamada, VACÍO),
// todo con un doble inyectado: determinista, sin red, sin acoplarse a cómo carga por dentro.
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 'Cargando…', luego la lista, y pide los datos una vez", async () => {
const cargarHeroes = vi.fn().mockResolvedValue(heroes);
render(<ListaHeroes cargarHeroes={cargarHeroes} />);
// Estado de carga (síncrono, antes de esperar).
expect(screen.getByText("Cargando héroes…")).toBeInTheDocument();
// Estado cargado (findBy espera la transición).
expect(await screen.findByText("Genji")).toBeInTheDocument();
expect(screen.getByText("Mercy")).toBeInTheDocument();
// La dependencia se usó según su contrato: una sola vez.
expect(cargarHeroes).toHaveBeenCalledTimes(1);
});
it("avisa cuando no hay héroes (lista vacía)", async () => {
// STUB que resuelve VACÍO: el componente no debe quedarse colgado en 'Cargando…'.
const cargarHeroes = vi.fn().mockResolvedValue([]);
render(<ListaHeroes cargarHeroes={cargarHeroes} />);
expect(await screen.findByText("No hay héroes.")).toBeInTheDocument();
});
}); Por qué es mejor que el anterior
- Prueba el contrato completo por su comportamiento: carga, datos, la dependencia llamada una vez, y el caso VACÍO —que un test del caso feliz no ve: el componente podría quedarse colgado en "Cargando…" para siempre—.
- Todo con un doble inyectado: sin red, sin esperas reales, determinista. No se acopla a CÓMO carga el componente por dentro, solo a su contrato (recibe una promesa de héroes y los pinta): si cambias el fetch por otra cosa, el test sigue valiendo.