Cómo probar formularios en React con React Testing Library (guía completa)

Gabriel Jiménez | Hace 5 días

En está guía encontrarás todo lo necesario para adentrarte en el mundo de testing en React. Aprenderemos porque es importante probar los formularios, las herramientas que se necesitan para ejecutar las pruebas. Veremos algunos ejemplos de cómo probar las interacciones de un usuario como: llenado de campos de tipo texto, select. También, probaremos validaciones tanto del cliente como del backend. Por último, validaremos todo lo relacionado al envío de data al backend.


Por qué es importante probar formularios en React

Los formularios son elementos en la web muy utilizados para comunicarnos con los servicios de una aplicación. Estos pueden ser tan simples o complejos. Generalmente, se componen de campos de tipo texto, seleccionadores, checkboxes, entre otros.


Al ser elementos que permiten usar los servicios de las aplicaciones debemos de saber que deberíamos probar y cuales son las ventajas de hacerlo.


Qué debería probar en un formulario

Lo principal que debemos probar en un formulario es que, la data se envíe con la estructura y formato correcto.


Segundo, validar cada uno de los flujos que el usuario pueda realizar. Por ejemplo, el happy path y los flujos alternos.


Ventajas de probar tus formularios

Algunas de las ventajas de probar los formularios son:


  1. Asegurar que la data se envíe con la estructura y formato.
  2. Permite hacer cambios sin miedo a romper algo existente.
  3. Agiliza el desarrollo.
  4. Sirve como documentación para que otros entienda mejor el código.
  5. No depende de levantar el backend.
  6. Permite probar los diferentes escenarios fácilmente.

Preparar el entorno de testing

Un entorno de testing permite escribir, ejecutar y evaluar las pruebas de una forma sencilla. En el mundo de testing en React, existen dos muy utilizadas: Jest y React Testing Library.


Qué es React Testing Library

React Testing Library es una librería para facilitar escribir pruebas en aplicaciones React. Entre las cosas destacadas tenemos:


  • Permite renderizar un componente.
  • Permite seleccionar elementos dentro de un componente.
  • Podemos ejecutar interacciones en los campos como: escribir en un campo.
  • Facilita la sincronización del estado interno del componente.
  • Trabaja perfectamente con peticiones asincronadas.

Si quieres indagar más sobre React Testing Library, tengo un artículo dedicado a ello: Qué es React Testing Library y cómo funciona con Jest


Qué es Jest y cómo funciona con React Testing Library

Jest es un framework para escribir, ejecutar y evaluar pruebas en Javascript. Mientras que, React Testing Library solo una librería para poder trabajar más cómodamente nuestras pruebas en React.


Si quieres aprender más sobre Jest, te invito a leer mi artículo sobre: Qué es Jest y cómo funciona con React


Cómo probar inputs e interacciones del usuario usando React Testing Library

React Testing Library permite poder probar las interacciones que un usuario tendría en un formulario, como escribir en un input de tipo texto, seleccionar una opción dentro de un catálogo, mediante el uso de los “queries”. Además, al momento de validar cada campo, en ocasiones también nos interesa validar su formato — a esto le llamamos validaciones del lado del frontend.


Escribir pruebas para inputs de tipo texto


// Escenario
<input type=“text” placeholder=“Nombre />

