Cómo probar formularios con Yup y React Testing Library paso a paso

Gabriel Jiménez | Hace 24 días

Cómo probar formularios con Yup y React Testing Library paso a paso

Si estás comenzando y desconoces qué es YUP y cómo se relaciona con React, te invito a revisar mi artículo Validaciones en formularios de React usando Yup.


En este artículo, aprenderemos por qué es necesario probar nuestros formularios usando React Testing Library, qué es la librería Yup y cómo se relaciona con React, las herramientas necesarias para comenzar a escribir nuestras primeras pruebas y finalmente, un caso práctico para que comiences a aplicarlo en tus proyectos.


Por qué debería probar mis formularios con React Testing Library

Los formularios son un componente UI muy utilizado en las aplicaciones frontend, generalmente, permiten comunicar los servicios de nuestras aplicaciones con el usuario final. 


A veces los tenemos con pocos campos. Sin embargo, cuando comienzan a crecer, el mantenimiento suele complicarse debido a:


  • Dependencia de entornos locales para funcionar
  • Reglas de negocio
  • Validaciones que depende de otras validaciones
  • Falta de pruebas

Qué es la librería Yup y cómo se relaciona con React

Es una librería que permite validar datos. Además, también permite transformar datos. Por ejemplo: 


Necesito validar que un campo tenga el formato correcto de un correo, pero antes, elimina espacios blancos al inicio y al final.


A nivel código esto se vería así:

let userSchema = object({
  email: string()
    .trim()        // elimina espacios al inicio y final
    .email()
    .required(),
});


Yup se relaciona con React porque se utiliza para validar campos en los formularios, pero puede ser utilizado en cualquier otro contexto. Más adelante, mostraremos dos ejemplos.


Herramientas para comenzar a escribir mis pruebas

Para comenzar a escribir nuestras primeras pruebas en React, necesitamos un entorno de pruebas.


Jest

Jest permite escribir, ejecutar y evaluar nuestras pruebas. 


Si quieres saber más sobre Jest, visita mi artículo Qué es Jest y cómo funciona con React


React Testing Library

Esta librería facilita funciones probar nuestros componentes React.


Si te interesa saber más sobre React Testing Library, tengo un artículo completo Qué es React Testing Library y cómo funciona con Jest


Caso práctico 1. Probar formularios con Yup y React Testing Library

Para nuestros caso práctico, utilizaremos un formulario bastante sencillo con validaciones sin la librería Yup, para después contrastarlo con el uso de Yup.


Formulario sin Yup


Supongamos que tenemos un formulario para registrar productos — nombre, precio, tipo de producto, descripción y nombre del cliente — con las siguientes validaciones:


Ahora, agreguemos las siguientes validaciones:


  • Todos los campos son obligatorios excepto la descripción.
  • El precio debe ser de tipo numérico, mayor a 0.
  • El precio debe permitir dos decimales.

Nuestro componente se vería más o menos así:

import { useState } from "react";

