 
                        
                    GUÍA PARA PROBAR APLICACIONES REACT CON TESTING LIBRARY
Introducción a las pruebas en React con Testing Library
Testear aplicaciones React puede parecer una tarea tediosa, pero es fundamental para garantizar la calidad del código y aumentar la confianza en los productos desarrollados. Las pruebas unitarias y de integración permiten verificar que los componentes funcionen correctamente y se comporten como se espera ante las interacciones del usuario. En este tutorial, exploraremos cómo utilizar React Testing Library junto con Jest para probar aplicaciones React de manera eficiente en ocho pasos prácticos. Desde la creación de snapshots hasta la simulación de peticiones HTTP, este artículo está diseñado para programadores que deseen dominar las pruebas en React con un enfoque profesional y basado en buenas prácticas.
React Testing Library es una biblioteca ligera creada por Kent C. Dodds que fomenta pruebas centradas en el comportamiento del DOM, simulando cómo los usuarios interactúan con la aplicación. A diferencia de otras herramientas como Enzyme, esta biblioteca se enfoca en el DOM real, lo que asegura que las pruebas sean más representativas del uso final. No se requieren conocimientos avanzados de React, pero es recomendable tener una comprensión básica de sus conceptos. A lo largo del artículo, usaremos ejemplos prácticos basados en un proyecto que puedes clonar y configurar localmente.
Para comenzar, clona el repositorio de ejemplo ejecutando el siguiente comando en tu terminal:
git clone https://github.com/ibrahima92/prep-react-testing-library-guide
Luego, instala las dependencias con Yarn o npm:
yarn
O, si prefieres npm:
npm install
Con el entorno configurado, exploremos los conceptos básicos de las pruebas antes de sumergirnos en los pasos prácticos.
Conceptos básicos de pruebas con React Testing Library
Antes de escribir pruebas, es importante entender algunos elementos clave que usaremos frecuentemente:
- it o test: Define un caso de prueba, recibiendo como parámetros el nombre del test y una función que contiene las verificaciones.
- expect: Establece la condición que el test debe cumplir, comparando un valor recibido con un matcher.
- matcher: Una función que evalúa si la condición esperada se cumple, como toMatchSnapshototoHaveTextContent.
- render: Método que renderiza un componente React para probarlo.
Por ejemplo, un test básico para tomar un snapshot de un componente podría verse así:
import React from "react";
import { render } from "@testing-library/react";
import App from "./App";
it("should take a snapshot", () => {
    const { asFragment } = render(<App />);
    expect(asFragment(<App />)).toMatchSnapshot();
});
En este código, usamos it para describir el test, render para renderizar el componente App, y expect con el matcher toMatchSnapshot para verificar que el componente coincide con un snapshot previo. El método render devuelve varias utilidades, como asFragment, que usamos para capturar el DOM renderizado.
Ahora que conocemos los fundamentos, veamos cómo aplicar React Testing Library en diferentes escenarios prácticos.
Crear un snapshot de un componente
Un snapshot captura el estado del DOM de un componente en un momento dado, lo que permite detectar cambios no deseados tras modificaciones o refactorizaciones. Este enfoque es útil para verificar la consistencia visual y estructural de un componente.
Para crear un snapshot del componente App.js, usaremos el siguiente código:
import React from "react";
import { render, cleanup } from "@testing-library/react";
import App from "./App";
afterEach(cleanup);
it("should take a snapshot", () => {
    const { asFragment } = render(<App />);
    expect(asFragment(<App />)).toMatchSnapshot();
});
En este ejemplo, importamos render y cleanup. El método cleanup, usado con afterEach, libera memoria después de cada test para evitar problemas. Luego, renderizamos el componente App y usamos asFragment para capturar su estructura, comparándola con un snapshot mediante toMatchSnapshot. Al ejecutar el test con el comando:
yarn test
Se generará una carpeta __snapshots__ con un archivo App.test.js.snap que contiene la representación del DOM:
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Take a snapshot should take a snapshot 1`] = `
<DocumentFragment>
  <div class="App">
    <h1>Testing</h1>
  </div>
