Español

Domina React Testing Library (RTL) con esta guía completa. Escribe pruebas efectivas y centradas en el usuario para tus aplicaciones React.

React Testing Library: Una Guía Completa

En el panorama actual del desarrollo web, que avanza a pasos agigantados, asegurar la calidad y la fiabilidad de tus aplicaciones React es primordial. React Testing Library (RTL) se ha convertido en una solución popular y eficaz para escribir pruebas que se centran en la perspectiva del usuario. Esta guía proporciona una visión general completa de RTL, que cubre todo, desde los conceptos fundamentales hasta las técnicas avanzadas, y te capacita para crear aplicaciones React robustas y mantenibles.

¿Por qué elegir React Testing Library?

Los enfoques de prueba tradicionales a menudo se basan en los detalles de implementación, lo que hace que las pruebas sean frágiles y propensas a romperse con cambios menores en el código. RTL, por otro lado, te anima a probar tus componentes como lo haría un usuario, centrándote en lo que el usuario ve y experimenta. Este enfoque ofrece varias ventajas clave:

Configuración de tu entorno de prueba

Antes de que puedas empezar a usar RTL, necesitas configurar tu entorno de prueba. Esto normalmente implica la instalación de las dependencias necesarias y la configuración de tu marco de pruebas.

Requisitos previos

Instalación

Instala los siguientes paquetes usando npm o yarn:

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

O, usando yarn:

yarn add --dev @testing-library/react @testing-library/jest-dom jest babel-jest @babel/preset-env @babel/preset-react

Explicación de los paquetes:

Configuración

Crea un archivo `babel.config.js` en la raíz de tu proyecto con el siguiente contenido:

module.exports = {
  presets: ['@babel/preset-env', '@babel/preset-react'],
};

Actualiza tu archivo `package.json` para incluir un script de prueba:

{
  "scripts": {
    "test": "jest"
  }
}

Crea un archivo `jest.config.js` en la raíz de tu proyecto para configurar Jest. Una configuración mínima podría verse así:

module.exports = {
  testEnvironment: 'jsdom',
  setupFilesAfterEnv: ['<rootDir>/src/setupTests.js'],
};

Crea un archivo `src/setupTests.js` con el siguiente contenido. Esto asegura que los matchers de Jest DOM estén disponibles en todas tus pruebas:

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

Escribiendo tu primera prueba

Empecemos con un ejemplo sencillo. Supón que tienes un componente React que muestra un mensaje de saludo:

// src/components/Greeting.js
import React from 'react';

function Greeting({ name }) {
  return <h1>¡Hola, {name}!</h1>;
}

export default Greeting;

Ahora, escribamos una prueba para este componente:

// src/components/Greeting.test.js
import React from 'react';
import { render, screen } from '@testing-library/react';
import Greeting from './Greeting';

test('muestra un mensaje de saludo', () => {
  render(<Greeting name="World" />);
  const greetingElement = screen.getByText(/¡Hola, World!/i);
  expect(greetingElement).toBeInTheDocument();
});

Explicación:

Para ejecutar la prueba, ejecuta el siguiente comando en tu terminal:

npm test

Si todo está configurado correctamente, la prueba debería pasar.

Consultas comunes de RTL

RTL proporciona varios métodos de consulta para encontrar elementos en el DOM. Estas consultas están diseñadas para imitar la forma en que los usuarios interactúan con tu aplicación.

`getByRole`

Esta consulta encuentra un elemento por su rol ARIA. Es una buena práctica usar `getByRole` siempre que sea posible, ya que promueve la accesibilidad y asegura que tus pruebas sean resistentes a los cambios en la estructura del DOM subyacente.

<button role="button">Haz clic aquí</button>
const buttonElement = screen.getByRole('button');
expect(buttonElement).toBeInTheDocument();

`getByLabelText`

Esta consulta encuentra un elemento por el texto de su etiqueta asociada. Es útil para probar elementos de formulario.

