🧪 3.8 · Testing automatizado: Jest, React Testing Library y la pirámide del testing

⏱ 6h 00min ⚡ 110 XP 🏅 Mid Junior 📖 Frontend Moderno y Proyecto Final
"Un código sin tests es un código que ya tiene bugs, solo que aún no los conoces."

🎯 Objetivo del tema

Al terminar este tema serás capaz de: Al terminar este tema serás capaz de escribir tests automatizados para componentes React con Jest y React Testing Library. Aprenderás: la pirámide del testing (unit, integration, e2e), qué testear y qué NO testear, mocks de APIs y módulos, coverage mínimo del 70%, y el patrón TDD (Test-Driven Development). El resultado: una app donde los cambios no rompen funcionalidad existente, y donde puedes refactorizar con confianza.
🎬
Video del instructor

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%.

1. La pirámide del testing: unit, integration, e2e

Qué tipo de test escribir en cada nivel y por qué.

2. Jest: el framework de testing de JavaScript

Test suites, assertions, beforeEach, mocks, spies.

3. React Testing Library: testear como el usuario

render, screen.getByRole, fireEvent, userEvent.

4. Mocks: simular APIs y módulos externos

jest.mock, MSW (Mock Service Worker), fetch mocks.

5. Coverage: medir qué % del código está testeado

jest --coverage, umbrales, qué cubre y qué no.

6. TDD: escribir el test ANTES del código

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.

📷 Imagen referencial: Pirámide del testing: en la base los tests unitarios (más, rápidos, baratos), en el medio los tests de integración (algunos, medios), y en la cima los tests E2E (pocos, lentos, frágiles pero completos).
NivelQué testeaVelocidadCantidadHerramientas
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.
⭐ 70% unit, 20% integration, 10% e2e: La pirámide del testing te da la regla de oro: 70% unit, 20% integration, 10% e2e. ¿Por qué? Los unit tests son baratos de escribir y mantener (milisegundos, sin red, sin navegador). Los E2E son caros (minutos, requieren navegador, son frágiles a cambios de CSS). Si tienes 1000 tests unitarios y 10 E2E, tu suite corre en 30 segundos. Si tienes 100 unit y 500 E2E, corre en 30 minutos. La pirámide te da velocidad sin perder cobertura.
🎬
Video del instructor

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

⭐ Vitest en Vite = Jest con mejor DX: Jest viene configurado por defecto en proyectos Vite (a través de Vitest, que es API-compatible). Solo necesitas instalar: npm install -D vitest @testing-library/react jsdom. El archivo vitest.config.js le dice a Vitest que use jsdom (entorno navegador) en vez de node. Luego npm test corre todos los archivos *.test.js o *.spec.js automáticamente. Es ZERO configuración, máxima productividad.
🎬
Video del instructor

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

⭐ RTL: cuanto más se parece al usuario, mejor: La filosofía de RTL es: 'cuanto más se parezca tu test a la forma en que tu app es usada, más confianza te da'. Por eso prefiere getByRole (cómo lo encuentra un lector de pantalla) sobre getByClassName (cómo lo encuentra tu CSS). Si tu test usa getByRole y pasa, significa que un usuario con lector de pantalla también puede usar tu componente. Es accesibilidad y testing al mismo tiempo. Es por eso que RTL es EL estándar: te fuerza a hacer componentes accesibles porque de lo contrario no puedes testearlos.
🎬
Video del instructor

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)

⭐ MSW intercepta a nivel de red, no de código: MSW (Mock Service Worker) es la MEJOR forma de mockear APIs en tests porque intercepta a nivel de red, no de código. Tu componente hace un fetch REAL (real fetch('https://api.open-meteo.com/...')), y MSW lo intercepta antes de que salga a internet. Esto significa que tu test prueba el código REAL de fetch, manejo de errores, etc. No estás probando un mock dentro de tu código. Es la diferencia entre 'testear lo que escribiste' y 'testear lo que el usuario experimentaría'.
🎬
Video del instructor

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

⭐ Coverage no es objetivo, es termómetro: El coverage es una HERRAMIENTA, no un OBJETIVO. Tener 100% de coverage no significa que tu código esté bien testeado: puedes tener tests que solo verifican que una función existe (expect(miFuncion).toBeDefined()) sin probar lo que hace. La métrica que SÍ importa: 'si cambio este código, ¿mis tests detectarían un bug?'. Si la respuesta es sí, tienes buenos tests. Si la respuesta es no, tienes tests vacíos aunque coverage sea 100%. Coverage te dice QUÉ no está testeado; no te dice si lo testeado es bueno.
🎬
Video del instructor

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            5min

