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.
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.
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.
Algunas de las ventajas de probar los formularios son:
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.
React Testing Library es una librería para facilitar escribir pruebas en aplicaciones React. Entre las cosas destacadas tenemos:
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
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
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.
// 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")
});
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();
});
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();
});
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.
// 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",
});
});
// 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();
});
});
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