Skip to content

15. Testing en React

BeneficioDescripción
ConfianzaCambios sin miedo a romper funcionalidad
DocumentaciónTests describen el comportamiento esperado
RefactoringModificar código con seguridad
BugsDetectar errores antes de producción
TipoQué pruebaHerramientas
UnitariosFunciones/componentes aisladosJest, Vitest
IntegraciónInteracción entre componentesReact Testing Library
E2EFlujo completo del usuarioCypress, Playwright

Instalar dependencias
npm install -D vitest @testing-library/react @testing-library/jest-dom jsdom
vite.config.js
// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
test: {
globals: true,
environment: 'jsdom',
setupFiles: './src/test/setup.js',
},
});
setup.js
// src/test/setup.js
import '@testing-library/jest-dom';
Scripts de test
// package.json
{
"scripts": {
"test": "vitest",
"test:ui": "vitest --ui",
"test:coverage": "vitest --coverage"
}
}

Componente a testear
// src/components/Saludo.jsx
function Saludo({ nombre }) {
return <h1>Hola, {nombre}!</h1>;
}
export default Saludo;
Saludo.test.jsx
// src/components/Saludo.test.jsx
import { render, screen } from '@testing-library/react';
import { describe, it, expect } from 'vitest';
import Saludo from './Saludo';
describe('Saludo', () => {
it('muestra el nombre correctamente', () => {
// Arrange: preparar
render(<Saludo nombre="Juan" />);
// Act: actuar (en este caso, solo renderizar)
// Assert: verificar
expect(screen.getByText('Hola, Juan!')).toBeInTheDocument();
});
it('muestra diferentes nombres', () => {
render(<Saludo nombre="María" />);
expect(screen.getByText('Hola, María!')).toBeInTheDocument();
});
});
Comandos
# Ejecutar todos los tests
npm test
# Modo watch (re-ejecuta al guardar)
npm test -- --watch
# Ver cobertura
npm run test:coverage

QueryUsoFalla si no encuentra
getByElemento existe✅ Sí
queryByElemento puede no existir❌ No (retorna null)
findByElemento aparece async✅ Sí (Promise)
Selectores
import { render, screen } from '@testing-library/react';
function MiComponente() {
return (
<div>
<h1>Título</h1>
<button>Enviar</button>
<input placeholder="Email" />
<img alt="Logo" src="/logo.png" />
<p data-testid="mensaje">Hola mundo</p>
</div>
);
}
// Tests
render(<MiComponente />);
// Por texto
screen.getByText('Título');
screen.getByText(/título/i); // Regex, case insensitive
// Por rol (accesibilidad)
screen.getByRole('button', { name: 'Enviar' });
screen.getByRole('heading', { level: 1 });
// Por placeholder
screen.getByPlaceholderText('Email');
// Por alt text
screen.getByAltText('Logo');
// Por test-id (último recurso)
screen.getByTestId('mensaje');
// Query (no falla si no existe)
const boton = screen.queryByText('No existe');
expect(boton).toBeNull();

Instalar userEvent
npm install -D @testing-library/user-event
Click events
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, it, expect, vi } from 'vitest';
// Componente
function Contador() {
const [count, setCount] = useState(0);
return (
<div>
<p>Contador: {count}</p>
<button onClick={() => setCount(c => c + 1)}>Incrementar</button>
</div>
);
}
// Test
describe('Contador', () => {
it('incrementa al hacer click', async () => {
const user = userEvent.setup();
render(<Contador />);
// Verificar estado inicial
expect(screen.getByText('Contador: 0')).toBeInTheDocument();
// Simular click
await user.click(screen.getByRole('button', { name: 'Incrementar' }));
// Verificar cambio
expect(screen.getByText('Contador: 1')).toBeInTheDocument();
});
});
Test de formulario
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
function Formulario({ onSubmit }) {
const [email, setEmail] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
onSubmit({ email });
};
return (
<form onSubmit={handleSubmit}>
<input
type="email"
placeholder="Email"
value={email}
onChange={e => setEmail(e.target.value)}
/>
<button type="submit">Enviar</button>
</form>
);
}
describe('Formulario', () => {
it('envía los datos correctamente', async () => {
const user = userEvent.setup();
const handleSubmit = vi.fn(); // Mock function
render(<Formulario onSubmit={handleSubmit} />);
// Escribir en input
await user.type(
screen.getByPlaceholderText('Email'),
'test@example.com'
);
// Enviar formulario
await user.click(screen.getByRole('button', { name: 'Enviar' }));
// Verificar que se llamó con los datos correctos
expect(handleSubmit).toHaveBeenCalledWith({
email: 'test@example.com'
});
});
});