it('validar campo de tipo texto', () => {
  // renderizamos el componente para poder interactuar con el
  render(<MiComponent />)
  
  // Buscamos dentro del componente un input con el placeholder nombre
  const campoNombre = screen.getByPlaceholderText(“Nombre")

  // Escribimos en el campo el valor de “Gabriel”
  fireEvent.change(campoNombre, { target: { value: "Gabriel" } });

   // Validamos que el campo nombre tenga el valor “Gabriel”
  expect(campoNombre).toHaveValue("Gabriel")
});

Probar campos de tipo select, radiobutton y checkbox


Validar select

// Escenario
 <select>
  <option value="">Seleccionar...</option>
  <option value="mx">México</option>
  <option value="us">USA</option>
</select>

it('valida un campo select', () => {
  render(<MiComponent />);
  
  // Seleccionamos el campo select
 // Automáticamente React Testing Library, sabe que el role del select es combobox
  const selectPais = screen.getByRole('combobox');

  // Seleccionamos la opción “mx”
  fireEvent.change(selectPais, { target: { value: 'mx' } });
  
  // Validamos que la opción seleccionada sea “mx”
  expect(selectPais).toHaveValue('mx');
});


Validar checkbox

// Escenario
<input type="checkbox" name="acepto_terminos" />


it('valida un checkbox', () => {
  render(<MiComponent />);

  // Selecciona el checkbox
  const checkbox = screen.getByRole('checkbox');

  // Hacemos clic en el checkbox
  fireEvent.click(checkbox);

  // Validamos que el checkbox se haya seleccionado
  expect(checkbox).toBeChecked();

  // Desmarcarlo
  fireEvent.click(checkbox);
  
   // Para validar que no este seleccionado negamos el “toBeChecked” con “.not”
  expect(checkbox).not.toBeChecked();
});


Validar radiobutton

// Escenario
<input type="radio" name="genero" value="hombre" />
<input type="radio" name="genero" value="mujer" />

it('valida un radiobutton', () => {
  render(<MiComponent />);

  // Seleccionamos los radio buttons
  const radioHombre = screen.getByDisplayValue("hombre");
  const radioMujer  = screen.getByDisplayValue("mujer");

  // Hacemos clic en la opción mujer
  fireEvent.click(radioMujer);
  
  // Validamos que la opción mujer este seleccionado, mientras la opción hombre no se encuentre seleccionada.
  expect(radioMujer).toBeChecked();
  expect(radioHombre).not.toBeChecked();
});

Probar validaciones del lado del cliente

Las validaciones del lado del cliente ocurren cuando el usuario interactúa con alguno campo dentro del formulario. Una validación muy común es: validar qué el usuario escriba un correo con un formato correcto.

// MiComponent.jsx
import { useState } from "react";

export default function MiComponent() {
  const [email, setEmail] = useState("");
  const [error, setError] = useState("");

  // Validación muy básica
  const validarEmail = (valor) => {
    const regex = /\S+@\S+\.\S+/;  // cualquier [email protected]
    return regex.test(valor);
  };

  const handleChange = (e) => {
    const valor = e.target.value;
    setEmail(valor);

    // Si el formato no es correcto, guardamos un mensaje simple
    if (!validarEmail(valor)) {
      setError("Formato de correo inválido");
    } else {
      setError("");
    }
  };

  return (
    <div>
      <input
        type="text"
        placeholder="Correo"
        value={email}
        onChange={handleChange}
      />

      {error && <p>{error}</p>}
    </div>
  );
}

// Prueba

it('muestra error cuando el correo no es válido', () => {
  render(<MiComponent />);

  const campoCorreo = screen.getByPlaceholderText("Correo");

  fireEvent.change(campoCorreo, { target: { value: "correo_malo" } });

  // Validamos que tenga el valor ingresado
  expect(campoCorreo).toHaveValue("correo_malo");

  // Debe aparecer el mensaje de error
  expect(screen.getByText("Formato de correo inválido")).toBeInTheDocument();
});

Cómo probar el envió del formulario al backend

Como sabemos un formulario se puede componer de varios inputs de diferente tipo. En lugar de probar campo por campo, validemos la data que enviamos al backend como un todo. Además, hay situaciones donde al enviar la data al backend puede respondernos con una respuesta exitosa o fallida, dependiendo de esta respuesta, el usuario debe ser notificado.

Validar si la data se envió correctamente al backend


// MiComponent.jsx
import { useState } from "react";
import { saveUser } from "./api";

export default function MiComponent() {
  const [nombre, setNombre] = useState("");
  const [correo, setCorreo] = useState("");
  const [password, setPassword] = useState("");

  const handleSubmit = () => {
    // Enviamos todos los datos al backend
    saveUser({
      nombre,
      email: correo,
      password,
    });
  };

  return (
    <div>
      <input
        type="text"
        placeholder="Nombre"
        value={nombre}
        onChange={(e) => setNombre(e.target.value)}
      />

      <input
        type="text"
        placeholder="Correo"
        value={correo}
        onChange={(e) => setCorreo(e.target.value)}
      />

      <input
        type="password"
        placeholder="Contraseña"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
      />

      <button onClick={handleSubmit}>
        Guardar
      </button>
    </div>
  );
}

// api.js
export function saveUser(data) {
  return fetch("/api/users", {
    method: "POST",
    body: JSON.stringify(data),
  });
}

import { render, screen, fireEvent } from "@testing-library/react";
import MiComponent from "./MiComponent";
import * as api from "./api";

it("envía nombre, correo y contraseña correctamente al backend", () => {
  // Mockeamos la función saveUser
  const mock = jest.spyOn(api, "saveUser").mockImplementation(jest.fn());

  render(<MiComponent />);

  // Inputs
  const campoNombre = screen.getByPlaceholderText("Nombre");
  const campoCorreo = screen.getByPlaceholderText("Correo");
  const campoPassword = screen.getByPlaceholderText("Contraseña");

  // Simulamos escribir en cada campo
  fireEvent.change(campoNombre, { target: { value: "Gabriel" } });
  fireEvent.change(campoCorreo, { target: { value: "[email protected]" } });
  fireEvent.change(campoPassword, { target: { value: "123456" } });

  // Click en "Guardar"
  fireEvent.click(screen.getByRole("button", { name: /guardar/i }));

  // Validamos que el backend recibiera exactamente esta data
  expect(mock).toHaveBeenCalledWith({
    nombre: "Gabriel",
    email: "[email protected]",
    password: "123456",
  });
});

Cómo probar si el backend devuelve una respuesta exitosa o fallida


// MiComponent.jsx
import { useState } from "react";
import { saveUser } from "./api";

export default function MiComponent() {
  const [nombre, setNombre] = useState("");
  const [correo, setCorreo] = useState("");
  const [password, setPassword] = useState("");

  const [mensaje, setMensaje] = useState(""); // ← mensaje para mostrar éxito/error

  const handleSubmit = async () => {
    try {
      const respuesta = await saveUser({
        nombre,
        email: correo,
        password,
      });

      // respuesta = { ok: true } o { ok: false, error: "mensaje" }
      if (respuesta.ok) {
        setMensaje("Registro exitoso");
      } else {
        setMensaje(respuesta.error);
      }
    } catch {
      setMensaje("Error inesperado");
    }
  };

  return (
    <div>
      <input
        type="text"
        placeholder="Nombre"
        value={nombre}
        onChange={(e) => setNombre(e.target.value)}
      />

      <input
        type="text"
        placeholder="Correo"
        value={correo}
        onChange={(e) => setCorreo(e.target.value)}
      />

      <input
        type="password"
        placeholder="Contraseña"
        value={password}
        onChange={(e) => setPassword(e.target.value)}
      />

      <button onClick={handleSubmit}>Guardar</button>

      {mensaje && <p>{mensaje}</p>}
    </div>
  );
}


Prueba de respuesta exitosa

import {
  render,
  screen,
  fireEvent,
  waitFor
} from "@testing-library/react";
import MiComponent from "./MiComponent";
import * as api from "./api";

it("muestra mensaje de éxito cuando el backend valida correctamente", async () => {
  // Mock del backend: validación exitosa
  jest.spyOn(api, "saveUser").mockResolvedValue({ ok: true });

  render(<MiComponent />);

  // Inputs
  fireEvent.change(screen.getByPlaceholderText("Nombre"), {
    target: { value: "Gabriel" }
  });

  fireEvent.change(screen.getByPlaceholderText("Correo"), {
    target: { value: "[email protected]" }
  });

  fireEvent.change(screen.getByPlaceholderText("Contraseña"), {
    target: { value: "123456" }
  });

  // Click en Guardar
  fireEvent.click(screen.getByRole("button", { name: /guardar/i }));

  // Esperamos el mensaje de éxito
  await waitFor(() => {
    expect(screen.getByText("Registro exitoso")).toBeInTheDocument();
  });
});


Prueba de respuesta fallida

it("muestra mensaje de error cuando el backend responde que el usuario ya existe", async () => {
  // Mock del backend: usuario duplicado
  jest.spyOn(api, "saveUser").mockResolvedValue({
    ok: false,
    error: "El usuario ya existe"
  });

  render(<MiComponent />);

  // Inputs
  fireEvent.change(screen.getByPlaceholderText("Nombre"), {
    target: { value: "Gabriel" }
  });

  fireEvent.change(screen.getByPlaceholderText("Correo"), {
    target: { value: "[email protected]" }
  });

  fireEvent.change(screen.getByPlaceholderText("Contraseña"), {
    target: { value: "123456" }
  });

  // Click en Guardar
  fireEvent.click(screen.getByRole("button", { name: /guardar/i }));

  // Esperamos el mensaje de error
  await waitFor(() => {
    expect(screen.getByText("El usuario ya existe")).toBeInTheDocument();
  });
});

Conclusiones

Probar los formularios debe de ser una buena práctica en el desarrollo de software en el frontend, ya que son parte fundamental para comunicar nuestros servicios con el cliente.


Si quieres aprender a probar tus componentes como un profesional, tengo un libro enfocado únicamente en testing en React o un HUB donde hablo únicamente sobre testing en React.


HUB: Testing en React
Libro: Testing en React: Guía práctica con Jest y React Testing Library