Gabriel Jiménez | Hace 4 días
En este artículo, aprenderemos porqué es importante probar un formulario en React. Para crear un formulario utilizaremos la librería React Hook Form, para facilitar manejar todo lo relacionado a un formulario como: validaciones, sincronización de estados. Por último, desarrollaremos un ejercicio práctico para aprender como probarlo usando React Testing Library.
Al final del artículo serás capas de entender la importancia de probar un formulario, las ventajas de usar librerías para manejar el estado de un formulario y lo más importante, a realizar pruebas sin miedo a romper funcionalidades ya desarrolladas.
Los formularios son esenciales para comunicar los servicios de una aplicación con el usuario. A que me refiero con esto, si quiero aprender ingles en Duolingo, tengo que registrarme usando su formulario.
Cuando llenamos un formulario, usualmente tenemos el flujo principal o también llamado Happy Path, donde esperamos que todo salga bien. Por otro lado, existen los flujos alternos, como su nombre lo indica son flujos que distintos que pueden ocurrir dentro del formulario.
Resumiendo, debemos enfocarnos en probar que la comunicación del frontend y el backend se realice correctamente.
Si quieres aprender más sobre este tema, te recomiendo mi artículo: Cómo probar formularios en React con React Testing Library (guía completa)
React Testing Library permite simular gran parte de lo que un usuario haría en un formulario, como escribir en campo, seleccionar una opción de una lista, hacer click en un checkbox. Por otro lado, React Hook Form nos permite crear un formulario para manejar los errores, validaciones, sincronización de data y más, de una forma más escalable.
Si quieres aprender más sobre React Hook Form y React Testing Library, te recomiendo estos artículos:
Tu primer formulario en React con React Hook Form
Qué es React Testing Library y cómo funciona con Jest
import { useForm } from "react-hook-form";
export function SimpleForm({ onSubmit }) {
// Inicializamos React Hook Form con un valor por defecto
const { register, handleSubmit } = useForm({
defaultValues: { nombre: "" }
});
return (
// handleSubmit permite procesar los datos del formulario
<form onSubmit={handleSubmit(onSubmit)}>
{/* Campo de texto sencillo */}
<input
type="text"
placeholder="Nombre"
{...register("nombre")}
/>
{/* Botón para enviar el formulario */}
<button type="submit">Guardar</button>
</form>
);
}
import { render, screen, fireEvent } from "@testing-library/react";
import { SimpleForm } from "./SimpleForm";
describe("SimpleForm", () => {
it("permite escribir en el campo de texto", () => {
// Renderizamos el componente para interactuar con él
render(<SimpleForm onSubmit={() => {}} />);
// Obtenemos el input usando su placeholder
const input = screen.getByPlaceholderText("Nombre");
// Simulamos que el usuario escribe "Gabriel"
fireEvent.change(input, { target: { value: "Gabriel" } });
// Validamos que el input tenga el valor esperado
expect(input).toHaveValue("Gabriel");
});
});
import { render, screen, fireEvent } from "@testing-library/react";
import { SimpleForm } from "./SimpleForm";
describe("SimpleForm", () => {
it("permite escribir en el campo de texto", () => {
// Renderizamos el componente para interactuar con él
render(<SimpleForm onSubmit={() => {}} />);
// Obtenemos el input usando su placeholder
const input = screen.getByPlaceholderText("Nombre");
// Simulamos que el usuario escribe "Gabriel"
fireEvent.change(input, { target: { value: "Gabriel" } });
// Validamos que el input tenga el valor esperado
expect(input).toHaveValue("Gabriel");
});
});
import { render, screen, fireEvent } from "@testing-library/react";
import { SimpleFormDate } from "./SimpleFormDate";
describe("SimpleFormDate", () => {
it("permite seleccionar una fecha", () => {
// Renderizamos el formulario para interactuar con él
render(<SimpleFormDate onSubmit={() => {}} />);
// Obtenemos el input de tipo date
const input = screen.getByPlaceholderText("Fecha");
// Simulamos seleccionar una fecha
fireEvent.change(input, { target: { value: "2025-11-19" } });
// Verificamos que el valor haya cambiado correctamente
expect(input).toHaveValue("2025-11-19");
});
});
import { useForm } from "react-hook-form";
export function SimpleFormSelect({ onSubmit }) {
// Inicializamos el formulario con un valor vacío
const { register, handleSubmit } = useForm({
defaultValues: { pais: "" }
});
return (
<form onSubmit={handleSubmit(onSubmit)}>
{/* Campo select con opciones */}
<select {...register("pais")} data-testid="select-pais">
<option value="">Selecciona un país</option>
<option value="mx">México</option>
<option value="ar">Argentina</option>
<option value="es">España</option>
</select>
<button type="submit">Guardar</button>
</form>
);
}
import { render, screen, fireEvent } from "@testing-library/react";
import { SimpleFormSelect } from "./SimpleFormSelect";
describe("SimpleFormSelect", () => {
it("permite seleccionar una opción del select", () => {
// Renderizamos el componente
render(<SimpleFormSelect onSubmit={() => {}} />);
// Obtenemos el select usando un data-testid
const select = screen.getByTestId("select-pais");
// Simulamos seleccionar "Argentina"
fireEvent.change(select, { target: { value: "ar" } });
// Validamos que el valor haya cambiado correctamente
expect(select).toHaveValue("ar");
});
});
import { useForm } from "react-hook-form";
export function SimpleFormCheckbox({ onSubmit }) {
// Inicializamos el formulario con el checkbox en false
const { register, handleSubmit } = useForm({
defaultValues: { acepto: false }
});
return (
<form onSubmit={handleSubmit(onSubmit)}>
{/* Checkbox simple */}
<input
type="checkbox"
{...register("acepto")}
data-testid="checkbox-acepto"
/>
<button type="submit">Guardar</button>
</form>
);
}
import { render, screen, fireEvent } from "@testing-library/react";
import { SimpleFormCheckbox } from "./SimpleFormCheckbox";
describe("SimpleFormCheckbox", () => {
it("permite marcar y desmarcar el checkbox", () => {
// Renderizamos el componente
render(<SimpleFormCheckbox onSubmit={() => {}} />);
// Obtenemos el checkbox
const checkbox = screen.getByTestId("checkbox-acepto");
// Simulamos marcar el checkbox
fireEvent.click(checkbox);
// Validamos que esté marcado
expect(checkbox).toBeChecked();
// Simulamos desmarcarlo
fireEvent.click(checkbox);
// Validamos que esté desmarcado
expect(checkbox).not.toBeChecked();
});
});
import { useForm } from "react-hook-form";
export function SimpleFormRadio({ onSubmit }) {
// Inicializamos el formulario con el valor vacío
const { register, handleSubmit } = useForm({
defaultValues: { genero: "" }
});
return (
<form onSubmit={handleSubmit(onSubmit)}>
{/* Radio para seleccionar género */}
<label>
<input
type="radio"
value="masculino"
{...register("genero")}
data-testid="radio-masculino"
/>
Masculino
</label>
<label>
<input
type="radio"
value="femenino"
{...register("genero")}
data-testid="radio-femenino"
/>
Femenino
</label>
<button type="submit">Guardar</button>
</form>
);
}
import { render, screen, fireEvent } from "@testing-library/react";
import { SimpleFormRadio } from "./SimpleFormRadio";
describe("SimpleFormRadio", () => {
it("permite seleccionar una opción de radio", () => {
// Renderizamos el componente
render(<SimpleFormRadio onSubmit={() => {}} />);
// Obtenemos ambos radios
const masculino = screen.getByTestId("radio-masculino");
const femenino = screen.getByTestId("radio-femenino");
// Marcamos el radio Masculino
fireEvent.click(masculino);
// Validamos que ese esté seleccionado
expect(masculino).toBeChecked();
// Validamos que el otro NO esté seleccionado
expect(femenino).not.toBeChecked();
// Ahora seleccionamos Femenino
fireEvent.click(femenino);
// Validamos que cambió correctamente
expect(femenino).toBeChecked();
expect(masculino).not.toBeChecked();
});
});
import { useForm } from "react-hook-form";
export function SimpleFormTextarea({ onSubmit }) {
// Inicializamos el formulario con un valor vacío
const { register, handleSubmit } = useForm({
defaultValues: { comentario: "" }
});
return (
<form onSubmit={handleSubmit(onSubmit)}>
{/* Campo textarea */}
<textarea
placeholder="Comentario"
{...register("comentario")}
/>
<button type="submit">Guardar</button>
</form>
);
}
import { render, screen, fireEvent } from "@testing-library/react";
import { SimpleFormTextarea } from "./SimpleFormTextarea";
describe("SimpleFormTextarea", () => {
it("permite escribir en el textarea", () => {
// Renderizamos el componente para interactuar con él
render(<SimpleFormTextarea onSubmit={() => {}} />);
// Obtenemos el textarea usando su placeholder
const textarea = screen.getByPlaceholderText("Comentario");
// Simulamos escribir texto dentro del textarea
fireEvent.change(textarea, { target: { value: "Este es un comentario" } });
// Validamos que el valor haya cambiado correctamente
expect(textarea).toHaveValue("Este es un comentario");
});
});
Cuando enviamos información al backend, esta tiene un formato definido. Nuestra tarea es asegurarnos que el formato se cumpla. Si el formato esta correcto y se procesa bien por el backend, nos devolverá una respuesta exitosa, mientras si ocurre algo mal, devuelve una respuesta fallida.
import { useForm } from "react-hook-form";
export function SimpleBackendForm({ onSubmit }) {
// Inicializamos el formulario con valores vacíos
const { register, handleSubmit } = useForm({
defaultValues: {
nombre: "",
email: "",
mensaje: ""
}
});
return (
<form onSubmit={handleSubmit(onSubmit)}>
{/* Campo: nombre */}
<input
type="text"
placeholder="Nombre"
{...register("nombre")}
/>
{/* Campo: email */}
<input
type="email"
placeholder="Email"
{...register("email")}
/>
{/* Campo: mensaje */}
<textarea
placeholder="Mensaje"
{...register("mensaje")}
/>
<button type="submit">Enviar</button>
</form>
);
}
import { render, screen, fireEvent } from "@testing-library/react";
import { SimpleBackendForm } from "./SimpleBackendForm";
describe("SimpleBackendForm", () => {
it("envía los datos correctos usando onSubmit", () => {
// Mock de la función que simula el envío al backend
const mockOnSubmit = jest.fn();
// Renderizamos el formulario con la función mock
render(<SimpleBackendForm onSubmit={mockOnSubmit} />);
// Obtenemos los campos
const nombreInput = screen.getByPlaceholderText("Nombre");
const emailInput = screen.getByPlaceholderText("Email");
const mensajeTextarea = screen.getByPlaceholderText("Mensaje");
const submitButton = screen.getByText("Enviar");
// Simulamos que el usuario escribe datos
fireEvent.change(nombreInput, { target: { value: "Gabriel" } });
fireEvent.change(emailInput, { target: { value: "[email protected]" } });
fireEvent.change(mensajeTextarea, {
target: { value: "Hola, este es un mensaje" }
});
// Simulamos enviar el formulario
fireEvent.click(submitButton);
// Validamos que onSubmit fue llamado con los datos correctos
expect(mockOnSubmit).toHaveBeenCalledWith({
nombre: "Gabriel",
email: "[email protected]",
mensaje: "Hola, este es un mensaje"
});
});
});
import { useForm } from "react-hook-form";
import { useState } from "react";
export function SimpleBackendFormSuccess({ onSubmit }) {
const [successMessage, setSuccessMessage] = useState("");
const { register, handleSubmit } = useForm({
defaultValues: {
nombre: "",
email: "",
mensaje: ""
}
});
const handleFormSubmit = async (data) => {
const response = await onSubmit(data);
// Mostramos un mensaje genérico si es el backend es exitoso.
setSuccessMessage("Creado correctamente");
};
return (
<form onSubmit={handleSubmit(handleFormSubmit)}>
<input type="text" placeholder="Nombre" {...register("nombre")} />
<input type="email" placeholder="Email" {...register("email")} />
<textarea placeholder="Mensaje" {...register("mensaje")} />
<button type="submit">Enviar</button>
{successMessage && <p>{successMessage}</p>}
</form>
);
}
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import { SimpleBackendFormSuccess } from "./SimpleBackendFormSuccess";
describe("SimpleBackendFormSuccess", () => {
it("muestra 'Creado correctamente' el backend es exitoso", async () => {
// Simulamos que el backend responde exitosamente
const mockOnSubmit = jest.fn().mockResolvedValue(true);
render(<SimpleBackendFormSuccess onSubmit={mockOnSubmit} />);
// Llenamos los campos del formulario
fireEvent.change(screen.getByPlaceholderText("Nombre"), {
target: { value: "Gabriel" }
});
fireEvent.change(screen.getByPlaceholderText("Email"), {
target: { value: "[email protected]" }
});
fireEvent.change(screen.getByPlaceholderText("Mensaje"), {
target: { value: "Hola" }
});
// Enviamos el formulario
fireEvent.click(screen.getByText("Enviar"));
// Validamos que renderizó el mensaje genérico
await waitFor(() => {
expect(screen.getByText("Creado correctamente")).toBeInTheDocument();
});
});
});
import { useForm } from "react-hook-form";
import { useState } from "react";
export function SimpleBackendFormError({ onSubmit }) {
const [errorMessage, setErrorMessage] = useState("");
const { register, handleSubmit } = useForm({
defaultValues: {
nombre: "",
email: "",
mensaje: ""
}
});
const handleFormSubmit = async (data) => {
try {
// onSubmit puede fallar, simulando un backend real
await onSubmit(data);
// NO mostramos nada aquí porque este componente es solo para error
} catch (error) {
// Mostramos mensaje genérico de error
setErrorMessage("Ocurrió un error");
}
};
return (
<form onSubmit={handleSubmit(handleFormSubmit)}>
<input type="text" placeholder="Nombre" {...register("nombre")} />
<input type="email" placeholder="Email" {...register("email")} />
<textarea placeholder="Mensaje" {...register("mensaje")} />
<button type="submit">Enviar</button>
{/* Mostramos el mensaje si el backend falló */}
{errorMessage && <p>{errorMessage}</p>}
</form>
);
}
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
import { SimpleBackendFormError } from "./SimpleBackendFormError";
describe("SimpleBackendFormError", () => {
it("muestra 'Ocurrió un error' cuando el backend falla", async () => {
// Simulamos un error del backend
const mockOnSubmit = jest.fn().mockRejectedValue(new Error("fail"));
render(<SimpleBackendFormError onSubmit={mockOnSubmit} />);
// Llenamos los campos
fireEvent.change(screen.getByPlaceholderText("Nombre"), {
target: { value: "Gabriel" }
});
fireEvent.change(screen.getByPlaceholderText("Email"), {
target: { value: "[email protected]" }
});
fireEvent.change(screen.getByPlaceholderText("Mensaje"), {
target: { value: "Hola" }
});
// Enviamos el formulario
fireEvent.click(screen.getByText("Enviar"));
// Esperamos a que aparezca el mensaje de error
await waitFor(() => {
expect(screen.getByText("Ocurrió un error")).toBeInTheDocument();
});
});
});
Es importante asegurar que los formularios funcionen como se espera, de esta manera el usuario podrá utilizar nuestros servicios correctamente. React Hook Form permite manejar los formularios de una forma más simple y sencilla. Mientras React Testing Library facilita interactuar y probar con los formularios.
Si quieres aprender a probar tus tablas como un profesional, tengo un libro enfocado únicamente en testing en React o un HUB donde hablo sobre testing en React.
HUB: Testing en React
Libro: Testing en React: Guía práctica con Jest y React Testing Library