Test asíncrono
import { render, screen } from '@testing-library/react';
function ListaUsuarios() {
const [usuarios, setUsuarios] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch('/api/usuarios')
.then(res => res.json())
.then(data => {
setUsuarios(data);
setLoading(false);
});
}, []);
if (loading) return <p>Cargando...</p>;
return (
<ul>
{usuarios.map(u => <li key={u.id}>{u.nombre}</li>)}
</ul>
);
}
describe('ListaUsuarios', () => {
it('muestra usuarios después de cargar', async () => {
// Mock del fetch
global.fetch = vi.fn(() =>
Promise.resolve({
json: () => Promise.resolve([
{ id: 1, nombre: 'Juan' },
{ id: 2, nombre: 'María' }
])
})
);
render(<ListaUsuarios />);
// Verificar loading
expect(screen.getByText('Cargando...')).toBeInTheDocument();
// Esperar a que aparezcan los usuarios (findBy es async)
expect(await screen.findByText('Juan')).toBeInTheDocument();
expect(screen.getByText('María')).toBeInTheDocument();
// Verificar que loading desapareció
expect(screen.queryByText('Cargando...')).not.toBeInTheDocument();
});
});
waitFor
import { render, screen, waitFor } from '@testing-library/react';
it('actualiza después de un delay', async () => {
render(<ComponenteConDelay />);
// Esperar hasta que se cumpla la condición
await waitFor(() => {
expect(screen.getByText('Completado')).toBeInTheDocument();
}, { timeout: 3000 });
});

Mock de funciones
import { vi } from 'vitest';
// Crear mock
const mockFn = vi.fn();
// Mock con valor de retorno
const mockConRetorno = vi.fn().mockReturnValue(42);
// Mock con implementación
const mockConImpl = vi.fn((x) => x * 2);
// Verificaciones
expect(mockFn).toHaveBeenCalled();
expect(mockFn).toHaveBeenCalledTimes(2);
expect(mockFn).toHaveBeenCalledWith('argumento');
Mock de módulos
// Mock de un módulo completo
vi.mock('./api', () => ({
fetchUsuarios: vi.fn(() => Promise.resolve([
{ id: 1, nombre: 'Test' }
]))
}));
// Mock de fetch global
global.fetch = vi.fn();
beforeEach(() => {
fetch.mockResolvedValue({
ok: true,
json: () => Promise.resolve({ data: 'test' })
});
});
afterEach(() => {
vi.clearAllMocks();
});

Qué testear
// ✅ TESTEAR:
// - Renderizado correcto con diferentes props
// - Interacciones del usuario (clicks, inputs)
// - Estados condicionales (loading, error, vacío)
// - Llamadas a funciones callback
// - Integración con APIs
// ❌ NO TESTEAR:
// - Detalles de implementación
// - Estilos CSS
// - Librerías de terceros
// - Código que no escribiste
Estructura recomendada
describe('NombreComponente', () => {
// Setup común
beforeEach(() => {
// Preparar mocks, etc.
});
afterEach(() => {
vi.clearAllMocks();
});
describe('renderizado', () => {
it('muestra el título', () => {});
it('muestra el contenido inicial', () => {});
});
describe('interacciones', () => {
it('responde al click', async () => {});
it('actualiza al escribir', async () => {});
});
describe('estados', () => {
it('muestra loading mientras carga', () => {});
it('muestra error si falla', () => {});
});
});
Test completo
// TodoList.test.jsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { describe, it, expect, vi } from 'vitest';
import TodoList from './TodoList';
describe('TodoList', () => {
it('agrega una nueva tarea', async () => {
const user = userEvent.setup();
render(<TodoList />);
// Escribir tarea
const input = screen.getByPlaceholderText('Nueva tarea');
await user.type(input, 'Aprender testing');
// Agregar
await user.click(screen.getByRole('button', { name: 'Agregar' }));
// Verificar que aparece
expect(screen.getByText('Aprender testing')).toBeInTheDocument();
// Verificar que input se limpió
expect(input).toHaveValue('');
});
it('marca tarea como completada', async () => {
const user = userEvent.setup();
render(<TodoList initialTodos={[{ id: 1, text: 'Test', done: false }]} />);
const checkbox = screen.getByRole('checkbox');
await user.click(checkbox);
expect(checkbox).toBeChecked();
});
it('elimina una tarea', async () => {
const user = userEvent.setup();
render(<TodoList initialTodos={[{ id: 1, text: 'Test', done: false }]} />);
await user.click(screen.getByRole('button', { name: 'Eliminar' }));
expect(screen.queryByText('Test')).not.toBeInTheDocument();
});
});

🐝