Semestre 01, 2026
En proyectos reales, el código cambia constantemente. Las pruebas son la red de seguridad que permite cambiar sin romper.
Un componente funciona hoy.
Alguien modifica una función que usa ese componente.
Dos semanas después, algo falla en producción.
Nadie recuerda que ese componente dependía de esa función.
Este escenario es tan común que tiene nombre: regresión.
| Tipo | ¿Qué prueba? | Velocidad | Confianza |
|---|---|---|---|
| Unitaria | Una función o componente aislado | Muy rápida | Baja cobertura del sistema |
| Integración | Varios componentes juntos | Rápida | Alta para flujos comunes |
| Visual | Apariencia y variantes del componente | Rápida | Alta para UI |
| E2E | El flujo completo en el navegador | Lenta | Muy alta |
En React, la mayoría son pruebas de integración — componentes con sus interacciones reales.
código fuente
qué debe hacer
refactorizar
todo sigue verde
Los tests son los que indican si se rompió algo al mejorar el código.
Framework de pruebas nativo del ecosistema Vite. Usa la misma configuración del proyecto, es muy rápido y tiene una API idéntica a Jest.
describe, it, expect, vi. Fácil de migrar desde proyectos existentes.
npm install -D vitest @vitest/ui jsdom
// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [react()],
test: {
globals: true, // sin imports de describe/it
environment: 'jsdom',// simula el navegador
setupFiles: './src/test/setup.js',
},
});
jsdom es un navegador simulado que corre en Node.js — permite renderizar componentes React sin abrir un navegador real.
package.json
{
"scripts": {
"test": "vitest",
"test:ui": "vitest --ui",
"test:run": "vitest run",
"coverage": "vitest run --coverage"
}
}
vitest — modo watch: se queda corriendo y re-ejecuta los tests al guardar un archivo
vitest run — ejecuta una sola vez y termina (ideal para CI/CD)
vitest --ui — abre una interfaz visual en el navegador para explorar los resultados
--coverage — genera un reporte de qué porcentaje del código está cubierto por tests
import { describe, it, expect } from 'vitest';
import { sumar, esPar } from './utils';
describe('utilidades matemáticas', () => { // agrupa tests relacionados
it('suma dos números correctamente', () => {
// Arrange → Act → Assert
expect(sumar(2, 3)).toBe(5);
});
it('reconoce números pares', () => {
expect(esPar(4)).toBe(true);
expect(esPar(7)).toBe(false);
});
it('lanza error si el argumento no es número', () => {
// Verificar que se lanza una excepción
expect(() => sumar('a', 1)).toThrow();
});
});
expect| Matcher | ¿Qué verifica? |
|---|---|
toBe(valor) | Igualdad estricta (===) — para primitivos |
toEqual(objeto) | Igualdad profunda de objetos/arrays |
toBeTruthy() / toBeFalsy() | Valor verdadero o falso |
toBeNull() | Es null |
toContain(elemento) | Array o string contiene el elemento |
toHaveLength(n) | Longitud es n |
toThrow() | La función lanza un error |
toBeGreaterThan(n) | Es mayor que n |
Usar toBe para valores simples, toEqual para comparar objetos o arrays completos.
La librería estándar para probar componentes React. Probar como lo haría un usuario, no como lo haría un desarrollador.
// buscar por clase CSS
const btn = document.querySelector('.btn-primary');
// se rompe si se refactoriza el CSS
// buscar por rol y nombre
screen.getByRole('button',
{ name: 'Enviar' });
// sobrevive cualquier refactor de estilos
Los tests que buscan por estructura interna son frágiles — se rompen con cualquier cambio de CSS o HTML.
npm install -D \
@testing-library/react \
@testing-library/user-event \
@testing-library/jest-dom
// src/test/setup.js
import '@testing-library/jest-dom';
// agrega matchers como:
// toBeInTheDocument()
// toHaveValue()
// toBeDisabled()
@testing-library/react — funciones render y screen para montar y consultar componentes
@testing-library/user-event — simula interacciones reales (clicks, teclado, formularios)
@testing-library/jest-dom — matchers extra para verificar el DOM
"Cuanto más se parezcan las pruebas a cómo se usa el software, más confianza dan."
No buscar componentes por clase CSS o estructura interna del HTML
Sí buscar por texto visible, rol ARIA o label — como lo haría el usuario
No verificar el estado interno de React (useState, useRef)
Sí verificar lo que aparece en pantalla y lo que puede hacer el usuario
render y screen
import { render, screen } from '@testing-library/react';
import { Saludo } from './Saludo';
it('muestra el nombre del usuario', () => {
// render() monta el componente en un DOM virtual
render( );
// screen da acceso a todos los elementos renderizados
// toBeInTheDocument() viene de @testing-library/jest-dom
expect(screen.getByText('Hola, Ana')).toBeInTheDocument();
});
render(...) — monta el componente en un DOM virtual con jsdom
screen — objeto global que da acceso al DOM renderizado
Después de cada test, el DOM se limpia automáticamente
screen| Query | Cuándo usarla |
|---|---|
getByRole('button', { name: 'Enviar' }) | Preferida — elemento por rol ARIA y nombre accesible |
getByLabelText('Email') | Input asociado a un <label> — formularios |
getByText('texto') | Elemento con texto exacto visible |
getByPlaceholderText('Buscar...') | Input por placeholder |
getByTestId('mi-id') | Último recurso con data-testid |
queryByText(...) | Como get pero devuelve null si no existe |
findByText(...) | Como get pero espera a que aparezca (async) |
Prioridad: getByRole > getByLabelText > getByText > getByTestId
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Contador } from './Contador';
it('incrementa el conteo al hacer clic', async () => {
// userEvent.setup() prepara el simulador de usuario
const user = userEvent.setup();
render( );
const boton = screen.getByRole('button', { name: 'Incrementar' });
expect(screen.getByText('Conteo: 0')).toBeInTheDocument();
await user.click(boton);
expect(screen.getByText('Conteo: 1')).toBeInTheDocument();
await user.click(boton);
expect(screen.getByText('Conteo: 2')).toBeInTheDocument();
});
userEvent simula eventos reales (mousedown, focus, keypress, mouseup) — más fiel al navegador real que fireEvent.
Tres patrones frecuentes: props condicionales, interacciones con formularios, y listas con estado vacío.
// Saludo.jsx
// Componente simple que recibe nombre y estado de conexión
export function Saludo({ nombre, activo }) {
return (
<div>
<h1>Hola, {nombre}</h1>
{activo ? <span>En línea</span> : <span>Desconectado</span>}
</div>
);
}
Este componente tiene dos ramas: cuando el usuario está activo y cuando no lo está. Cada rama debe tener su propio test.
// Saludo.test.jsx
import { render, screen } from '@testing-library/react';
import { Saludo } from './Saludo';
describe('Saludo', () => {
it('muestra el nombre recibido por props', () => {
render(<Saludo nombre="Carlos" activo={true} />);
expect(screen.getByText('Hola, Carlos')).toBeInTheDocument();
});
it('muestra "En línea" cuando activo es true', () => {
render(<Saludo nombre="Ana" activo={true} />);
expect(screen.getByText('En línea')).toBeInTheDocument();
expect(screen.queryByText('Desconectado')).not.toBeInTheDocument();
});
it('muestra "Desconectado" cuando activo es false', () => {
render(<Saludo nombre="Ana" activo={false} />);
expect(screen.getByText('Desconectado')).toBeInTheDocument();
expect(screen.queryByText('En línea')).not.toBeInTheDocument();
});
});
Probar el "camino feliz" y el caso alternativo — cada rama del componente merece un test.
// Buscador.jsx
// Componente de formulario controlado que notifica al padre
export function Buscador({ onBuscar }) {
const [texto, setTexto] = useState('');
return (
<form onSubmit={e => { e.preventDefault(); onBuscar(texto); }}>
<label>
Buscar
<input value={texto} onChange={e => setTexto(e.target.value)} />
</label>
<button type="submit">Buscar</button>
</form>
);
}
El test debe verificar que la función onBuscar recibe el valor correcto cuando el usuario usa el formulario.
// Buscador.test.jsx
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Buscador } from './Buscador';
it('llama a onBuscar con el texto ingresado al enviar', async () => {
const user = userEvent.setup();
const mockBuscar = vi.fn(); // función espía — registra las llamadas
render(<Buscador onBuscar={mockBuscar} />);
// Simular al usuario escribiendo y haciendo clic
await user.type(screen.getByLabelText('Buscar'), 'React testing');
await user.click(screen.getByRole('button', { name: 'Buscar' }));
// Verificar que el callback recibió los datos correctos
expect(mockBuscar).toHaveBeenCalledWith('React testing');
expect(mockBuscar).toHaveBeenCalledTimes(1);
});
describe('ListaProductos', () => {
// Caso borde: lista vacía — siempre probar este escenario
it('muestra mensaje cuando la lista está vacía', () => {
render(<ListaProductos productos={[]} />);
expect(screen.getByText('No hay productos disponibles'))
.toBeInTheDocument();
});
// Caso normal: lista con datos
it('renderiza cada producto de la lista', () => {
const productos = [
{ id: 1, nombre: 'Laptop' },
{ id: 2, nombre: 'Mouse' },
];
render(<ListaProductos productos={productos} />);
expect(screen.getByText('Laptop')).toBeInTheDocument();
expect(screen.getByText('Mouse')).toBeInTheDocument();
});
});
Siempre probar el estado vacío — es el error más común que los usuarios reportan.
viEn los tests, no queremos hacer llamadas reales a APIs ni depender de servicios externos. Los mocks reemplazan esas dependencias con versiones controladas.
Los mocks también permiten probar escenarios difíciles de reproducir: errores de red, respuestas vacías, timeouts.
vi.fn()
// Una función espía registra cada vez que es llamada
const mockFn = vi.fn();
mockFn('argumento');
// ¿Fue llamada?
expect(mockFn).toHaveBeenCalled();
// ¿Con qué argumentos?
expect(mockFn).toHaveBeenCalledWith('argumento');
// ¿Cuántas veces?
expect(mockFn).toHaveBeenCalledTimes(1);
// También puede retornar un valor predefinido
const mockSuma = vi.fn().mockReturnValue(42);
expect(mockSuma()).toBe(42);
Ideal para probar callbacks y props de tipo función — como onClick, onChange, onSubmit.
// Interceptar el módulo completo de la API
vi.mock('./api', () => ({
obtenerProductos: vi.fn().mockResolvedValue([
{ id: 1, nombre: 'Laptop', precio: 5000 },
{ id: 2, nombre: 'Mouse', precio: 150 },
]),
}));
import { render, screen } from '@testing-library/react';
import { CatalogoProductos } from './CatalogoProductos';
it('muestra los productos cargados desde la API', async () => {
render(<CatalogoProductos />);
// findByText espera a que el componente termine de cargar
expect(await screen.findByText('Laptop')).toBeInTheDocument();
expect(screen.getByText('Mouse')).toBeInTheDocument();
});
mockResolvedValue simula una respuesta exitosa de una promesa. mockRejectedValue simula un error.
it('muestra "Cargando..." y luego los productos', async () => {
render(<CatalogoProductos />);
// Verificar el estado inicial de carga
expect(screen.getByText('Cargando...')).toBeInTheDocument();
// findBy espera (con timeout) hasta que aparezca el elemento
expect(await screen.findByText('Laptop')).toBeInTheDocument();
// Verificar que el spinner desapareció
expect(screen.queryByText('Cargando...')).not.toBeInTheDocument();
});
getBy — busca ahora mismo, falla si no existe
findBy — espera hasta 1000ms por defecto (ideal para operaciones async)
queryBy — devuelve null si no existe (para verificar ausencia)
Un entorno de desarrollo aislado para componentes de UI — permite construir, documentar y probar visualmente cada componente de forma independiente.
# Detecta el framework automáticamente (React + Vite)
npx storybook@latest init
# Inicia el servidor de Storybook
npm run storybook
# Build estático (para publicar)
npm run build-storybook
El comando init detecta el stack y configura todo automáticamente.
Crea la carpeta .storybook/ con la configuración y ejemplos en src/stories/.
Storybook corre en http://localhost:6006 por defecto.
Una story es un estado específico de un componente con una combinación de props.
Un componente Boton podría tener las historias:
PrimarioSecundarioDeshabilitadoCargandoIconoIzquierdaCada story es un ejemplo vivo del componente — se puede interactuar con él directamente en Storybook.
// Boton.stories.jsx
import { Boton } from './Boton';
// Meta: define el componente y su configuración global
export default {
title: 'Componentes/Boton', // ruta en el explorador
component: Boton,
argTypes: {
variante: { control: 'select', options: ['primario', 'secundario'] },
disabled: { control: 'boolean' },
},
};
// Cada export nombrado es una story
export const Primario = {
args: {
children: 'Guardar',
variante: 'primario',
},
};
export const Secundario = {
args: {
children: 'Cancelar',
variante: 'secundario',
},
};
export const Deshabilitado = {
args: {
children: 'No disponible',
disabled: true,
},
};
Los Controls de Storybook generan automáticamente paneles para modificar las props del componente en tiempo real.
'text' | Input de texto |
'boolean' | Toggle on/off |
'number' | Input numérico |
'select' | Dropdown con opciones |
'color' | Color picker |
argTypes: {
label: {
control: 'text',
},
size: {
control: 'select',
options: ['sm', 'md', 'lg'],
},
disabled: {
control: 'boolean',
},
}
Sin escribir código, se puede probar cualquier combinación de props directamente en el navegador.
play()
import { within, userEvent, expect } from '@storybook/test';
export const FormularioEnviado = {
args: {
onEnviar: fn(), // función espía de Storybook
},
// play() se ejecuta automáticamente al cargar la story
play: async ({ canvasElement, args }) => {
const canvas = within(canvasElement);
const user = userEvent.setup();
// Simular al usuario llenando el formulario
await user.type(canvas.getByLabelText('Email'), '[email protected]');
await user.type(canvas.getByLabelText('Mensaje'), 'Hola mundo');
await user.click(canvas.getByRole('button', { name: 'Enviar' }));
// Verificar el resultado
await expect(args.onEnviar).toHaveBeenCalledWith({
email: '[email protected]',
mensaje: 'Hola mundo',
});
},
};
Las historias con play() son tests de integración visual — se ven en el navegador y también se pueden correr en CI.
| Vitest + Testing Library | Storybook | |
|---|---|---|
| ¿Qué prueba? | Lógica y comportamiento | Apariencia y variantes |
| ¿Dónde corre? | Terminal / CI | Navegador |
| ¿Para quién? | Desarrolladores | Diseñadores y desarrolladores |
| ¿Documenta? | No | Sí, automáticamente |
| ¿Interactivo? | No | Sí, con controls |
Son herramientas complementarias — se usan juntas. Los tests de lógica van en Vitest, la documentación y pruebas visuales van en Storybook.
Chromatic es el servicio oficial de Storybook para detectar cambios visuales automáticamente.
npm install --save-dev chromatic
npx chromatic --project-token=<token>
Analiza el código sin ejecutarlo y reporta bugs potenciales, malas prácticas y problemas de estilo — antes de que lleguen a producción.
npm install -D eslint @eslint/js eslint-plugin-react eslint-plugin-react-hooks
eslint — el motor principal del linter
@eslint/js — reglas base para JavaScript
eslint-plugin-react — reglas específicas para JSX y React
eslint-plugin-react-hooks — valida las reglas de los Hooks (muy importante)
eslint.config.js
import js from '@eslint/js';
import reactPlugin from 'eslint-plugin-react';
import reactHooksPlugin from 'eslint-plugin-react-hooks';
export default [
js.configs.recommended,
{
plugins: {
react: reactPlugin,
'react-hooks': reactHooksPlugin,
},
rules: {
// React
'react/prop-types': 'warn',
'react/jsx-no-duplicate-props': 'error',
'react/self-closing-comp': 'warn',
// React Hooks — estas dos son críticas
'react-hooks/rules-of-hooks': 'error', // hooks en el lugar correcto
'react-hooks/exhaustive-deps': 'warn', // dependencias completas en useEffect
// JavaScript general
'no-unused-vars': 'warn',
'no-console': 'warn',
'eqeqeq': 'error', // siempre === nunca ==
},
},
];
// ERROR: react-hooks/rules-of-hooks — hook dentro de condicional
if (condicion) {
const [valor, setValor] = useState(0); // los hooks siempre al nivel superior
}
// WARN: react-hooks/exhaustive-deps — dependencia faltante en useEffect
useEffect(() => {
fetchDatos(userId); // userId se usa pero no está en el array de dependencias
}, []); // ← debería ser [userId]
// WARN: no-unused-vars — variable declarada pero nunca usada
const resultado = calcular(); // resultado nunca se usa en el componente
// ERROR: eqeqeq — comparación débil (puede causar bugs sutiles)
if (valor == null) { ... } // debe ser ===
{
"scripts": {
"lint": "eslint src/",
"lint:fix": "eslint src/ --fix"
}
}
npm run lint — lista todos los problemas encontrados en el código.
npm run lint:fix — corrige automáticamente los que puede (espacios, punto y coma, comillas) y lista los que requieren atención manual.
npm install -D prettier \
eslint-config-prettier
Prettier formatea el código automáticamente — indentación, comillas, longitud de línea. eslint-config-prettier desactiva las reglas de ESLint que entrarían en conflicto.
// .prettierrc
{
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "es5",
"printWidth": 100
}
Git hooks para ejecutar linting y tests automáticamente antes de cada commit — sin que nadie tenga que recordarlo.
Los scripts de npm run lint y npm run test solo se ejecutan cuando alguien recuerda hacerlo.
Con git hooks: se ejecutan automáticamente antes de cada commit.
Si el linting o los tests fallan, el commit se bloquea.
Código con errores de lint o tests fallidos nunca llega al repositorio.
npm install -D husky lint-staged
npx husky init
# crea .husky/ con el hook pre-commit
// package.json
{
"lint-staged": {
"src/**/*.{js,jsx}": [
"eslint --fix",
"prettier --write"
],
"src/**/*.{json,css}": [
"prettier --write"
]
}
}
lint-staged ejecuta los comandos solo sobre los archivos modificados en el commit — no sobre todo el proyecto.
.husky/pre-commit
#!/bin/sh
npx lint-staged
# npm run test:run ← opcional: bloquea el commit si falla algún test
Este archivo se ejecuta automáticamente cada vez que alguien hace git commit. Si algún comando falla, el commit se cancela y Git muestra el error.
git add .
git commit -m "feat: agregar formulario de contacto"
↓
.husky/pre-commit ejecuta:
├── lint-staged → eslint --fix sobre archivos cambiados
├── lint-staged → prettier --write sobre archivos cambiados
└── vitest run (si está configurado)
↓
¿Errores? → commit bloqueado, ver el mensaje de error
¿Todo OK? → commit realizado ✅
Dónde colocar los tests y las stories en un proyecto React real.
src/
├── components/
│ ├── Buscador.jsx
│ ├── Buscador.test.jsx ← pruebas de lógica e interacción
│ ├── Buscador.stories.jsx ← stories para Storybook
│ ├── ListaProductos.jsx
│ ├── ListaProductos.test.jsx
│ └── ListaProductos.stories.jsx
├── hooks/
│ ├── useFetch.js
│ └── useFetch.test.js
├── utils/
│ ├── formatear.js
│ └── formatear.test.js
└── test/
└── setup.js ← configuración global de Vitest
El test y la story junto al componente — si el componente se mueve, los tests se mueven con él.
| Herramienta | Para qué sirve | Cuándo usarla |
|---|---|---|
| Vitest | Framework de tests | Siempre — base de todo |
| Testing Library | Renderizar y probar componentes | Para cada componente React |
| Storybook | Documentar y probar visualmente | Componentes reutilizables de UI |
| ESLint | Detectar errores y malas prácticas | Todo proyecto desde el inicio |
| Prettier | Formatear el código | Todo proyecto en equipo |
| Husky | Automatizar en git commits | Proyectos con múltiples personas |