¿Por qué React Testing Library?
Cuando escribimos pruebas para nuestros componentes de React, es fácil caer en la trampa de probar la implementación en lugar de probar el comportamiento. ¿Cuántas veces has visto tests que verifican el estado interno de un componente o acceden a métodos privados? Esto crea tests frágiles que se rompen cuando refactorizamos código, aunque la funcionalidad siga funcionando perfectamente.
La biblioteca fue creada por Kent C. Dodds como una respuesta a la pregunta: "¿Cuánto de esta prueba me da confianza en que mi aplicación funciona para mis usuarios?"
Instalación y Configuración
Para comenzar a usar React Testing Library, necesitas instalarla junto con Jest (el runner de tests más común para React):
npm install --save-dev @testing-library/react @testing-library/jest-dom
Si estás usando Create React App o Vite, probablemente ya venga configurado. Solo necesitas crear archivos de test con la extensión .test.js o .spec.js.
El Principio de las Pruebas Centradas en el Usuario
Imagina que tienes un formulario de inicio de sesión. El usuario ve:
- Un campo con la etiqueta "Correo electrónico"
- Un campo con la etiqueta "Contraseña"
- Un botón que dice "Iniciar sesión"
- Mensajes de error como "El correo electrónico es requerido"
Un test centrado en el usuario buscaría estos elementos por su texto o accesibilidad, no por selectores CSS o IDs generados por el framework.
Consultas: Encontrando Elementos
RTL proporciona diferentes queries para encontrar elementos en el DOM. Cada una tiene una prioridad recomendada:
| Query | Prioridad | Uso |
|---|---|---|
getByRole |
1ª (Accesibilidad) | Encuentra elementos por su rol ARIA |
getByLabelText |
2ª (Formularios) | Encuentra inputs por su etiqueta |
getByPlaceholderText |
3ª | Encuentra inputs por placeholder |
getByText |
4ª | Encuentra elementos por contenido de texto |
getByTestId |
Última opción | Atributo data-testid |
data-testid para todo, pero esto es precisamente lo que RTL quiere evitar. Solo úsalo cuando no hay otra forma semántica de encontrar el elemento.Ejemplo Práctico: Componente LoginForm
Vamos a crear un componente de formulario de login y sus pruebas asociadas:
// LoginForm.jsx
import { useState } from 'react';
export function LoginForm({ onSubmit }) {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
if (!email) {
setError('El correo electrónico es requerido');
return;
}
onSubmit({ email, password });
};
return (
<form onSubmit={handleSubmit}>
<div>
<label htmlFor="email">Correo electrónico</label>
<input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
</div>
<div>
<label htmlFor="password">Contraseña</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
{error && <p role="alert">{error}</p>}
<button type="submit">Iniciar sesión</button>
</form>
);
}
Las Pruebas
// LoginForm.test.jsx
import { render, screen, fireEvent } from '@testing-library/react';
import { LoginForm } from './LoginForm';
describe('LoginForm', () => {
it('debe renderizar el formulario con todos sus campos', () => {
render(<LoginForm onSubmit={jest.fn()} />);
// El usuario ve "Correo electrónico" y "Contraseña"
expect(screen.getByLabelText(/correo electrónico/i)).toBeInTheDocument();
expect(screen.getByLabelText(/contraseña/i)).toBeInTheDocument();
// El usuario ve el botón
expect(screen.getByRole('button', { name: /iniciar sesión/i })).toBeInTheDocument();
});
it('debe mostrar error cuando el email está vacío', async () => {
render(<LoginForm onSubmit={jest.fn()} />);
const user = userEvent.setup();
// Dejamos el campo de email vacío y enviamos el formulario
await user.click(screen.getByRole('button', { name: /iniciar sesión/i }));
// El usuario ve el mensaje de error
expect(screen.getByRole('alert')).toHaveTextContent('El correo electrónico es requerido');
});
it('debe llamar a onSubmit con los datos cuando el formulario es válido', async () => {
const mockSubmit = jest.fn();
render(<LoginForm onSubmit={mockSubmit} />);
const user = userEvent.setup();
// El usuario llena el formulario
await user.type(screen.getByLabelText(/correo electrónico/i), '[email protected]');
await user.type(screen.getByLabelText(/contraseña/i), 'contraseña123');
// El usuario hace clic en el botón
await user.click(screen.getByRole('button', { name: /iniciar sesión/i }));
// La función onSubmit fue llamada con los datos correctos
expect(mockSubmit).toHaveBeenCalledWith({
email: '[email protected]',
password: 'contraseña123',
});
});
});
Eventos: Simulando Interacciones
fireEvent (más directo) y userEvent de @testing-library/user-event (más realista, simula comportamiento humano real).La librería userEvent es preferible porque:
- Simula múltiples eventos que ocurrirían en un navegador real (keydown, keyup, change)
- Maneja mejor casos edge como escribir en un campo deshabilitado
- Es más cercana a como un usuario real interactúa con la interfaz
import userEvent from '@testing-library/user-event';
// Más realista que fireEvent
await userEvent.type(screen.getByRole('textbox', { name: /búsqueda/i }), 'React');
await userEvent.click(screen.getByRole('button', { name: /buscar/i }));
Pruebas de Componentes Asíncronos
Los componentes que cargan datos de forma asíncrona son comunes. RTL proporciona waitFor y queries asíncronas:
import { render, screen, waitFor } from '@testing-library/react';
import { AsyncComponent } from './AsyncComponent';
it('debe mostrar los datos cuando se cargan', async () => {
render(<AsyncComponent />);
// Al inicio, muestra loading
expect(screen.getByText(/cargando/i)).toBeInTheDocument();
// Después de cargar, muestra los datos
await waitFor(() => {
expect(screen.getByText('Datos cargados')).toBeInTheDocument();
});
});
// O más simple, usando findBy
it('debe mostrar los datos cuando se cargan (versión simple)', async () => {
render(<AsyncComponent />);
expect(await screen.findByText('Datos cargados')).toBeInTheDocument();
});
findBy son combinaciones de getBy + waitFor. Devuelven una promesa que se resuelve cuando el elemento aparece o se rechaza si no aparece.Debugging: Viendo el HTML Generado
Una de las características más útiles de RTL es la función debug():
const { container } = render(<MiComponente />);
// Imprime el HTML en la consola
console.log(container.innerHTML);
// O más bonito
screen.debug();
// Para un elemento específico
screen.debug(screen.getByRole('button'));
Matchers Personalizados
Con @testing-library/jest-dom obtienes matchers adicionales que hacen tus aserciones más legibles:
import '@testing-library/jest-dom';
// Aserciones útiles
expect(button).toBeDisabled();
expect(input).toHaveValue('texto');
expect(element).toBeInTheDocument();
expect(element).toHaveTextContent('hola mundo');
expect(input).toHaveFocus();
expect(element).toHaveAttribute('data-active', 'true');
Buenas Prácticas
- No pruebes la implementación: No accedas a estados, métodos internos o refs.
- Prioriza la accesibilidad: Si puedes encontrar algo por su rol o etiqueta, úsalo.
- Escribe tests desde la perspectiva del usuario: ¿Qué haría un usuario? ¿Qué vería?
- Mantén tus tests simples: Un test debe probar una sola cosa.
- Usa setup functions: Si tienes configuración repetitiva, usa
beforeEach.
Configuración de Jest Globals
Para que todo funcione correctamente, asegúrate de configurar Jest en tu archivo de configuración:
// jest.config.js
module.exports = {
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['@testing-library/jest-dom'],
moduleNameMapper: {
'\\.(css|less|scss|sass)$': 'identity-obj-proxy',
},
};
Ver másEl identity-obj-proxy es un mock que permite importar archivos CSS sin errores. Si usas CSS modules, puedes configurarlo así:
moduleNameMapper: {
'\\.module\\.css$': 'identity-obj-proxy',
}
Integración con Mocks
A veces necesitas simular módulos o funciones, especialmente para APIs externas:
// Mock de fetch
global.fetch = jest.fn(() =>
Promise.resolve({
json: () => Promise.resolve({ data: 'mocked data' }),
})
);
// Mock de un módulo
jest.mock('./api', () => ({
fetchUser: jest.fn(() => Promise.resolve({ name: 'Ana' })),
}));
Resumen
React Testing Library representa un cambio de paradigma en cómo escribimos pruebas para React. En lugar de obsesionarnos con el estado interno y la implementación, nos centramos en lo que el usuario realmente ve y puede hacer. Este enfoque nos da:
- Tests más robustos que no se rompen con refactors
- Mejor accesibilidad (al buscar por roles y etiquetas)
- Mayor confianza de que la aplicación funciona para los usuarios
- Tests más mantenibles a largo plazo
La mejor prueba es aquella que verifica que tu aplicación funciona correctamente, y la mejor forma de verificar esto es interactuando con ella como lo haría un usuario real.
¿Cuál es el query de mayor prioridad en React Testing Library para encontrar elementos?
- A) getByTestId
- B) getByText
- C) getByRole
- D) getByClassName