export default function ProductForm({ createProductApi }) {
  const [form, setForm] = useState({
    name: "",
    price: "",
    type: "",
    description: "",
    customerName: "",
  });

  const [errors, setErrors] = useState({});

  const handleChange = (e) => {
    const { name, value } = e.target;
    setForm((prev) => ({ ...prev, [name]: value }));
  };

  const validate = () => {
    const newErrors = {};

    if (!form.name.trim()) newErrors.name = "El nombre es obligatorio";
    if (!form.type.trim()) newErrors.type = "El tipo es obligatorio";
    if (!form.customerName.trim())
      newErrors.customerName = "El cliente es obligatorio";

    if (!form.price.trim()) {
      newErrors.price = "El precio es obligatorio";
    } else {
      const numericValue = Number(form.price);

      if (isNaN(numericValue) || numericValue <= 0) {
        newErrors.price = "El precio debe ser mayor a 0";
      }

      const decimalRegex = /^\d+(\.\d{1,2})?$/;
      if (!decimalRegex.test(form.price)) {
        newErrors.price =
          "El precio debe tener máximo dos decimales";
      }
    }

    setErrors(newErrors);
    return Object.keys(newErrors).length === 0;
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    if (!validate()) return;

    createProductApi(form);
  };

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label htmlFor="name">Nombre del producto</label>
        <input id="name" name="name" value={form.name} onChange={handleChange} />
        {errors.name && <p>{errors.name}</p>}
      </div>

      <div>
        <label htmlFor="price">Precio</label>
        <input id="price" name="price" value={form.price} onChange={handleChange} />
        {errors.price && <p>{errors.price}</p>}
      </div>

      <div>
        <label htmlFor="type">Tipo de producto</label>
        <input id="type" name="type" value={form.type} onChange={handleChange} />
        {errors.type && <p>{errors.type}</p>}
      </div>

      <div>
        <label htmlFor="description">Descripción</label>
        <textarea
          id="description"
          name="description"
          value={form.description}
          onChange={handleChange}
        />
      </div>

      <div>
        <label htmlFor="customerName">Nombre del cliente</label>
        <input
          id="customerName"
          name="customerName"
          value={form.customerName}
          onChange={handleChange}
        />
        {errors.customerName && <p>{errors.customerName}</p>}
      </div>

      <button type="submit">Guardar</button>
    </form>
  );
}


Listo, ahora en lugar de usar nuestro ambiente local, usemos nuestro ambiente de pruebas con React Testing Library y Jest.


Probando formulario usando React Testing Library y Jest


import { render, screen, fireEvent } from "@testing-library/react";
import ProductForm from "./ProductForm";

describe("ProductForm", () => {
  test("envía el formulario correctamente", () => {
    const createProductApi = jest.fn();

    render(<ProductForm createProductApi={createProductApi} />);

    fireEvent.change(screen.getByLabelText(/nombre del producto/i), {
      target: { value: "Café" },
    });

    fireEvent.change(screen.getByLabelText(/precio/i), {
      target: { value: "10.50" },
    });

    fireEvent.change(screen.getByLabelText(/tipo de producto/i), {
      target: { value: "Bebida" },
    });

    fireEvent.change(screen.getByLabelText(/nombre del cliente/i), {
      target: { value: "Gabriel" },
    });

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

    expect(createProductApi).toHaveBeenCalledWith(
      {
        name: "Café",
        price: "10.50",
        type: "Bebida",
        customerName: "Gabriel",
        description: ""
      }
    );
  });

  test("muestra errores cuando el formulario es inválido", () => {
    render(<ProductForm />);

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

    expect(
      screen.getByText(/el nombre es obligatorio/i)
    ).toBeInTheDocument();

    expect(
      screen.getByText(/el precio es obligatorio/i)
    ).toBeInTheDocument();

    expect(
      screen.getByText(/el tipo es obligatorio/i)
    ).toBeInTheDocument();

    expect(
      screen.getByText(/el cliente es obligatorio/i)
    ).toBeInTheDocument();
  });
});

Análisis de pruebas


Primera prueba “envía el formulario correctamente”

Estamos validando que el objeto venga con las propiedades correctas así como su data.


Segunda prueba “muestra errores cuando el formulario es inválido”

Aquí estamos validando las validaciones de cada uno de los campos. Es importante validar que el mensaje de error se le muestre al usuario y no validarlo como en la primera prueba.


Formulario con Yup

Cuando los formularios comienzan a crecer empezamos a tener código repetido, ya sea de una funcionalidad en particular o de validaciones en campos que se repiten en diferentes formularios. Las validaciones pueden ser tan simples hasta complejas y ahí es donde entra Yup al rescate.


Veamos el formulario que creamos anteriormente pero ahora usando Yup:

import { useState } from "react";
import * as yup from "yup";

const schema = yup.object({
  name: yup.string().trim().required("El nombre es obligatorio"),
  type: yup.string().trim().required("El tipo es obligatorio"),
  customerName: yup.string().trim().required("El cliente es obligatorio"),

  // price llega como string del input
  price: yup
    .string()
    .trim()
    .required("El precio es obligatorio")
    .matches(/^\d+(\.\d{1,2})?$/, "El precio debe tener máximo dos decimales")
    .test("gt-zero", "El precio debe ser mayor a 0", (value) => {
      if (!value) return false;
      const n = Number(value);
      return !Number.isNaN(n) && n > 0;
    }),
  description: yup.string().trim().notRequired(),
});