<label htmlFor="name">Nombre:</label>
<input type="text" id="name" />
const nameInputElement = screen.getByLabelText('Nombre:');
expect(nameInputElement).toBeInTheDocument();

`getByPlaceholderText`

Esta consulta encuentra un elemento por su texto de marcador de posición.

<input type="text" placeholder="Introduce tu correo electrónico" />
const emailInputElement = screen.getByPlaceholderText('Introduce tu correo electrónico');
expect(emailInputElement).toBeInTheDocument();

`getByAltText`

Esta consulta encuentra un elemento de imagen por su texto alternativo. Es importante proporcionar un texto alternativo significativo para todas las imágenes para garantizar la accesibilidad.

<img src="logo.png" alt="Logotipo de la empresa" />
const logoImageElement = screen.getByAltText('Logotipo de la empresa');
expect(logoImageElement).toBeInTheDocument();

`getByTitle`

Esta consulta encuentra un elemento por su atributo title.

<span title="Cerrar">X</span>
const closeElement = screen.getByTitle('Cerrar');
expect(closeElement).toBeInTheDocument();

`getByDisplayValue`

Esta consulta encuentra un elemento por su valor de visualización. Esto es útil para probar las entradas de formulario con valores pre-rellenados.

<input type="text" value="Valor inicial" />
const inputElement = screen.getByDisplayValue('Valor inicial');
expect(inputElement).toBeInTheDocument();

Consultas `getAllBy*`

Además de las consultas `getBy*`, RTL también proporciona consultas `getAllBy*`, que devuelven una matriz de elementos coincidentes. Estas son útiles cuando necesitas afirmar que múltiples elementos con las mismas características están presentes en el DOM.

<li>Elemento 1</li>
<li>Elemento 2</li>
<li>Elemento 3</li>
const listItems = screen.getAllByRole('listitem');
expect(listItems).toHaveLength(3);

Consultas `queryBy*`

Las consultas `queryBy*` son similares a las consultas `getBy*`, pero devuelven `null` si no se encuentra ningún elemento coincidente, en lugar de lanzar un error. Esto es útil cuando quieres afirmar que un elemento *no* está presente en el DOM.

const missingElement = screen.queryByText('Texto no existente');
expect(missingElement).toBeNull();

Consultas `findBy*`

Las consultas `findBy*` son versiones asíncronas de las consultas `getBy*`. Devuelven una Promesa que se resuelve cuando se encuentra el elemento coincidente. Estas son útiles para probar operaciones asíncronas, como la obtención de datos de una API.

// Simulación de una obtención de datos asíncrona
const fetchData = () => new Promise(resolve => {
  setTimeout(() => resolve('¡Datos cargados!'), 1000);
});

function MyComponent() {
  const [data, setData] = React.useState(null);

  React.useEffect(() => {
    fetchData().then(setData);
  }, []);

  return <div>{data}</div>;
}
test('carga datos asíncronamente', async () => {
  render(<MyComponent />);
  const dataElement = await screen.findByText('¡Datos cargados!');
  expect(dataElement).toBeInTheDocument();
});

Simulación de interacciones del usuario

RTL proporciona las API `fireEvent` y `userEvent` para simular interacciones del usuario, como hacer clic en botones, escribir en campos de entrada y enviar formularios.

`fireEvent`

`fireEvent` te permite activar programáticamente eventos DOM. Es una API de bajo nivel que te da un control preciso sobre los eventos que se activan.

<button onClick={() => alert('¡Botón pulsado!')}>Haz clic aquí</button>
import { fireEvent } from '@testing-library/react';

test('simula un clic en un botón', () => {
  const alertMock = jest.spyOn(window, 'alert').mockImplementation(() => {});
  render(<button onClick={() => alert('¡Botón pulsado!')}>Haz clic aquí</button>);
  const buttonElement = screen.getByRole('button');
  fireEvent.click(buttonElement);
  expect(alertMock).toHaveBeenCalledTimes(1);
  alertMock.mockRestore();
});