</DocumentFragment>
`;
Si modificas App.js, el test fallará porque el snapshot ya no coincidirá. Para actualizarlo, ejecuta yarn test y presiona u. Esto regenerará el snapshot con los cambios.
Probar elementos del DOM
Ahora que sabemos cómo capturar snapshots, avancemos a probar elementos del DOM. Consideremos un componente TestElements.js que contiene un contador y dos botones:
import React from "react";
const TestElements = () => {
    const [counter, setCounter] = React.useState(0);
    return (
        <>
            <h1 data-testid="counter">{counter}</h1>
            <button
                data-testid="button-up"
                onClick={() => setCounter(counter + 1)}
            >
                Up
            </button>
            <button
                disabled
                data-testid="button-down"
                onClick={() => setCounter(counter - 1)}
            >
                Down
            </button>
        </>
    );
};
export default TestElements;
El atributo data-testid permite seleccionar elementos específicos en las pruebas. Vamos a escribir pruebas para verificar el valor inicial del contador y el estado de los botones:
import React from "react";
import { render, cleanup } from "@testing-library/react";
import TestElements from "./TestElements";
afterEach(cleanup);
it("should equal to 0", () => {
    const { getByTestId } = render(<TestElements />);
    expect(getByTestId("counter")).toHaveTextContent(0);
});
it("should be enabled", () => {
    const { getByTestId } = render(<TestElements />);
    expect(getByTestId("button-up")).not.toHaveAttribute("disabled");
});
it("should be disabled", () => {
    const { getByTestId } = render(<TestElements />);
    expect(getByTestId("button-down")).toBeDisabled();
});
En estas pruebas, usamos getByTestId para seleccionar elementos por su data-testid. La primera prueba verifica que el contador inicie en 0 con toHaveTextContent. Las otras dos pruebas comprueban si el botón “Up” está habilitado y el botón “Down” está deshabilitado, usando not.toHaveAttribute y toBeDisabled, respectivamente. Ejecuta yarn test para confirmar que los tests pasan.
Probar eventos en componentes
Probar eventos, como clics en botones, es esencial para asegurar que las interacciones del usuario funcionen correctamente. Consideremos el componente TestEvents.js, similar al anterior pero sin el botón “Down” deshabilitado:
import React from "react";
const TestEvents = () => {
    const [counter, setCounter] = React.useState(0);
    return (
        <>
            <h1 data-testid="counter">{counter}</h1>
            <button
                data-testid="button-up"
                onClick={() => setCounter(counter + 1)}
            >
                Up
            </button>
            <button
                data-testid="button-down"
                onClick={() => setCounter(counter - 1)}
            >
                Down
            </button>
        </>
    );
};
export default TestEvents;
Para probar los eventos de clic, escribimos las siguientes pruebas:
import React from "react";
import { render, cleanup, fireEvent } from "@testing-library/react";
import TestEvents from "./TestEvents";
afterEach(cleanup);
it("increments counter", () => {
    const { getByTestId } = render(<TestEvents />);
    fireEvent.click(getByTestId("button-up"));
    expect(getByTestId("counter")).toHaveTextContent("1");
});
it("decrements counter", () => {
    const { getByTestId } = render(<TestEvents />);
    fireEvent.click(getByTestId("button-down"));
    expect(getByTestId("counter")).toHaveTextContent("-1");
});
Aquí, usamos fireEvent.click para simular clics en los botones. La primera prueba verifica que el contador se incremente a 1 tras hacer clic en “Up”, y la segunda comprueba que se reduzca a -1 tras hacer clic en “Down”. La biblioteca React Testing Library ofrece otros métodos en fireEvent para simular eventos como cambios en inputs o teclas presionadas, lo que puedes explorar en su documentación.
Probar acciones asíncronas
Las acciones asíncronas, como peticiones HTTP o temporizadores, requieren un manejo especial debido a su naturaleza no inmediata. Analicemos el componente TestAsync.js, que incrementa un contador tras un retraso:
import React from "react";
const TestAsync = () => {
    const [counter, setCounter] = React.useState(0);
    const delayCount = () => {
        setTimeout(() => {
            setCounter(counter + 1);
        }, 500);
    };
    return (
        <>
            <h1 data-testid="counter">{counter}</h1>
            <button data-testid="button-up" onClick={delayCount}>
                Up
            </button>
            <button
                data-testid="button-down"
                onClick={() => setCounter(counter - 1)}
            >
                Down
            </button>
        </>
    );
};
export default TestAsync;
Para probar el incremento asíncrono, usamos async/await y waitForElement:
import React from "react";
import {
    render,
    cleanup,
    fireEvent,
    waitForElement,
} from "@testing-library/react";
import TestAsync from "./TestAsync";
afterEach(cleanup);
it("increments counter after 0.5s", async () => {
    const { getByTestId, getByText } = render(<TestAsync />);
    fireEvent.click(getByTestId("button-up"));
    const counter = await waitForElement(() => getByText("1"));
    expect(counter).toHaveTextContent("1");
});
En este test, usamos fireEvent.click para activar el botón “Up” y waitForElement para esperar a que el texto “1” aparezca en el DOM. El uso de async/await asegura que la prueba espere la resolución de la acción asíncrona antes de evaluar el resultado.
Probar componentes con React Redux
Probar componentes que usan React Redux requiere configurar un store para simular el estado global. Veamos el componente TestRedux.js:
import React from "react";
import { connect } from "react-redux";
const TestRedux = ({ counter, dispatch }) => {
    const increment = () => dispatch({ type: "INCREMENT" });
    const decrement = () => dispatch({ type: "DECREMENT" });
    return (
        <>
            <h1 data-testid="counter">{counter}</h1>
            <button data-testid="button-up" onClick={increment}>
                Up
            </button>
            <button data-testid="button-down" onClick={decrement}>
                Down
            </button>
        </>
    );
};
export default connect((state) => ({ counter: state.count }))(TestRedux);
Y el reducer asociado:
export const initialState = {
    count: 0,
};
export function reducer(state = initialState, action) {
    switch (action.type) {
        case "INCREMENT":
            return { count: state.count + 1 };
        case "DECREMENT":
            return { count: state.count - 1 };
        default:
            return state;
    }
}
Las pruebas para este componente son:
import React from "react";
import { createStore } from "redux";
import { Provider } from "react-redux";
import { render, cleanup, fireEvent } from "@testing-library/react";
import { initialState, reducer } from "../store/reducer";
import TestRedux from "./TestRedux";
const renderWithRedux = (
    component,
    { initialState, store = createStore(reducer, initialState) } = {}
) => {
    return {
        ...render(<Provider store={store}>{component}</Provider>),
        store,
    };
};
afterEach(cleanup);
it("checks initial state is equal to 0", () => {
    const { getByTestId } = renderWithRedux(<TestRedux />);
    expect(getByTestId("counter")).toHaveTextContent("0");
});
it("increments the counter through redux", () => {
    const { getByTestId } = renderWithRedux(<TestRedux />, {
        initialState: { count: 5 },
    });
    fireEvent.click(getByTestId("button-up"));
    expect(getByTestId("counter")).toHaveTextContent("6");
});
it("decrements the counter through redux", () => {
    const { getByTestId } = renderWithRedux(<TestRedux />, {
        initialState: { count: 100 },
    });
    fireEvent.click(getByTestId("button-down"));
    expect(getByTestId("counter")).toHaveTextContent("99");
});
Creamos una función auxiliar renderWithRedux para renderizar el componente con un store Redux. La primera prueba verifica el estado inicial, mientras que las otras dos comprueban las acciones de incremento y decremento con un estado inicial personalizado.
Probar componentes con React Context
React Context es una alternativa para gestionar estado global sin Redux. Analicemos el componente TestContext.js:
import React from "react";
export const CounterContext = React.createContext();
const CounterProvider = () => {
    const [counter, setCounter] = React.useState(0);
    const increment = () => setCounter(counter + 1);
    const decrement = () => setCounter(counter - 1);
    return (
        <CounterContext.Provider value={{ counter, increment, decrement }}>
            <Counter />
        </CounterContext.Provider>
    );
};
export const Counter = () => {
    const { counter, increment, decrement } = React.useContext(CounterContext);
    return (
        <>
            <h1 data-testid="counter">{counter}</h1>
            <button data-testid="button-up" onClick={increment}>
                Up
            </button>
            <button data-testid="button-down" onClick={decrement}>
                Down
            </button>
        </>
    );
};
export default CounterProvider;
Las pruebas para este componente son:
import React from "react";
import { render, cleanup, fireEvent } from "@testing-library/react";
import CounterProvider, { CounterContext, Counter } from "./TestContext";
const renderWithContext = (component) => {
    return {
        ...render(
            <CounterProvider value={CounterContext}>
                {component}
            </CounterProvider>
        ),
    };
};
afterEach(cleanup);
it("checks if initial state is equal to 0", () => {
    const { getByTestId } = renderWithContext(<Counter />);
    expect(getByTestId("counter")).toHaveTextContent("0");
});
it("increments the counter", () => {
    const { getByTestId } = renderWithContext(<Counter />);
    fireEvent.click(getByTestId("button-up"));
    expect(getByTestId("counter")).toHaveTextContent("1");
});
it("decrements the counter", () => {
    const { getByTestId } = renderWithContext(<Counter />);
    fireEvent.click(getByTestId("button-down"));
    expect(getByTestId("counter")).toHaveTextContent("-1");
});
Usamos renderWithContext para envolver el componente en un CounterProvider. Las pruebas verifican el estado inicial y las acciones de incremento y decremento, similares a las de Redux pero adaptadas al contexto.
Probar React Router
Probar la navegación con React Router implica simular rutas y parámetros. Veamos el componente TestRouter.js:
import React from "react";
import { Link, Route, Switch, useParams } from "react-router-dom";
const About = () => <h1>About page</h1>;
const Home = () => <h1>Home page</h1>;
const Contact = () => {
    const { name } = useParams();
    return <h1 data-testid="contact-name">{name}</h1>;
};
const TestRouter = () => {
    const name = "John Doe";
    return (
        <>
            <nav data-testid="navbar">
                <Link data-testid="home-link" to="/">
                    Home
                </Link>
                <Link data-testid="about-link" to="/about">
                    About
                </Link>
                <Link data-testid="contact-link" to={`/contact/${name}`}>
                    Contact
                </Link>
            </nav>
            <Switch>
                <Route exact path="/" component={Home} />
                <Route path="/about" component={About} />
                <Route path="/contact/:name" component={Contact} />
            </Switch>
        </>
    );
};
export default TestRouter;
Las pruebas para este componente son:
import React from "react";
import { Router } from "react-router-dom";
import { render, fireEvent } from "@testing-library/react";
import { createMemoryHistory } from "history";
import TestRouter from "./TestRouter";
const renderWithRouter = (component) => {
    const history = createMemoryHistory();
    return {
        ...render(<Router history={history}>{component}</Router>),
    };
};
it("should render the home page", () => {
    const { container, getByTestId } = renderWithRouter(<TestRouter />);
    const navbar = getByTestId("navbar");
    const link = getByTestId("home-link");
    expect(container.innerHTML).toMatch("Home page");
    expect(navbar).toContainElement(link);
});
it("should navigate to the about page", () => {
    const { container, getByTestId } = renderWithRouter(<TestRouter />);
    fireEvent.click(getByTestId("about-link"));
    expect(container.innerHTML).toMatch("About page");
});
it("should navigate to the contact page with the params", () => {
    const { container, getByTestId } = renderWithRouter(<TestRouter />);
    fireEvent.click(getByTestId("contact-link"));
    expect(container.innerHTML).toMatch("John Doe");
});
Usamos createMemoryHistory para simular la navegación y renderWithRouter para envolver el componente en un Router. Las pruebas verifican que la página inicial se renderice, que la navegación a “About” funcione y que los parámetros de ruta se manejen correctamente.
Probar peticiones HTTP con Axios
Finalmente, probemos un componente que realiza peticiones HTTP con Axios. El componente TestAxios.js es:
import React from "react";
import axios from "axios";
const TestAxios = ({ url }) => {
    const [data, setData] = React.useState();
    const fetchData = async () => {
        const response = await axios.get(url);
        setData(response.data.greeting);
    };
    return (
        <>
            <button onClick={fetchData} data-testid="fetch-data">
                Load Data
            </button>
            {data ? (
                <div data-testid="show-data">{data}</div>
            ) : (
                <h1 data-testid="loading">Loading...</h1>
            )}
        </>
    );
};
export default TestAxios;
Las pruebas para este componente son:
import React from "react";
import { render, waitForElement, fireEvent } from "@testing-library/react";
import axiosMock from "axios";
import TestAxios from "./TestAxios";
jest.mock("axios");
it("should display a loading text", () => {
    const { getByTestId } = render(<TestAxios />);
    expect(getByTestId("loading")).toHaveTextContent("Loading...");
});
it("should load and display the data", async () => {
    const url = "/greeting";
    const { getByTestId } = render(<TestAxios url={url} />);
    axiosMock.get.mockResolvedValueOnce({
        data: { greeting: "hello there" },
    });
    fireEvent.click(getByTestId("fetch-data"));
    const greetingData = await waitForElement(() => getByTestId("show-data"));
    expect(axiosMock.get).toHaveBeenCalledTimes(1);
    expect(axiosMock.get).toHaveBeenCalledWith(url);
    expect(greetingData).toHaveTextContent("hello there");
});
Aquí, usamos jest.mock('axios') para simular la petición HTTP. La primera prueba verifica el mensaje de carga inicial, mientras que la segunda simula una respuesta exitosa de Axios y comprueba que los datos se muestren correctamente tras hacer clic en el botón.
Conclusiones
Probar aplicaciones React con React Testing Library y Jest es una práctica poderosa que mejora la calidad y confiabilidad del código. A través de los ocho pasos presentados, hemos cubierto desde la creación de snapshots hasta la simulación de peticiones HTTP, pasando por pruebas de eventos, acciones asíncronas, Redux, Context y Router. Estas técnicas permiten a los desarrolladores verificar el comportamiento de sus componentes de manera que refleje el uso real de los usuarios, siguiendo las mejores prácticas de la industria. Al integrar estas pruebas en tu flujo de trabajo, podrás construir aplicaciones más robustas y mantener la confianza en los cambios y refactorizaciones. Explora la documentación oficial de React Testing Library y Jest para profundizar en matchers y métodos avanzados, y comienza a implementar estas estrategias en tus proyectos hoy mismo.
 
                                
                                 
                                
                                 
                                
                                 
                                
                                