TDD en acción: 3 pasos para validarEmail

⭐ TDD = programar con confianza: TDD parece más lento al principio ('¿cómo escribo el test si no sé qué hace?'), pero es MÁS RÁPIDO a largo plazo. ¿Por qué? Porque: (1) cada bug se detecta al instante (5 minutos después de escribirlo), no 2 semanas después en producción. (2) tu código está diseñado para ser testeable desde el inicio (mejor arquitectura). (3) los tests son documentación ejecutable: cuando un nuevo developer ve el test, entiende qué hace la función sin leer el código. (4) refactorizas con confianza: si rompes algo, el test lo detecta. Es la diferencia entre 'programar con miedo' y 'programar con confianza'.
🎬
Video del instructor

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

  1. Instala dependencias: npm install -D vitest @testing-library/react @testing-library/jest-dom @testing-library/user-event jsdom msw.
  2. Crea vitest.config.js con environment: 'jsdom' y coverage thresholds.
  3. Crea tests/setup.js con imports de @testing-library/jest-dom y MSW server.
  4. Crea tests/atoms/Button.test.jsx con 4 tests: render, click, disabled, loading.
  5. Crea tests/hooks/useFetch.test.js mockeando fetch con vi.spyOn.
  6. Crea tests/molecules/SearchBar.test.jsx con userEvent (no fireEvent).
  7. Crea tests/organisms/PaisCard.test.jsx mockeando useFetch con vi.mock.
  8. Crea tests/integration/PaisDetalle.test.jsx usando MSW para mockear 2 APIs.
  9. Ejecuta: npm run test:coverage. Verifica >70%.
  10. Identifica archivos con coverage <50% y agrega tests específicos.
  11. Abre coverage/index.html en el navegador. Mira visualmente qué líneas NO están cubiertas.
  12. Pídele a la IA los 8 casos edge que un junior olvidaría (inputs vacíos, números negativos, caracteres especiales, etc.).
  13. Implementa esos 8 casos. Re-corre coverage. Verifica que subió.
  14. 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

Error 1: Testear detalles de implementación (clases CSS, IDs, estado interno).
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.
Error 2: Usar fireEvent en vez de userEvent.
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).
Error 3: No limpiar mocks entre tests (uno contamina al siguiente).
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.
Error 4: Hacer tests que solo verifican que el componente se renderiza (expect(...).toBeInTheDocument()).
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.
Error 5: Confundir coverage alto con buenos tests.
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

  1. Setup: npm install -D vitest @testing-library/react @testing-library/jest-dom @testing-library/user-event jsdom msw.
  2. Crea vitest.config.js (environment: jsdom, globals: true, setupFiles).
  3. Crea tests/setup.js con import '@testing-library/jest-dom' y MSW server.
  4. Crea mocks/handlers.js con 2 APIs mockeadas (REST Countries + Open-Meteo).
  5. Crea mocks/server.js con setupServer(...handlers).
  6. Test 1: tests/atoms/Button.test.jsx (4 tests).
  7. Test 2: tests/atoms/Input.test.jsx (3 tests: cambia valor, validación, label).
  8. Test 3: tests/molecules/SearchBar.test.jsx con userEvent (4 tests).
  9. Test 4: tests/molecules/PaisCard.test.jsx mockeando useFetch (3 tests: loading, datos, error).
  10. Test 5: tests/organisms/Header.test.jsx (3 tests: nav links, mobile menu, search).
  11. Test 6: tests/hooks/useFetch.test.js mockeando global.fetch con vi.spyOn (5 tests).
  12. Test 7: tests/integration/PaisDetalle.test.jsx con MSW (2 tests: carga exitosa, error de API).
  13. Ejecuta: npm run test:coverage. Verifica que coverage >70%.
  14. Para archivos con coverage <50%, agrega tests específicos.
  15. Configura GitHub Action para correr tests automáticamente (preview del Tema 3.9).
  16. 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.

📓 Tu cuaderno: Anota pseudocódigo, diagramas, errores que encontraste y respuestas a "explica sin código". La escritura manual refuerza tu razonamiento. Tu profesor puede pedirte que subas fotos de páginas específicas.