← CC3062

Testing y Linting en React

Semestre 01, 2026

Motivación

En proyectos reales, el código cambia constantemente. Las pruebas son la red de seguridad que permite cambiar sin romper.

Lo que pasa sin pruebas

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.

Lo que logran las pruebas automáticas

Confianza para refactorizar
Si los tests siguen pasando después de un cambio, el comportamiento no se rompió. Se puede mejorar el código sin miedo.
Detección temprana de regresiones
Cuando algo se rompe, el test falla de inmediato — antes de que llegue a producción.
Documentación viva
Un test bien escrito muestra qué hace el componente, qué props acepta y cuál es el comportamiento esperado.

Tipos de pruebas

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.

El ciclo de desarrollo

1
Escribir el componente

código fuente

2
Escribir el test

qué debe hacer

3
Cambiar código

refactorizar

4
Correr tests

todo sigue verde

Los tests son los que indican si se rompió algo al mejorar el código.

Vitest

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.

Ventajas frente a otras opciones

Comparte la config de Vite — no hay que mantener dos configuraciones separadas para el build y los tests.
Soporte nativo de ESM — funciona con módulos modernos sin necesidad de transpiladores adicionales.
API compatible con Jestdescribe, it, expect, vi. Fácil de migrar desde proyectos existentes.
UI integrada — interfaz visual en el navegador para ver qué tests pasan y cuáles fallan.

Instalación y configuración


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.

Scripts en 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

Anatomía de un test


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();
  });

});
        

Matchers comunes de 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.

Testing Library

La librería estándar para probar componentes React. Probar como lo haría un usuario, no como lo haría un desarrollador.

Limitaciones del DOM directo

Sin Testing Library
// buscar por clase CSS
const btn = document.querySelector('.btn-primary');
// se rompe si se refactoriza el CSS
Con Testing Library
// 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.

Instalación


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

La guía de Testing Library

"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

buscar por texto visible, rol ARIA o label — como lo haría el usuario

No verificar el estado interno de React (useState, useRef)

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

Queries de screen

QueryCuá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

Simular interacciones del usuario


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.

Pruebas de componentes

Tres patrones frecuentes: props condicionales, interacciones con formularios, y listas con estado vacío.

Saludo — componente


// 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 — tests


// 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 — componente


// 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


// 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);
});
        

Lista condicional — tests


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.

Mocking con vi

En los tests, no queremos hacer llamadas reales a APIs ni depender de servicios externos. Los mocks reemplazan esas dependencias con versiones controladas.

El problema de las dependencias externas

Sin mocking: el test hace una llamada real al servidor, que puede fallar, tardar o devolver datos diferentes cada vez.
Con mocking: el test controla exactamente qué devuelve la API — resultados predecibles, tests rápidos.

Los mocks también permiten probar escenarios difíciles de reproducir: errores de red, respuestas vacías, timeouts.

Función espía con 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.

Mockear módulos externos


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

Probar estados de carga


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)

Storybook

Un entorno de desarrollo aislado para componentes de UI — permite construir, documentar y probar visualmente cada componente de forma independiente.

Storybook como entorno de componentes

Aislamiento total — cada componente se desarrolla y prueba en su propio entorno, sin depender del resto de la app.
Catálogo visual — un explorador donde se ven todas las variantes del componente: estados, tamaños, colores, casos borde.
Documentación automática — genera automáticamente una guía de props y ejemplos de uso.
Testing visual e interactivo — permite escribir y correr pruebas directamente sobre las historias.

Instalación


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

El concepto de Story

Una story es un estado específico de un componente con una combinación de props.

Un componente Boton podría tener las historias:

Primario
variante principal
Secundario
variante outline
Deshabilitado
estado disabled
Cargando
con spinner
IconoIzquierda
con ícono

Cada story es un ejemplo vivo del componente — se puede interactuar con él directamente en Storybook.

Escribir stories — Component Story Format


// 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,
  },
};
        

Controles interactivos

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.

Pruebas de interacción con 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.

Storybook vs Testing Library

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.

Pruebas de regresión visual con Chromatic

Chromatic es el servicio oficial de Storybook para detectar cambios visuales automáticamente.

Captura screenshots de cada story en cada PR.
Compara pixel a pixel con la versión aprobada anterior.
Muestra las diferencias visuales para que el equipo las apruebe o rechace.

npm install --save-dev chromatic
npx chromatic --project-token=<token>
        

Linting con ESLint

Analiza el código sin ejecutarlo y reporta bugs potenciales, malas prácticas y problemas de estilo — antes de que lleguen a producción.

ESLint como primera línea de defensa

Análisis estático — lee el código como texto y detecta patrones problemáticos sin ejecutar nada.
Reglas configurables — cada equipo decide qué prácticas son obligatorias, cuáles son advertencias y cuáles se ignoran.
Plugins especializados — existen plugins para React, Hooks, accesibilidad, testing, y más.

Instalació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 ==
    },
  },
];
        

ESLint en React


// 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 de linting


{
  "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.

Prettier — formateo automático del código


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
}
            
ESLint
Calidad y correctitud
Prettier
Estilo visual y formato

Husky y lint-staged

Git hooks para ejecutar linting y tests automáticamente antes de cada commit — sin que nadie tenga que recordarlo.

La necesidad de automatizar

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.

Instalación y configuración


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.

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

Flujo completo con automatización


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 ✅
        

Estructura recomendada

Dónde colocar los tests y las stories en un proyecto React real.

Tests y stories junto al componente


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.

Cuándo usar cada herramienta

HerramientaPara qué sirveCuá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