En esta página
Testing con Vitest y React Testing Library
La filosofía de React Testing Library
React Testing Library (RTL) fue creada con una filosofía clara: tus tests deben parecerse a cómo los usuarios usan tu aplicación. Esto significa:
- Buscar elementos por rol accesible, texto o label (como un usuario real)
- No acceder al estado interno del componente
- No testear detalles de implementación
npm install -D vitest @testing-library/react @testing-library/user-event @testing-library/jest-dom jsdomConfiguración de Vitest con React
// vitest.config.ts
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
test: {
environment: 'jsdom',
globals: true,
setupFiles: './src/test/setup.ts',
},
});// src/test/setup.ts
import '@testing-library/jest-dom';Queries — encontrar elementos correctamente
RTL ofrece múltiples formas de encontrar elementos, ordenadas por preferencia:
// 1. Por rol (PREFERIDO — accesible)
screen.getByRole('button', { name: /guardar/i });
screen.getByRole('textbox', { name: /correo/i });
screen.getByRole('heading', { level: 1 });
screen.getByRole('listitem');
screen.getByRole('checkbox', { name: /aceptar términos/i });
// 2. Por label (bueno para formularios)
screen.getByLabelText(/contraseña/i);
// 3. Por placeholder (aceptable)
screen.getByPlaceholderText(/buscar/i);
// 4. Por texto visible
screen.getByText(/bienvenido/i);
// 5. Por test-id (último recurso)
screen.getByTestId('contador');Las variantes get* lanzan error si no encuentran, query* retorna null, find* espera (promesa).
Testing de interacciones con userEvent
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Contador } from './Contador';
describe('Contador', () => {
it('incrementa al hacer clic en el botón', async () => {
const usuario = userEvent.setup(); // Crear instancia de usuario
render(<Contador valorInicial={5} />);
const boton = screen.getByRole('button', { name: /incrementar/i });
const display = screen.getByRole('status'); // aria-live="polite" region
expect(display).toHaveTextContent('5');
await usuario.click(boton);
expect(display).toHaveTextContent('6');
await usuario.click(boton);
await usuario.click(boton);
expect(display).toHaveTextContent('8');
});
it('teclea texto en un campo de búsqueda', async () => {
const usuario = userEvent.setup();
render(<BuscadorProductos />);
const campo = screen.getByRole('searchbox');
await usuario.type(campo, 'zapatillas');
expect(campo).toHaveValue('zapatillas');
// Verificar que los resultados aparecen
expect(await screen.findByText(/zapatillas deportivas/i)).toBeInTheDocument();
});
});Testing asíncrono
import { describe, it, expect, vi } from 'vitest';
import { render, screen, waitFor } from '@testing-library/react';
// Mock de fetch global
vi.stubGlobal('fetch', vi.fn());
describe('ListaUsuarios', () => {
it('muestra usuarios después de cargar', async () => {
// Configurar el mock de fetch
vi.mocked(fetch).mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve([
{ id: 1, nombre: 'Ana García' },
{ id: 2, nombre: 'Carlos López' },
]),
} as Response);
render(<ListaUsuarios />);
// Verificar estado de carga
expect(screen.getByText(/cargando/i)).toBeInTheDocument();
// Esperar que los datos aparezcan
await waitFor(() => {
expect(screen.getByText('Ana García')).toBeInTheDocument();
expect(screen.getByText('Carlos López')).toBeInTheDocument();
});
// Estado de carga ya no debe estar
expect(screen.queryByText(/cargando/i)).not.toBeInTheDocument();
});
it('muestra error cuando la petición falla', async () => {
vi.mocked(fetch).mockResolvedValueOnce({
ok: false,
status: 500,
} as Response);
render(<ListaUsuarios />);
await waitFor(() => {
expect(screen.getByRole('alert')).toHaveTextContent(/error/i);
});
});
});Testing de hooks personalizados
import { renderHook, act } from '@testing-library/react';
import { useToggle } from './useToggle';
describe('useToggle', () => {
it('comienza con el valor inicial', () => {
const { result } = renderHook(() => useToggle(false));
expect(result.current[0]).toBe(false);
});
it('alterna el valor al llamar toggle', () => {
const { result } = renderHook(() => useToggle(false));
const [, toggle] = result.current;
act(() => toggle());
expect(result.current[0]).toBe(true);
act(() => toggle());
expect(result.current[0]).toBe(false);
});
});Mocking de módulos con vi.mock
import { vi, describe, it, expect } from 'vitest';
// Mock de React Router
vi.mock('react-router-dom', () => ({
useNavigate: vi.fn(() => vi.fn()),
useParams: vi.fn(() => ({ id: '123' })),
Link: ({ children, to }: { children: React.ReactNode; to: string }) => (
<a href={to}>{children}</a>
),
}));
// Mock de Zustand store
vi.mock('../store/carritoStore', () => ({
useCarrito: vi.fn((selector) => selector({
items: [{ id: '1', nombre: 'Laptop', precio: 999, cantidad: 1 }],
vaciar: vi.fn(),
eliminar: vi.fn(),
totalItems: () => 1,
totalPrecio: () => 999,
})),
}));Principios de un buen test
- Un test, una responsabilidad: cada test verifica una sola cosa
- AAA: Arrange (preparar), Act (actuar), Assert (verificar)
- Nombres descriptivos:
it('muestra error cuando el email es inválido') - No depender del orden: los tests deben ser independientes entre sí
- Limpiar mocks: usa
beforeEach(() => vi.clearAllMocks()) - Preferir la integración: un test que renderiza y hace clic es más valioso que uno que testea useState directamente
Los tests bien escritos son documentación viva de tu aplicación: describen el comportamiento esperado de forma inequívoca.
Usa queries por rol accesible — son más robustas
Prefiere getByRole('button', {name: /enviar/i}) sobre getByText o getByTestId. Las queries por rol son más robustas porque reflejan cómo los usuarios y lectores de pantalla interactúan con tu app. Si tu test no puede encontrar el elemento por rol, probablemente tampoco es accesible.
userEvent vs fireEvent — usa siempre userEvent
fireEvent dispara eventos sintéticos directamente y puede no reflejar el comportamiento real del navegador. userEvent simula la secuencia completa de eventos que un usuario real generaría (mousedown, mouseup, click, focus, blur). Para tests más confiables, siempre usa userEvent.setup().
No testees los detalles de implementación
Evita tests que dependen de nombres de variables internas, estado del componente o estructura del DOM. Un buen test verifica el comportamiento observable (qué ve el usuario) no cómo está implementado. Si renombras una variable interna, el test no debería romperse.
Inicia sesión para guardar tu progreso