Testing con React Testing Library: Pruebas Centradas en el Usuario

Lectura
35 min~7 min lectura

¿Por qué React Testing Library?

Cuando escribimos pruebas para nuestros componentes de React, es fácil caer en la trampa de probar la implementación en lugar de probar el comportamiento. ¿Cuántas veces has visto tests que verifican el estado interno de un componente o acceden a métodos privados? Esto crea tests frágiles que se rompen cuando refactorizamos código, aunque la funcionalidad siga funcionando perfectamente.

CONCEPTO CLAVE: React Testing Library (RTL) está construida sobre DOM Testing Library y se basa en un principio fundamental: cuanto más se parezcan tus tests a como se usa tu software, más confianza te darán. No probamos los detalles de implementación; probamos lo que el usuario ve y puede interactuar.

La biblioteca fue creada por Kent C. Dodds como una respuesta a la pregunta: "¿Cuánto de esta prueba me da confianza en que mi aplicación funciona para mis usuarios?"

Instalación y Configuración

Para comenzar a usar React Testing Library, necesitas instalarla junto con Jest (el runner de tests más común para React):

npm install --save-dev @testing-library/react @testing-library/jest-dom

Si estás usando Create React App o Vite, probablemente ya venga configurado. Solo necesitas crear archivos de test con la extensión .test.js o .spec.js.

El Principio de las Pruebas Centradas en el Usuario

📌 La filosofía de RTL se resume en una frase: "Obtén el texto, no el elemento". Si el usuario puede encontrar algo por su texto, tu test también debería poder hacerlo.

Imagina que tienes un formulario de inicio de sesión. El usuario ve:

  • Un campo con la etiqueta "Correo electrónico"
  • Un campo con la etiqueta "Contraseña"
  • Un botón que dice "Iniciar sesión"
  • Mensajes de error como "El correo electrónico es requerido"

Un test centrado en el usuario buscaría estos elementos por su texto o accesibilidad, no por selectores CSS o IDs generados por el framework.

Consultas: Encontrando Elementos

RTL proporciona diferentes queries para encontrar elementos en el DOM. Cada una tiene una prioridad recomendada:

Query Prioridad Uso
getByRole 1ª (Accesibilidad) Encuentra elementos por su rol ARIA
getByLabelText 2ª (Formularios) Encuentra inputs por su etiqueta
getByPlaceholderText Encuentra inputs por placeholder
getByText Encuentra elementos por contenido de texto
getByTestId Última opción Atributo data-testid
⚠️ Evita abusar de getByTestId: Es tentador usar data-testid para todo, pero esto es precisamente lo que RTL quiere evitar. Solo úsalo cuando no hay otra forma semántica de encontrar el elemento.

Ejemplo Práctico: Componente LoginForm

Vamos a crear un componente de formulario de login y sus pruebas asociadas:

// LoginForm.jsx
import { useState } from 'react';

export function LoginForm({ onSubmit }) {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');
  const [error, setError] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault();
    if (!email) {
      setError('El correo electrónico es requerido');
      return;
    }
    onSubmit({ email, password });
  };

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label htmlFor="email">Correo electrónico</label>
        <input
          id="email"
          type="email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
        />
      </div>
      <div>
        <label htmlFor="password">Contraseña</label>
        <input
          id="password"
          type="password"
          value={password}
          onChange={(e) => setPassword(e.target.value)}
        />
      </div>
      {error && <p role="alert">{error}</p>}
      <button type="submit">Iniciar sesión</button>
    </form>
  );
}

Las Pruebas

// LoginForm.test.jsx
import { render, screen, fireEvent } from '@testing-library/react';
import { LoginForm } from './LoginForm';

describe('LoginForm', () => {
  it('debe renderizar el formulario con todos sus campos', () => {
    render(<LoginForm onSubmit={jest.fn()} />);
    
    // El usuario ve "Correo electrónico" y "Contraseña"
    expect(screen.getByLabelText(/correo electrónico/i)).toBeInTheDocument();
    expect(screen.getByLabelText(/contraseña/i)).toBeInTheDocument();
    
    // El usuario ve el botón
    expect(screen.getByRole('button', { name: /iniciar sesión/i })).toBeInTheDocument();
  });

  it('debe mostrar error cuando el email está vacío', async () => {
    render(<LoginForm onSubmit={jest.fn()} />);
    
    const user = userEvent.setup();
    
    // Dejamos el campo de email vacío y enviamos el formulario
    await user.click(screen.getByRole('button', { name: /iniciar sesión/i }));
    
    // El usuario ve el mensaje de error
    expect(screen.getByRole('alert')).toHaveTextContent('El correo electrónico es requerido');
  });

  it('debe llamar a onSubmit con los datos cuando el formulario es válido', async () => {
    const mockSubmit = jest.fn();
    render(<LoginForm onSubmit={mockSubmit} />);
    
    const user = userEvent.setup();
    
    // El usuario llena el formulario
    await user.type(screen.getByLabelText(/correo electrónico/i), '[email protected]');
    await user.type(screen.getByLabelText(/contraseña/i), 'contraseña123');
    
    // El usuario hace clic en el botón
    await user.click(screen.getByRole('button', { name: /iniciar sesión/i }));
    
    // La función onSubmit fue llamada con los datos correctos
    expect(mockSubmit).toHaveBeenCalledWith({
      email: '[email protected]',
      password: 'contraseña123',
    });
  });
});

Eventos: Simulando Interacciones