`userEvent`

`userEvent` es una API de nivel superior que simula las interacciones del usuario de forma más realista. Maneja detalles como la gestión del enfoque y el orden de los eventos, haciendo que tus pruebas sean más robustas y menos frágiles.

<input type="text" onChange={e => console.log(e.target.value)} />
import userEvent from '@testing-library/user-event';

test('simula escribir en un campo de entrada', () => {
  const inputElement = screen.getByRole('textbox');
  userEvent.type(inputElement, '¡Hola, mundo!');
  expect(inputElement).toHaveValue('¡Hola, mundo!');
});

Pruebas de código asíncrono

Muchas aplicaciones React implican operaciones asíncronas, como la obtención de datos de una API. RTL proporciona varias herramientas para probar código asíncrono.

`waitFor`

`waitFor` te permite esperar a que una condición se cumpla antes de hacer una afirmación. Es útil para probar operaciones asíncronas que tardan algún tiempo en completarse.

function MyComponent() {
  const [data, setData] = React.useState(null);

  React.useEffect(() => {
    setTimeout(() => {
      setData('¡Datos cargados!');
    }, 1000);
  }, []);

  return <div>{data}</div>;
}
import { waitFor } from '@testing-library/react';

test('espera a que se carguen los datos', async () => {
  render(<MyComponent />);
  await waitFor(() => screen.getByText('¡Datos cargados!'));
  const dataElement = screen.getByText('¡Datos cargados!');
  expect(dataElement).toBeInTheDocument();
});

Consultas `findBy*`

Como se mencionó anteriormente, las consultas `findBy*` son asíncronas y devuelven una Promesa que se resuelve cuando se encuentra el elemento coincidente. Estas son útiles para probar operaciones asíncronas que resultan en cambios en el DOM.

Pruebas de Hooks

Los React Hooks son funciones reutilizables que encapsulan la lógica con estado. RTL proporciona la utilidad `renderHook` de `@testing-library/react-hooks` (que está en desuso a favor de `@testing-library/react` directamente a partir de la v17) para probar Hooks personalizados de forma aislada.

// src/hooks/useCounter.js
import { useState } from 'react';

function useCounter(initialValue = 0) {
  const [count, setCount] = useState(initialValue);

  const increment = () => {
    setCount(prevCount => prevCount + 1);
  };

  const decrement = () => {
    setCount(prevCount => prevCount - 1);
  };

  return { count, increment, decrement };
}

export default useCounter;
// src/hooks/useCounter.test.js
import { renderHook, act } from '@testing-library/react';
import useCounter from './useCounter';

test('incrementa el contador', () => {
  const { result } = renderHook(() => useCounter());

  act(() => {
    result.current.increment();
  });

  expect(result.current.count).toBe(1);
});

Explicación:

Técnicas de pruebas avanzadas

Una vez que hayas dominado los conceptos básicos de RTL, puedes explorar técnicas de pruebas más avanzadas para mejorar la calidad y el mantenimiento de tus pruebas.

Mocks de módulos

A veces, es posible que necesites simular módulos o dependencias externas para aislar tus componentes y controlar su comportamiento durante las pruebas. Jest proporciona una potente API de mocking para este propósito.

// src/api/dataService.js
export const fetchData = async () => {
  const response = await fetch('/api/data');
  const data = await response.json();
  return data;
};
// src/components/MyComponent.js
import React, { useState, useEffect } from 'react';
import { fetchData } from '../api/dataService';

function MyComponent() {
  const [data, setData] = useState(null);

  useEffect(() => {
    fetchData().then(setData);
  }, []);

  return <div>{data}</div>;
}
// src/components/MyComponent.test.js
import { render, screen, waitFor } from '@testing-library/react';
import MyComponent from './MyComponent';
import * as dataService from '../api/dataService';

jest.mock('../api/dataService');

