
En un proyecto React profesional, la lógica se extrae en custom hooks que encapsulan estado, efectos secundarios y llamadas a servicios. Los tests unitarios sobre esos hooks son mucho más baratos de mantener que los tests sobre componentes, porque no dependen del DOM ni del layout. Además, cuando un servicio realiza peticiones HTTP reales, se sustituye por un mock para que los tests sean deterministas y rápidos.
Testear un custom hook con renderHook
La función renderHook de Testing Library ejecuta un hook como si estuviera dentro de un componente, devuelve un objeto result con el valor actual y expone rerender y unmount para controlar el ciclo de vida.
// useContador.ts
import { useState, useCallback } from "react";
export function useContador(inicial = 0) {
const [valor, setValor] = useState(inicial);
const incrementar = useCallback(() => setValor((v) => v + 1), []);
const reiniciar = useCallback(() => setValor(inicial), [inicial]);
return { valor, incrementar, reiniciar };
}
// useContador.test.ts
import { describe, it, expect } from "vitest";
import { renderHook, act } from "@testing-library/react";
import { useContador } from "./useContador";
describe("useContador", () => {
it("inicializa con el valor por defecto", () => {
const { result } = renderHook(() => useContador());
expect(result.current.valor).toBe(0);
});
it("incrementa al llamar a incrementar", () => {
const { result } = renderHook(() => useContador(10));
act(() => {
result.current.incrementar();
});
expect(result.current.valor).toBe(11);
});
});
La función act envuelve cualquier acción que modifique el estado del hook. Sin act, React avisaría por consola de que hay actualizaciones que no se han procesado en un bloque de test, y el siguiente result.current podría estar desactualizado.
Cambiar parámetros con rerender
Cuando un hook depende de parámetros que pueden cambiar (por ejemplo, un id que viene de la URL), se usa rerender para reejecutar el hook con nuevos argumentos y verificar que reacciona correctamente.
const { result, rerender } = renderHook(
({ inicial }) => useContador(inicial),
{ initialProps: { inicial: 5 } }
);
expect(result.current.valor).toBe(5);
rerender({ inicial: 20 });
act(() => result.current.reiniciar());
expect(result.current.valor).toBe(20);
Mock de módulos con vi.mock
Cuando un hook o un componente importa un servicio que hace peticiones HTTP, se sustituye el módulo completo con vi.mock. Vitest hace hoisting de esta llamada al inicio del archivo, de modo que los imports del módulo original reciben la versión mockeada.
// services/apiUsuarios.ts
export async function obtenerUsuario(id: number) {
const res = await fetch(`/api/usuarios/${id}`);
return res.json();
}
// useUsuario.ts
import { useState, useEffect } from "react";
import { obtenerUsuario } from "./services/apiUsuarios";
interface Usuario {
id: number;
nombre: string;
}
export function useUsuario(id: number) {
const [usuario, setUsuario] = useState<Usuario | null>(null);
const [cargando, setCargando] = useState(true);
useEffect(() => {
setCargando(true);
obtenerUsuario(id).then((u) => {
setUsuario(u);
setCargando(false);
});
}, [id]);
return { usuario, cargando };
}
// useUsuario.test.ts
import { describe, it, expect, vi } from "vitest";
import { renderHook, waitFor } from "@testing-library/react";
import { useUsuario } from "./useUsuario";
import { obtenerUsuario } from "./services/apiUsuarios";
vi.mock("./services/apiUsuarios", () => ({
obtenerUsuario: vi.fn(),
}));
describe("useUsuario", () => {
it("carga el usuario desde el servicio", async () => {
vi.mocked(obtenerUsuario).mockResolvedValue({ id: 1, nombre: "María González" });
const { result } = renderHook(() => useUsuario(1));
expect(result.current.cargando).toBe(true);
await waitFor(() => expect(result.current.cargando).toBe(false));
expect(result.current.usuario).toEqual({ id: 1, nombre: "María González" });
expect(obtenerUsuario).toHaveBeenCalledWith(1);
});
});
La factoría pasada a vi.mock devuelve una versión del módulo donde cada export es un vi.fn. Con vi.mocked TypeScript reconoce el tipo original del servicio y expone los métodos propios de un spy, como mockResolvedValue o mockRejectedValue, manteniendo el autocompletado.
Separar siempre los servicios en un archivo dedicado (una carpeta
services/) facilita enormemente el mocking. Si las llamadas afetchestuvieran dispersas por los componentes, cada test tendría que interceptar la red directamente, algo mucho más frágil y lento.
Spies con vi.fn y verificación de llamadas
Para verificar que un componente invoca un callback o que un hook dispara una acción, se pasa un vi.fn() como prop y se consulta luego su historial de llamadas.
import { describe, it, expect, vi } from "vitest";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { Buscador } from "./Buscador";
it("invoca onBuscar con el término escrito", async () => {
const onBuscar = vi.fn();
const user = userEvent.setup();
render(<Buscador onBuscar={onBuscar} placeholder="Escribe" />);
await user.type(screen.getByPlaceholderText("Escribe"), "react");
expect(onBuscar).toHaveBeenCalled();
expect(onBuscar).toHaveBeenLastCalledWith("react");
});
Los matchers más útiles sobre spies son toHaveBeenCalled, toHaveBeenCalledTimes, toHaveBeenCalledWith y toHaveBeenLastCalledWith. Con ellos se verifica no solo que la función se llamó, sino con qué argumentos y en qué orden.
Esperas asíncronas con waitFor
La utilidad waitFor repite una aserción hasta que pase o hasta agotar el timeout (por defecto un segundo). Se usa cuando un cambio de estado ocurre como consecuencia de una promesa, sin que haya un elemento nuevo en el DOM que permita usar findBy.
await waitFor(() => {
expect(result.current.cargando).toBe(false);
});
Cuando lo que se espera es que aparezca un elemento en el DOM, findByText es más declarativo y más eficiente que un waitFor envolviendo un getByText. La regla es: si estás esperando a un elemento del DOM, usa findBy; si estás esperando a un cambio de estado o de valor interno, usa waitFor.
Con estos tres bloques (custom hooks con renderHook, servicios mockeados con vi.mock y esperas asíncronas) cubres prácticamente cualquier patrón de tests que un cliente B2B exija en una auditoría técnica de un proyecto React.
Alan Sastre
Ingeniero de Software y formador, CEO en CertiDevs
Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, React es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.
Más tutoriales de React
Explora más contenido relacionado con React y continúa aprendiendo con nuestros tutoriales gratuitos.
Aprendizajes de esta lección
Testear custom hooks aislados con renderHook y act. Aplicar vi.mock para sustituir módulos completos. Crear spies con vi.fn para verificar llamadas a funciones. Usar waitFor y findBy para afirmaciones asíncronas.