💡 Tip importante: RTL ofrece dos formas de simular eventos: fireEvent (más directo) y userEvent de @testing-library/user-event (más realista, simula comportamiento humano real).

La librería userEvent es preferible porque:

  1. Simula múltiples eventos que ocurrirían en un navegador real (keydown, keyup, change)
  2. Maneja mejor casos edge como escribir en un campo deshabilitado
  3. Es más cercana a como un usuario real interactúa con la interfaz
import userEvent from '@testing-library/user-event';

// Más realista que fireEvent
await userEvent.type(screen.getByRole('textbox', { name: /búsqueda/i }), 'React');
await userEvent.click(screen.getByRole('button', { name: /buscar/i }));

Pruebas de Componentes Asíncronos

Los componentes que cargan datos de forma asíncrona son comunes. RTL proporciona waitFor y queries asíncronas:

import { render, screen, waitFor } from '@testing-library/react';
import { AsyncComponent } from './AsyncComponent';

it('debe mostrar los datos cuando se cargan', async () => {
  render(<AsyncComponent />);
  
  // Al inicio, muestra loading
  expect(screen.getByText(/cargando/i)).toBeInTheDocument();
  
  // Después de cargar, muestra los datos
  await waitFor(() => {
    expect(screen.getByText('Datos cargados')).toBeInTheDocument();
  });
});

// O más simple, usando findBy
it('debe mostrar los datos cuando se cargan (versión simple)', async () => {
  render(<AsyncComponent />);
  
  expect(await screen.findByText('Datos cargados')).toBeInTheDocument();
});
📌 Nota sobre findBy: Los queries findBy son combinaciones de getBy + waitFor. Devuelven una promesa que se resuelve cuando el elemento aparece o se rechaza si no aparece.

Debugging: Viendo el HTML Generado

Una de las características más útiles de RTL es la función debug():

const { container } = render(<MiComponente />);

// Imprime el HTML en la consola
console.log(container.innerHTML);

// O más bonito
screen.debug();

// Para un elemento específico
screen.debug(screen.getByRole('button'));

Matchers Personalizados

Con @testing-library/jest-dom obtienes matchers adicionales que hacen tus aserciones más legibles:

import '@testing-library/jest-dom';

// Aserciones útiles
expect(button).toBeDisabled();
expect(input).toHaveValue('texto');
expect(element).toBeInTheDocument();
expect(element).toHaveTextContent('hola mundo');
expect(input).toHaveFocus();
expect(element).toHaveAttribute('data-active', 'true');

Buenas Prácticas

  1. No pruebes la implementación: No accedas a estados, métodos internos o refs.
  2. Prioriza la accesibilidad: Si puedes encontrar algo por su rol o etiqueta, úsalo.
  3. Escribe tests desde la perspectiva del usuario: ¿Qué haría un usuario? ¿Qué vería?
  4. Mantén tus tests simples: Un test debe probar una sola cosa.
  5. Usa setup functions: Si tienes configuración repetitiva, usa beforeEach.
⚠️ Error común: No importes componentes internos para hacer testing de sus funciones. Los tests deben atravesar la interfaz pública del componente, no sus partes privadas.

Configuración de Jest Globals

Para que todo funcione correctamente, asegúrate de configurar Jest en tu archivo de configuración:

// jest.config.js
module.exports = {
  testEnvironment: 'jsdom',
  setupFilesAfterEnv: ['@testing-library/jest-dom'],
  moduleNameMapper: {
    '\\.(css|less|scss|sass)$': 'identity-obj-proxy',
  },
};
Ver más

El identity-obj-proxy es un mock que permite importar archivos CSS sin errores. Si usas CSS modules, puedes configurarlo así:

moduleNameMapper: {
  '\\.module\\.css$': 'identity-obj-proxy',
}

Integración con Mocks

A veces necesitas simular módulos o funciones, especialmente para APIs externas:

// Mock de fetch
global.fetch = jest.fn(() =>
  Promise.resolve({
    json: () => Promise.resolve({ data: 'mocked data' }),
  })
);

// Mock de un módulo
jest.mock('./api', () => ({
  fetchUser: jest.fn(() => Promise.resolve({ name: 'Ana' })),
}));

Resumen

React Testing Library representa un cambio de paradigma en cómo escribimos pruebas para React. En lugar de obsesionarnos con el estado interno y la implementación, nos centramos en lo que el usuario realmente ve y puede hacer. Este enfoque nos da:

  • Tests más robustos que no se rompen con refactors
  • Mejor accesibilidad (al buscar por roles y etiquetas)
  • Mayor confianza de que la aplicación funciona para los usuarios
  • Tests más mantenibles a largo plazo
La mejor prueba es aquella que verifica que tu aplicación funciona correctamente, y la mejor forma de verificar esto es interactuando con ella como lo haría un usuario real.
🧠 Quiz

¿Cuál es el query de mayor prioridad en React Testing Library para encontrar elementos?

  • A) getByTestId
  • B) getByText
  • C) getByRole
  • D) getByClassName
✅ Respuesta correcta: C) getByRole. La accesibilidad es prioritaria porque refleja cómo los usuarios (especialmente aquellos con tecnologías asistivas) interactúan con la interfaz. Si un elemento es importante para el usuario, debería tener un rol accesible semánticamente.
💡 Próximo paso: Practica refactorizando tests existentes de enzyme (si los tienes) a React Testing Library. Verás cómo los tests se vuelven más simples y significativos.