export default function ProductForm({ createProductApi }) {
  const [form, setForm] = useState({
    name: "",
    price: "",
    type: "",
    description: "",
    customerName: "",
  });

  const [errors, setErrors] = useState({});

  const handleChange = (e) => {
    const { name, value } = e.target;
    setForm((prev) => ({ ...prev, [name]: value }));
  };

  const validate = async () => {
    try {
      await schema.validate(form, { abortEarly: false });
      setErrors({});
      return true;
    } catch (err) {
      const newErrors = {};
      // err.inner trae todos los errores cuando abortEarly: false
      for (const e of err.inner || []) {
        if (!newErrors[e.path]) newErrors[e.path] = e.message; // 1er error por campo
      }
      setErrors(newErrors);
      return false;
    }
  };

  const handleSubmit = async (e) => {
    e.preventDefault();
    if (!(await validate())) return;

    createProductApi(form);
  };

  return (
    <form onSubmit={handleSubmit}>
      <div>
        <label htmlFor="name">Nombre del producto</label>
        <input id="name" name="name" value={form.name} onChange={handleChange} />
        {errors.name && <p>{errors.name}</p>}
      </div>

      <div>
        <label htmlFor="price">Precio</label>
        <input id="price" name="price" value={form.price} onChange={handleChange} />
        {errors.price && <p>{errors.price}</p>}
      </div>

      <div>
        <label htmlFor="type">Tipo de producto</label>
        <input id="type" name="type" value={form.type} onChange={handleChange} />
        {errors.type && <p>{errors.type}</p>}
      </div>

      <div>
        <label htmlFor="description">Descripción</label>
        <textarea
          id="description"
          name="description"
          value={form.description}
          onChange={handleChange}
        />
      </div>

      <div>
        <label htmlFor="customerName">Nombre del cliente</label>
        <input
          id="customerName"
          name="customerName"
          value={form.customerName}
          onChange={handleChange}
        />
        {errors.customerName && <p>{errors.customerName}</p>}
      </div>

      <button type="submit">Guardar</button>
    </form>
  );
}


Bastante sencillo, creamos un objeto de tipo “yup” para validar cada uno de los campos de nuestro estado interno “form”.


Pruebas usando Yup y React Testing Library

Para probar el formulario usando Yup es necesario usar la función “waitFor” que permite esperar a que las peticiones asíncronas hayan terminado. En nuestro caso, esa petición asíncrona se da en la función “validate”.

const validate = async () => {
  try {
    await schema.validate(form, { abortEarly: false }); // Petición asincrona.
    setErrors({});
    return true;
  } catch (err) {
. . .
  }
};


Listo, ya que sabemos esto, así quedan nuestras pruebas:

import {render, screen, fireEvent, waitFor} from "@testing-library/react";
import ProductForm from "./ProductForm";

describe("ProductForm", () => {
  test("envía el formulario correctamente", async () => {
. . .

    await waitFor(() => {
      expect(createProductApi).toHaveBeenCalledWith(
        {
          name: "Café",
          price: "10.50",
          type: "Bebida",
          customerName: "Gabriel",
          description: ""
        }
      );
    })
  });

  test("muestra errores cuando el formulario es inválido", async () => {
. . .

    await waitFor(() => {
      expect(
        screen.getByText(/el nombre es obligatorio/i)
      ).toBeInTheDocument();
    })
. . .
  });
});

Conclusión

Yup es una librería muy utilizada para validar los campos de los formularios, es importante tener en cuenta cuando las probamos, que esas validaciones se le estén mostrando correctamente al usuario, ya sea haciendo pruebas manuales o pruebas automatizadas, si tuviera que elegir, por su puesto, elegiría las pruebas automatizadas para formularios más complejos.


Si estás interesado en aprender más sobre Testing en React, te invito a visitar mi hub de Testing en React, donde encontrarás todo lo necesario para adentrarte en el mundo de las pruebas.


Hub de Testing en React