test('obtiene datos de la API', async () => {
  dataService.fetchData.mockResolvedValue({ message: '¡Datos simulados!' });

  render(<MyComponent />);

  await waitFor(() => screen.getByText('¡Datos simulados!'));

  expect(screen.getByText('¡Datos simulados!')).toBeInTheDocument();
  expect(dataService.fetchData).toHaveBeenCalledTimes(1);
});

Explicación:

Proveedores de contexto

Si tu componente se basa en un Proveedor de Contexto, deberás envolver tu componente en el proveedor durante las pruebas. Esto asegura que el componente tenga acceso a los valores del contexto.

// src/contexts/ThemeContext.js
import React, { createContext, useState } from 'react';

export const ThemeContext = createContext();

export function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('light');

  const toggleTheme = () => {
    setTheme(prevTheme => (prevTheme === 'light' ? 'dark' : 'light'));
  };

  return (
    <ThemeContext.Provider value={{ theme, toggleTheme }}>
      {children}
    </ThemeContext.Provider>
  );
}
// src/components/MyComponent.js
import React, { useContext } from 'react';
import { ThemeContext } from '../contexts/ThemeContext';

function MyComponent() {
  const { theme, toggleTheme } = useContext(ThemeContext);

  return (
    <div style={{ backgroundColor: theme === 'light' ? '#fff' : '#000', color: theme === 'light' ? '#000' : '#fff' }}>
      <p>Tema actual: {theme}</p>
      <button onClick={toggleTheme}>Cambiar tema</button>
    </div>
  );
}
// src/components/MyComponent.test.js
import { render, screen, fireEvent } from '@testing-library/react';
import MyComponent from './MyComponent';
import { ThemeProvider } from '../contexts/ThemeContext';

test('cambia el tema', () => {
  render(
    <ThemeProvider>
      <MyComponent />
    </ThemeProvider>
  );

  const themeParagraph = screen.getByText(/Tema actual: light/i);
  const toggleButton = screen.getByRole('button', { name: /Cambiar tema/i });

  expect(themeParagraph).toBeInTheDocument();

  fireEvent.click(toggleButton);

  expect(screen.getByText(/Tema actual: dark/i)).toBeInTheDocument();
});

Explicación:

Pruebas con Router

Al probar componentes que usan React Router, necesitarás proporcionar un contexto de Router simulado. Puedes lograr esto usando el componente `MemoryRouter` de `react-router-dom`.

// src/components/MyComponent.js
import React from 'react';
import { Link } from 'react-router-dom';

function MyComponent() {
  return (
    <div>
      <Link to="/about">Ir a la página Acerca de</Link>
    </div>
  );
}
// src/components/MyComponent.test.js
import { render, screen } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import MyComponent from './MyComponent';

test('muestra un enlace a la página Acerca de', () => {
  render(
    <MemoryRouter>
      <MyComponent />
    </MemoryRouter>
  );

  const linkElement = screen.getByRole('link', { name: /Ir a la página Acerca de/i });
  expect(linkElement).toBeInTheDocument();
  expect(linkElement).toHaveAttribute('href', '/about');
});

Explicación:

Mejores prácticas para escribir pruebas efectivas

Aquí hay algunas de las mejores prácticas a seguir al escribir pruebas con RTL:

Conclusión

React Testing Library es una herramienta poderosa para escribir pruebas efectivas, mantenibles y centradas en el usuario para tus aplicaciones React. Al seguir los principios y técnicas descritos en esta guía, puedes construir aplicaciones robustas y confiables que satisfagan las necesidades de tus usuarios. Recuerda centrarte en las pruebas desde la perspectiva del usuario, evitar probar los detalles de implementación y escribir pruebas claras y concisas. Al adoptar RTL y adoptar las mejores prácticas, puedes mejorar significativamente la calidad y el mantenimiento de tus proyectos React, independientemente de tu ubicación o de los requisitos específicos de tu audiencia global.