TUTORIAL DE PRUEBAS UNITARIAS CON REACT TESTING LIBRARY
Introducción a las Pruebas Unitarias en React
Las pruebas unitarias son un pilar fundamental en el desarrollo de software moderno, especialmente en aplicaciones web construidas con React. Garantizan que los componentes individuales de una aplicación funcionen correctamente de forma aislada, lo que facilita la detección de errores y mejora la mantenibilidad del código. En este tutorial, exploraremos cómo utilizar React Testing Library para escribir pruebas unitarias efectivas en proyectos React, con un enfoque en la simplicidad, la claridad y las mejores prácticas. React Testing Library es una herramienta poderosa que fomenta pruebas centradas en el comportamiento del usuario, en lugar de los detalles de implementación, lo que resulta en aplicaciones más robustas y fáciles de refactorizar.
Este artículo está dirigido a desarrolladores que deseen mejorar la calidad de sus aplicaciones React mediante pruebas automatizadas. Cubriremos la configuración del entorno, la escritura de pruebas para componentes, la simulación de eventos y el manejo de casos asíncronos, todo con ejemplos prácticos. Además, nos aseguraremos de que la información esté actualizada a noviembre de 2025, integrando las últimas versiones de las herramientas y prácticas recomendadas en el ecosistema React.
Configuración del Entorno de Pruebas
Antes de escribir pruebas, es necesario configurar un entorno adecuado. La mayoría de los proyectos React creados con herramientas como Create React App o Vite incluyen Jest y React Testing Library de forma predeterminada, pero si necesitas configurarlos manualmente, sigue estos pasos.
Primero, asegúrate de tener Node.js instalado (versión 18 o superior recomendada). Luego, crea un nuevo proyecto React si no tienes uno:
npx create-react-app mi-proyecto --template typescript
cd mi-proyecto
Si usas Vite, puedes iniciar un proyecto con:
npm create vite@latest mi-proyecto -- --template react-ts
cd mi-proyecto
npm install
Para instalar React Testing Library y sus dependencias, ejecuta:
npm install --save-dev @testing-library/react @testing-library/jest-dom
Jest suele estar incluido en proyectos creados con Create React App, pero si necesitas instalarlo manualmente:
npm install --save-dev jest
Configura Jest para que reconozca las extensiones de TypeScript y React Testing Library. Crea un archivo jest.config.js en la raíz del proyecto:
module.exports = {
testEnvironment: "jsdom",
setupFilesAfterEnv: ["<rootDir>/src/setupTests.ts"],
moduleNameMapper: {
"\\.(css|less|scss|sass)$": "identity-obj-proxy",
},
};
Crea un archivo src/setupTests.ts para importar las aserciones personalizadas de Jest DOM:
import "@testing-library/jest-dom";
Este archivo asegura que las funciones como toBeInTheDocument estén disponibles en todas las pruebas. La configuración anterior habilita un entorno de pruebas basado en jsdom, que simula un navegador para probar componentes React.
Estructura de un Proyecto para Pruebas
Organizar los archivos de prueba es crucial para mantener un proyecto escalable. Una práctica común es colocar los archivos de prueba junto a los componentes, usando la convención .test. o .spec. en los nombres de archivo. Por ejemplo, para un componente Button.tsx, el archivo de prueba sería Button.test.tsx.
Estructura de directorios típica:
src/
├── components/
│ ├── Button.tsx
│ ├── Button.test.tsx
├── setupTests.ts
├── App.tsx
├── index.tsx
Esta estructura facilita la localización de pruebas y mantiene el proyecto ordenado. Asegúrate de que los archivos de prueba terminen con .test.tsx o .spec.tsx para que Jest los detecte automáticamente.
Escribiendo la Primera Prueba Unitaria
Comencemos con un componente simple de React para ilustrar cómo escribir pruebas unitarias. Considera un componente Button que renderiza un botón con una etiqueta y ejecuta una función al hacer clic:
import React from "react";
interface ButtonProps {
label: string;
onClick: () => void;
}
const Button: React.FC<ButtonProps> = ({ label, onClick }) => {
return <button onClick={onClick}>{label}</button>;
};
export default Button;
Para probar este componente, crea un archivo Button.test.tsx. La primera prueba verificará que el botón se renderiza correctamente con la etiqueta proporcionada:
import { render, screen } from "@testing-library/react";
import Button from "./Button";
test("renderiza el botón con la etiqueta correcta", () => {
render(<Button label="Haz clic" onClick={() => {}} />);
const buttonElement = screen.getByText("Haz clic");
expect(buttonElement).toBeInTheDocument();
});
En este ejemplo, usamos render para montar el componente en un DOM virtual y screen.getByText para buscar el elemento con el texto “Haz clic”. La aserción toBeInTheDocument verifica que el elemento está presente en el DOM.
Ejecuta las pruebas con:
npm test
Si todo está configurado correctamente, verás un mensaje indicando que la prueba pasó.
Probando Interacciones del Usuario
Las aplicaciones React suelen incluir interacciones como clics o cambios en formularios. React Testing Library proporciona herramientas como fireEvent para simular estas interacciones. Ampliemos la prueba del componente Button para verificar que la función onClick se ejecuta al hacer clic:
import { render, screen, fireEvent } from "@testing-library/react";
import Button from "./Button";
test("llama a onClick cuando se hace clic en el botón", () => {
const handleClick = jest.fn();
render(<Button label="Haz clic" onClick={handleClick} />);
const buttonElement = screen.getByText("Haz clic");
fireEvent.click(buttonElement);
expect(handleClick).toHaveBeenCalledTimes(1);
});
Aquí, creamos una función simulada (jest.fn()) para rastrear las llamadas a onClick. Usamos fireEvent.click para simular un clic en el botón y verificamos que la función se llamó exactamente una vez con toHaveBeenCalledTimes.
Probando Componentes con Estado
Muchos componentes React manejan estado interno con el hook useState. Considera un componente Counter que incrementa un contador al hacer clic en un botón:
import React, { useState } from "react";
const Counter: React.FC = () => {
const [count, setCount] = useState(0);
return (
<div>
<p>Contador: {count}</p>
<button onClick={() => setCount(count + 1)}>Incrementar</button>
</div>
);
};
export default Counter;
Para probar este componente, verifica que el contador se renderiza correctamente y que se incrementa al hacer clic:
import { render, screen, fireEvent } from "@testing-library/react";
import Counter from "./Counter";
test("renderiza el contador inicial en 0", () => {
render(<Counter />);
const countElement = screen.getByText("Contador: 0");
expect(countElement).toBeInTheDocument();
});
test("incrementa el contador al hacer clic", () => {
render(<Counter />);
const buttonElement = screen.getByText("Incrementar");
fireEvent.click(buttonElement);
const countElement = screen.getByText("Contador: 1");
expect(countElement).toBeInTheDocument();
});
Estas pruebas aseguran que el estado inicial es correcto y que la interacción del usuario actualiza el estado como se espera. Usar pruebas unitarias efectivas en componentes con estado ayuda a detectar errores en la lógica de actualización.
Manejo de Pruebas Asíncronas
En aplicaciones modernas, los componentes suelen realizar operaciones asíncronas, como solicitudes HTTP. React Testing Library ofrece herramientas como waitFor para manejar estas situaciones. Considera un componente UserList que obtiene una lista de usuarios desde una API:
import React, { useState, useEffect } from "react";
const UserList: React.FC = () => {
const [users, setUsers] = useState<string[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
const fetchUsers = async () => {
const response = await fetch("https://api.example.com/users");
const data = await response.json();
setUsers(data);
setLoading(false);
};
fetchUsers();
}, []);
if (loading) return <p>Cargando...</p>;
return (
<ul>
{users.map((user, index) => (
<li key={index}>{user}</li>
))}
</ul>
);
};
export default UserList;
Probar este componente requiere simular la respuesta de la API. Usaremos jest.mock para mockear la función fetch:
import { render, screen, waitFor } from "@testing-library/react";
import UserList from "./UserList";
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve(["Alice", "Bob"]),
} as Response)
);
test("muestra los usuarios después de cargar", async () => {
render(<UserList />);
expect(screen.getByText("Cargando...")).toBeInTheDocument();
await waitFor(() => {
expect(screen.getByText("Alice")).toBeInTheDocument();
expect(screen.getByText("Bob")).toBeInTheDocument();
});
});
En esta prueba, simulamos una respuesta de fetch que devuelve un arreglo de usuarios. Usamos waitFor para esperar a que el componente actualice el DOM después de la operación asíncrona. Esto garantiza que las pruebas reflejen el comportamiento real del componente.
Probando Componentes con Contexto
React Context es común para gestionar estado global. Supongamos un componente ThemeToggle que usa un contexto de tema:
import React, { createContext, useContext, useState } from "react";
interface ThemeContextType {
theme: string;
toggleTheme: () => void;
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
const [theme, setTheme] = useState("light");
const toggleTheme = () => setTheme(theme === "light" ? "dark" : "light");
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
};
const ThemeToggle: React.FC = () => {
const context = useContext(ThemeContext);
if (!context)
throw new Error("ThemeToggle must be used within ThemeProvider");
const { theme, toggleTheme } = context;
return (
<button onClick={toggleTheme}>
Cambiar a {theme === "light" ? "oscuro" : "claro"}
</button>
);
};
export default ThemeToggle;
Para probar ThemeToggle, necesitamos envolverlo en ThemeProvider:
import { render, screen, fireEvent } from "@testing-library/react";
import ThemeToggle, { ThemeProvider } from "./ThemeToggle";
test("cambia el tema al hacer clic", () => {
render(
<ThemeProvider>
<ThemeToggle />
</ThemeProvider>
);
const buttonElement = screen.getByText("Cambiar a oscuro");
fireEvent.click(buttonElement);
expect(screen.getByText("Cambiar a claro")).toBeInTheDocument();
});
Esta prueba verifica que el botón cambia el texto según el estado del tema. Envolver el componente en su proveedor de contexto asegura que las pruebas reflejen el uso real.
Mejores Prácticas para Pruebas con React Testing Library
Para maximizar la efectividad de las pruebas, sigue estas mejores prácticas:
-
Centrarse en el comportamiento del usuario: Escribe pruebas que simulen cómo los usuarios interactúan con la interfaz, en lugar de probar detalles internos como estados o funciones específicas.
-
Usar selectores semánticos: Prefiere
getByText,getByRoleogetByLabelTextsobre selectores comogetByTestId, ya que reflejan mejor la experiencia del usuario. -
Evitar pruebas frágiles: No dependas de detalles de implementación que puedan cambiar, como nombres de clases o estructuras internas del DOM.
-
Mantener pruebas legibles: Usa nombres descriptivos para los casos de prueba y organiza el código en bloques claros.
-
Medir cobertura de código: Ejecuta
npm test -- --coveragepara identificar áreas del código que necesitan más pruebas.
Por ejemplo, en lugar de probar si un componente tiene una clase específica, verifica su output visible:
test("muestra un mensaje de error", () => {
render(<Form error="Campo requerido" />);
expect(screen.getByText("Campo requerido")).toBeInTheDocument();
});
Estas prácticas aseguran que las pruebas sean robustas y mantenibles a largo plazo.
Integración con Flujos de CI/CD
Incorporar pruebas unitarias en un flujo de integración continua (CI) es esencial para proyectos profesionales. Herramientas como GitHub Actions, CircleCI o Jenkins pueden ejecutar pruebas automáticamente en cada commit. Un flujo típico incluye:
- Configurar un script en
package.jsonpara ejecutar pruebas:"scripts": { "test": "jest" } - Crear un archivo de configuración para GitHub Actions (
.github/workflows/ci.yml):name: CI on: [push] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: "18" - run: npm install - run: npm test
Este flujo ejecuta las pruebas en cada push y falla si alguna prueba no pasa, asegurando que el código en producción sea confiable.
Manejo de Errores Comunes
Al escribir pruebas, es común encontrar errores. Aquí hay soluciones para problemas frecuentes:
-
Elemento no encontrado: Usa
screen.debug()para inspeccionar el DOM renderizado y verifica que el selector sea correcto. Por ejemplo:test("prueba con debug", () => { render(<Button label="Haz clic" onClick={() => {}} />); screen.debug(); }); -
Actualizaciones asíncronas no detectadas: Usa
waitForpara esperar cambios en el DOM. -
Mock de dependencias complejas: Usa
jest.mockpara simular módulos o APIs externas.
Por ejemplo, si una prueba falla porque un componente no se renderiza, inspecciona el DOM:
import { render, screen } from "@testing-library/react";
import ProblematicComponent from "./ProblematicComponent";
test("debuggea el componente", () => {
render(<ProblematicComponent />);
screen.debug();
});
Conclusiones
Las pruebas unitarias con React Testing Library son una herramienta esencial para garantizar la calidad y fiabilidad de las aplicaciones React. Al centrarse en el comportamiento del usuario, estas pruebas permiten construir componentes robustos que resisten cambios y refactorizaciones. Este tutorial ha cubierto desde la configuración del entorno hasta la escritura de pruebas para componentes con estado, interacciones, contexto y operaciones asíncronas, todo con ejemplos prácticos y mejores prácticas actualizadas a noviembre de 2025.
Adoptar un enfoque centrado en pruebas no solo mejora la calidad del código, sino que también aumenta la confianza del equipo al realizar cambios. Integrar estas pruebas en flujos de CI/CD y seguir prácticas como el uso de selectores semánticos asegura que los proyectos sean escalables y mantenibles. Con React Testing Library, los desarrolladores pueden escribir pruebas que reflejan cómo los usuarios interactúan con la aplicación, lo que resulta en un software más confiable y una mejor experiencia de usuario.