🎯 Objetivo del tema
El administrador aún no ha insertado un video para esta sección.
🗺️ Mapa del tema
El testing es el SUPERPODER del developer profesional. Te permite cambiar código con confianza: 'estoy rompiendo algo?'. Si tienes tests, lo sabes en 10 segundos. Si no, te enteras cuando el usuario final se queja en producción. En este tema vas a aprender Jest (el test runner #1 de JS), React Testing Library (la librería estándar para testear componentes como los usaría un usuario), mocks, coverage, y el flujo de trabajo de TDD. Al final vas a tener una suite de tests para tu Atlas Mundial con cobertura >70%.
Qué tipo de test escribir en cada nivel y por qué.
Test suites, assertions, beforeEach, mocks, spies.
render, screen.getByRole, fireEvent, userEvent.
jest.mock, MSW (Mock Service Worker), fetch mocks.
jest --coverage, umbrales, qué cubre y qué no.
El flujo Red → Green → Refactor que usan los profesionales.
3.8.1 La pirámide del testing: unit, integration, e2e
Antes de escribir tu primer test, necesitas entender la pirámide del testing. Es un modelo mental creado por Martin Fowler que te dice QUÉ tipo de test escribir y CUÁNTOS de cada uno. La regla: muchos tests unitarios (rápidos, baratos), algunos tests de integración (más lentos, más completos), y pocos tests E2E (lentos, frágiles, pero necesarios). Es el balance que mantiene tu app rápida de testear y cobertura alta.
| Nivel | Qué testea | Velocidad | Cantidad | Herramientas |
|---|---|---|---|---|
| Unit (unitarios) | Una función o componente aislado, sin dependencias externas. | Muy rápida (ms). | 70-80% de la suite. | Jest, Vitest. |
| Integration (integración) | Varios componentes juntos, o componente + API mockeada. | Media (segundos). | 15-25% de la suite. | Jest + RTL + MSW. |
| E2E (end-to-end) | Flujo completo del usuario en navegador real. | Lenta (minutos). | 5-10% de la suite. | Playwright, Cypress. |
El administrador aún no ha insertado un video para esta sección.
3.8.2 Jest: el framework de testing de JavaScript
Jest es el framework de testing creado por Facebook (Meta) y es el estándar de la industria para JavaScript. Vite lo incluye automáticamente, solo ejecutas npm test. Jest te da: assertions (expect), mocks (jest.mock), spies (jest.spyOn), coverage (--coverage), y watch mode (corre solo los tests que cambiaron). En este bloque vas a aprender la API esencial de Jest: describe, it/test, expect, beforeEach, afterEach.
Anatomía de un test con Jest
// tests/sumar.test.js
// Convención: nombreDelArchivo.test.js o .spec.js
import { sumar, dividir } from '../utils/math';
// describe agrupa tests relacionados (opcional pero recomendado)
describe('utils/math', () => {
// it() o test() definen UN caso de prueba
it('sumar(2, 3) devuelve 5', () => {
// expect es la assertion: 'espero que sumar(2,3) sea 5'
expect(sumar(2, 3)).toBe(5);
});
it('sumar(0, 0) devuelve 0', () => {
expect(sumar(0, 0)).toBe(0);
});
it('sumar(-1, 1) devuelve 0', () => {
expect(sumar(-1, 1)).toBe(0);
});
// Test de un caso de error con toThrow
it('dividir por 0 lanza error', () => {
// Para funciones que lanzan error, envuelve en arrow function
expect(() => dividir(10, 0)).toThrow('No se puede dividir por 0');
});
// beforeEach: se ejecuta ANTES de cada test (útil para setup)
let lista;
beforeEach(() => {
lista = [1, 2, 3, 4, 5];
});
it('lista tiene 5 elementos', () => {
expect(lista).toHaveLength(5);
});
it('lista incluye el 3', () => {
expect(lista).toContain(3);
});
});Test suite con describe, it, expect, beforeEach
Matchers más usados de Jest
// Matchers de igualdad y comparación
expect(2 + 2).toBe(4); // === (estricto)
expect({ a: 1 }).toEqual({ a: 1 }); // igualdad profunda (objetos)
expect(0.1 + 0.2).toBeCloseTo(0.3); // números flotantes
// Matchers de verdad
expect(true).toBeTruthy();
expect(0).toBeFalsy();
expect(null).toBeNull();
expect(undefined).toBeUndefined();
// Matchers de números
expect(10).toBeGreaterThan(5);
expect(10).toBeLessThanOrEqual(10);
// Matchers de strings
expect('Hola Mundo').toMatch(/mundo/i);
expect('Hola Mundo').toContain('Mundo');
// Matchers de arrays e iterables
expect(['a', 'b']).toContain('a');
expect([{a:1}]).toContainEqual({a:1});
expect(new Set([1,2])).toContain(1);
// Matchers de excepciones
expect(() => fn()).toThrow();
expect(() => fn()).toThrow('Error específico');
expect(() => fn()).toThrow(/regex/);
// Negación con .not
expect(sumar(2, 2)).not.toBe(5);
expect(lista).not.toContain(99);Los 20 matchers de Jest que usarás el 95% del tiempo
El administrador aún no ha insertado un video para esta sección.
3.8.3 React Testing Library: testear como el usuario
React Testing Library (RTL) es la librería estándar para testear componentes React. Su filosofía: testear como el usuario usaría el componente, NO como está implementado internamente. En vez de buscar por clase CSS o ID (que son detalles de implementación), busca por ROL, TEXTO, o LABEL (que es lo que el usuario ve y usa). Esto hace que tus tests no se rompan cuando refactorizas el código interno pero la UI sigue igual.
Queries de RTL: cómo encontrar elementos
// tests/Button.test.jsx
import { render, screen, fireEvent } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Button } from '../atoms/Button';
describe('<Button />', () => {
it('renderiza el texto del children', () => {
render(<Button>Click me</Button>);
// getByText: busca un elemento con ESE texto
expect(screen.getByText('Click me')).toBeInTheDocument();
});
it('llama onClick cuando se hace click', async () => {
const handleClick = vi.fn(); // mock function de Vitest
const user = userEvent.setup();
render(<Button onClick={handleClick}>Click me</Button>);
// getByRole: busca por rol ARIA (button, link, heading, etc.)
const button = screen.getByRole('button', { name: /click me/i });
await user.click(button);
expect(handleClick).toHaveBeenCalledTimes(1);
});
it('NO llama onClick cuando está disabled', async () => {
const handleClick = vi.fn();
const user = userEvent.setup();
render(<Button disabled onClick={handleClick}>Click</Button>);
const button = screen.getByRole('button', { name: /click/i });
await user.click(button);
expect(handleClick).not.toHaveBeenCalled();
});
it('muestra el spinner cuando loading es true', () => {
render(<Button loading>Guardar</Button>);
// getByLabelText: busca input por su label asociado
// getByRole('status'): busca elemento con role='status'
expect(screen.getByRole('status')).toBeInTheDocument();
});
});Tests de Button con React Testing Library
Las queries de RTL: orden de prioridad
// Las queries tienen un ORDEN DE PRIORIDAD recomendado por la doc oficial:
// 1. getByRole: la MEJOR opción (accesibilidad + como lo ve el usuario)
const button = screen.getByRole('button', { name: /enviar/i });
// 2. getByLabelText: para inputs con label
const input = screen.getByLabelText(/email/i);
// 3. getByPlaceholderText: solo si no hay label
const input2 = screen.getByPlaceholderText(/buscar/i);
// 4. getByText: para texto no interactivo (párrafos, headings)
const heading = screen.getByText(/bienvenido/i);
// 5. getByDisplayValue: para inputs con valor
const input3 = screen.getByDisplayValue('Juan');
// 6. getByAltText: para imágenes
const logo = screen.getByAltText(/logo de la empresa/i);
// 7. getByTitle: para elementos con title attribute
const tooltip = screen.getByTitle('Más información');
// 8. getByTestId: ÚLTIMO RECURSO. Es un escape hatch.
// data-testid solo se usa cuando ninguna otra query funciona.
const weird = screen.getByTestId('custom-chart');
// Variantes:
// getBy: retorna el elemento o falla el test (usar cuando DEBE existir)
// queryBy: retorna null si no existe (usar cuando queremos verificar AUSENCIA)
// findBy: retorna una Promise (usar para elementos que aparecen asíncronamente)
// Ejemplo de findBy (esperar a que aparezca un elemento async):
it('muestra los datos después de cargar', async () => {
render(<PaisDetalle id='ARG' />);
// Espera HASTA 1000ms a que aparezca el elemento
const nombre = await screen.findByText('Argentina', {}, { timeout: 1000 });
expect(nombre).toBeInTheDocument();
});Queries de RTL: prioridad y cuándo usar cada una
El administrador aún no ha insertado un video para esta sección.
3.8.4 Mocks: simular APIs y módulos externos
Cuando testeas un componente que hace fetch, no quieres hacer una petición real a la API: sería lento, frágil (si la API está caída, falla el test), y no es determinista. La solución: MOCKEAR la API. Hay 2 formas: (1) jest.mock para módulos completos, (2) MSW (Mock Service Worker) para interceptar fetch a nivel de red. La #1 es más simple; la #2 es más realista. En este bloque aprendes ambas.
Mock simple con vi.mock (Vitest)
// tests/PaisCard.test.jsx
import { render, screen, waitFor } from '@testing-library/react';
import { PaisCard } from '../organisms/PaisCard';
// Mockear el módulo useFetch con un mock simple
vi.mock('../hooks/useFetch', () => ({
useFetch: vi.fn(),
}));
import { useFetch } from '../hooks/useFetch';
describe('<PaisCard />', () => {
it('muestra loading mientras carga', () => {
// useFetch devuelve { loading: true } antes de resolver
useFetch.mockReturnValue({ data: null, loading: true, error: null });
render(<PaisCard idPais='ARG' />);
expect(screen.getByText(/cargando/i)).toBeInTheDocument();
});
it('muestra el nombre del país cuando carga', async () => {
// useFetch devuelve los datos mockeados
useFetch.mockReturnValue({
data: { name: { common: 'Argentina' }, capital: ['Buenos Aires'] },
loading: false,
error: null,
});
render(<PaisCard idPais='ARG' />);
expect(screen.getByRole('heading', { name: /argentina/i })).toBeInTheDocument();
expect(screen.getByText(/buenos aires/i)).toBeInTheDocument();
});
it('muestra error si la API falla', () => {
useFetch.mockReturnValue({
data: null,
loading: false,
error: new Error('Network error'),
});
render(<PaisCard idPais='ARG' />);
expect(screen.getByText(/error/i)).toBeInTheDocument();
});
});Mockear useFetch con vi.mock
Mock realista con MSW (Mock Service Worker)
// mocks/handlers.js: definir las respuestas mockeadas
import { http, HttpResponse } from 'msw';
export const handlers = [
// Mockear GET https://restcountries.com/v3.1/alpha/ARG
http.get('https://restcountries.com/v3.1/alpha/:id', ({ params }) => {
if (params.id === 'ARG') {
return HttpResponse.json([{
name: { common: 'Argentina' },
capital: ['Buenos Aires'],
population: 45000000,
flag: 'https://flagcdn.com/ar.png',
}]);
}
return new HttpResponse(null, { status: 404 });
}),
// Mockear GET https://api.open-meteo.com/v1/forecast
http.get('https://api.open-meteo.com/v1/forecast', () => {
return HttpResponse.json({
current_weather: { temperature: 22, windspeed: 10 },
});
}),
];
// mocks/server.js: configurar el servidor de MSW
import { setupServer } from 'msw/node';
import { handlers } from './handlers';
export const server = setupServer(...handlers);
// tests/setup.js: setup global para Vitest
import { server } from '../mocks/server';
beforeAll(() => server.listen()); // arrancar MSW antes de todos los tests
afterEach(() => server.resetHandlers()); // reset después de cada test
afterAll(() => server.close()); // cerrar al final
// tests/PaisDetalle.test.jsx: ahora el fetch REAL funciona (mockeado por MSW)
import { render, screen, waitFor } from '@testing-library/react';
import { PaisDetalle } from '../pages/PaisDetalle';
it('carga y muestra datos de Argentina', async () => {
render(<PaisDetalle id='ARG' />);
// findByText espera ASÍNCRONAMENTE a que aparezca
const titulo = await screen.findByRole('heading', { name: /argentina/i });
expect(titulo).toBeInTheDocument();
});MSW: interceptar fetch a nivel de red (más realista)
El administrador aún no ha insertado un video para esta sección.
3.8.5 Coverage: medir qué % del código está testeado
El coverage (cobertura) es el % de líneas, funciones y branches de tu código que se EJECUTAN durante los tests. No es la métrica perfecta (puedes tener 100% coverage y 0% de tests útiles), pero es un buen termómetro. La regla profesional: mínimo 70% de coverage. Si tienes menos, probablemente hay código que no has pensado bien. Si tienes 100%, no te confíes: coverage alto != tests útiles.
Cómo generar y leer el reporte de coverage
// vitest.config.js: configurar coverage
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['./tests/setup.js'],
coverage: {
provider: 'v8',
reporter: ['text', 'html', 'lcov'], // 3 formatos de salida
include: ['src/**/*.{js,jsx}'], // qué analizar
exclude: ['src/**/*.test.{js,jsx}', // excluir los tests
'src/main.jsx', // excluir entry point
'src/**/*.stories.jsx'], // excluir stories de Storybook
thresholds: { // umbrales mínimos (falla si no los cumples)
lines: 70,
functions: 70,
branches: 65,
statements: 70,
},
},
},
});
// package.json: scripts
{
"scripts": {
"test": "vitest",
"test:ui": "vitest --ui", // interfaz gráfica de tests
"test:coverage": "vitest --coverage", // genera reporte
"test:watch": "vitest --watch" // watch mode
}
}
// SALIDA del comando npm run test:coverage:
// ----------|---------|----------|---------|---------|-------------------
// File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
// ----------|---------|----------|---------|---------|-------------------
// All files | 78.4 | 72.1 | 80.0 | 78.4 |
// atoms/ | 95.2 | 88.9 | 100.0 | 95.2 |
// Button | 95.2 | 88.9 | 100.0 | 95.2 | 23-25
// hooks/ | 82.1 | 75.0 | 85.7 | 82.1 |
// useFetch| 82.1 | 75.0 | 85.7 | 82.1 | 45,67
// pages/ | 45.2 | 33.3 | 50.0 | 45.2 | (muchas)
// ----------|---------|----------|---------|---------|-------------------Configurar coverage en Vitest con umbrales
El administrador aún no ha insertado un video para esta sección.
3.8.6 TDD: escribir el test ANTES del código
TDD (Test-Driven Development) es la metodología donde escribes el TEST primero, luego el código para que pase, luego refactorizas. Es contraintuitivo (¿cómo testear algo que no existe?), pero es la forma más rápida de escribir código de calidad. El flujo se llama Red-Green-Refactor: (1) Red: test falla porque el código no existe. (2) Green: escribes el código mínimo para que pase. (3) Refactor: mejoras el código sin romper el test. Cada ciclo es de 5-15 minutos. Al final tienes código + tests juntos.
Ejemplo de TDD: función validarEmail
// PASO 1 (RED): Escribir el test ANTES del código
// tests/validarEmail.test.js
import { validarEmail } from '../utils/validar';
describe('validarEmail', () => {
it('rechaza email sin @', () => {
expect(validarEmail('usuario.example.com')).toBe(false);
});
it('acepta email válido', () => {
expect(validarEmail('usuario@example.com')).toBe(true);
});
it('rechaza string vacío', () => {
expect(validarEmail('')).toBe(false);
});
it('rechaza email sin dominio', () => {
expect(validarEmail('usuario@')).toBe(false);
});
});
// Ejecutar: npm test --> TODOS FALLAN (RED) porque validarEmail no existe
// PASO 2 (GREEN): Escribir el código MÍNIMO para que los tests pasen
// utils/validar.js
export function validarEmail(email) {
if (!email) return false;
if (!email.includes('@')) return false;
if (email.endsWith('@')) return false;
return true; // versión simple, suficiente para pasar los tests
}
// Ejecutar: npm test --> TODOS PASAN (GREEN)
// PASO 3 (REFACTOR): Mejorar el código sin romper los tests
// utils/validar.js (versión mejorada con regex robusto)
export function validarEmail(email) {
if (typeof email !== 'string') return false;
if (email.length < 5) return false;
// Regex: algo@algo.algo (versión simplificada)
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return regex.test(email);
}
// Ejecutar: npm test --> SIGUEN PASANDO (los tests no se rompieron)
// Y ahora el código es más robusto.
// CICLO TDD:
// RED → GREEN → REFACTOR → (siguiente test) → RED → ...
// 5min 5min 10min 5min 5minTDD en acción: 3 pasos para validarEmail
El administrador aún no ha insertado un video para esta sección.
📚 Contenido ampliado
Material adicional, referencias externas verificadas y ejemplos extendidos.
🤖 AI Mission
La IA no escribe tus tests: te ayuda a escribirlos mejor.
Misión: Toma tu app Atlas Mundial y escribe una suite de tests con Jest + React Testing Library. Cubre: al menos 5 componentes, 1 custom hook (useFetch), y 1 flujo de integración (página de detalle con mock de API). Apunta a coverage >70%. Pídele a la IA: 'Tengo este componente PaisCard. ¿Qué casos de prueba debería cubrir? Dame 8 casos edge que un junior olvidaría.' Anota en tu cuaderno: 3 bugs que tus tests encontraron que no habrías detectado manualmente.
Pasos sugeridos
- Instala dependencias: npm install -D vitest @testing-library/react @testing-library/jest-dom @testing-library/user-event jsdom msw.
- Crea vitest.config.js con environment: 'jsdom' y coverage thresholds.
- Crea tests/setup.js con imports de @testing-library/jest-dom y MSW server.
- Crea tests/atoms/Button.test.jsx con 4 tests: render, click, disabled, loading.
- Crea tests/hooks/useFetch.test.js mockeando fetch con vi.spyOn.
- Crea tests/molecules/SearchBar.test.jsx con userEvent (no fireEvent).
- Crea tests/organisms/PaisCard.test.jsx mockeando useFetch con vi.mock.
- Crea tests/integration/PaisDetalle.test.jsx usando MSW para mockear 2 APIs.
- Ejecuta: npm run test:coverage. Verifica >70%.
- Identifica archivos con coverage <50% y agrega tests específicos.
- Abre coverage/index.html en el navegador. Mira visualmente qué líneas NO están cubiertas.
- Pídele a la IA los 8 casos edge que un junior olvidaría (inputs vacíos, números negativos, caracteres especiales, etc.).
- Implementa esos 8 casos. Re-corre coverage. Verifica que subió.
- Haz commit: 'test: suite con 30+ tests, coverage 78%'.
📓 Entregable: Reporte de coverage (captura de la tabla), código de los 5+ archivos de test, log de los 3 bugs que encontraste gracias a los tests, y commit en Git.
🚫 Errores típicos de razonamiento
Por qué: Si tu test hace 'document.querySelector('.btn-primary')', estás atando el test a la implementación. Si mañana refactorizas a un Button con variant='primary' (mismo estilo, distinto código), el test se rompe aunque el componente siga funcionando. La regla de RTL: testear COMO el usuario, no CÓMO está implementado. Usa getByRole, getByText, getByLabelText. Si encuentras un test que necesita getByTestId, casi siempre hay una mejor query.
Por qué: fireEvent.click() es un evento SINTÉTICO que dispara directamente el handler, sin pasar por las validaciones del navegador. userEvent.click() simula el comportamiento REAL del usuario: mousedown, mouseup, click, focus, blur, etc. Es la diferencia entre 'el handler se llamó' y 'el usuario realmente interactuó'. Para tests realistas, usa SIEMPRE userEvent. fireEvent solo para casos muy específicos (como onSubmit que no se puede con userEvent).
Por qué: Si en el test 1 mockeas useFetch para que devuelva datos de Argentina, y en el test 2 no haces mock nuevo, el test 2 RECIBE los datos de Argentina del test 1. Es un bug sutil que causa tests que pasan en local y fallan en CI (o al revés). Solución: vi.clearAllMocks() en beforeEach, o usa MSW con server.resetHandlers() que es automático. La regla: cada test debe ser INDEPENDIENTE, no depender del estado de otros tests.
Por qué: Un test como 'expect(screen.getByText('Click')).toBeInTheDocument()' solo verifica que el texto existe. No prueba que el botón HACE algo al hacer click, ni que el estado cambia, ni que los datos se muestran correctamente. Es un test de HUMO (smoke test): útil como PRIMER test, pero insuficiente como único. La regla: cada test debe verificar UNA interacción o comportamiento, no solo la existencia.
Por qué: Puedes tener 100% de coverage con tests inútiles: 'expect(component).toBeDefined()' cubre 1 línea, no prueba nada. Coverage mide QUÉ se ejecutó, no QUÉ se verificó. La verdadera métrica: 'si cambio este código, ¿mis tests detectarían un bug?'. Si la respuesta es no, el test no sirve aunque cubra 100 líneas. Coverage >70% es el suelo; tests que detectan bugs reales es el techo.
🧪 Laboratorio práctico
Tests no son overhead: son tu red de seguridad.
Laboratorio: Suite de tests completa para Atlas Mundial
Objetivo: Construir una suite de tests robusta para tu app Atlas Mundial con Jest/Vitest + React Testing Library. Cubrir: 5 componentes, 1 custom hook, 1 flujo de integración con MSW. Coverage objetivo: >70%. El resultado: puedes refactorizar tu app con confianza, sabiendo que si rompes algo, los tests te avisan en 10 segundos.
Pasos
- Setup: npm install -D vitest @testing-library/react @testing-library/jest-dom @testing-library/user-event jsdom msw.
- Crea vitest.config.js (environment: jsdom, globals: true, setupFiles).
- Crea tests/setup.js con import '@testing-library/jest-dom' y MSW server.
- Crea mocks/handlers.js con 2 APIs mockeadas (REST Countries + Open-Meteo).
- Crea mocks/server.js con setupServer(...handlers).
- Test 1: tests/atoms/Button.test.jsx (4 tests).
- Test 2: tests/atoms/Input.test.jsx (3 tests: cambia valor, validación, label).
- Test 3: tests/molecules/SearchBar.test.jsx con userEvent (4 tests).
- Test 4: tests/molecules/PaisCard.test.jsx mockeando useFetch (3 tests: loading, datos, error).
- Test 5: tests/organisms/Header.test.jsx (3 tests: nav links, mobile menu, search).
- Test 6: tests/hooks/useFetch.test.js mockeando global.fetch con vi.spyOn (5 tests).
- Test 7: tests/integration/PaisDetalle.test.jsx con MSW (2 tests: carga exitosa, error de API).
- Ejecuta: npm run test:coverage. Verifica que coverage >70%.
- Para archivos con coverage <50%, agrega tests específicos.
- Configura GitHub Action para correr tests automáticamente (preview del Tema 3.9).
- Haz commit: 'test: 25+ tests, coverage 78%, MSW configurado'.
📓 Entregable: Reporte de coverage en HTML, código de los 7+ archivos de test, log mostrando >70% coverage, captura de los tests pasando en watch mode, y commit en Git.