Cómo crear y probar un formulario con MUI usando React Testing Library

Gabriel Jiménez | Hace 22 días

Si estás comenzando y desconoces qué es MUI y cómo probar cada uno de sus componentes usando React Testing Library, te invito qué empieces revisando estos artículos:


En este artículo, vamos a crear un formulario utilizando los componentes más comunes de Material UI y lo probaremos usando React Testing Library.

Caso práctico: Cómo probar un formulario MUI usando React Testing Library

Para este caso práctico, simularemos la creación de producto con las siguientes características:


  • Los campos nombre, precio y tipo de producto son obligatorios.
  • El campo precio solo permite valores numéricos.
  • El tipo de producto es una lista de productos.
  • La lista de productos se obtiene desde un API.
  • Si el tipo de producto es “Mayor de edad”, se debe mostrar un campo para asignar la edad mínima requerida.

Ya teniendo los requerimientos, podemos escribir nuestra primera prueba “crear un producto correctamente”


Prueba: Crear un producto correctamente

En esta prueba, nos interesa poder crear un producto con los campos mínimos, esto es: nombre, precio y cualquier tipo de producto — excepto “Mayor de edad”.


Comencemos definiendo primero la prueba.


Implementación de la prueba


    1: import { render, screen, fireEvent } from "@testing-library/react";
    2: import ProductForm from "./ProductForm";
    3: 
    4: describe('ProductForm', () => {
    5:   it("crear un producto correctamente", () => {
    6:     const createProductApi = jest.fn();
    7: 
    8:     render(
    9:       <ProductForm
   10:         createProductApi={createProductApi}
   11:       />
   12:     );
   13: 
   14:     fireEvent.change(screen.getByLabelText("Nombre"), {
   15:       target: { value: "Coca" },
   16:     });
   17: 
   18:     fireEvent.change(screen.getByLabelText("Precio"), {
   19:       target: { value: "25" },
   20:     });
   21: 
   22:     fireEvent.mouseDown(screen.getByLabelText("Tipo"));
   23: 
   24:     fireEvent.click(screen.getByText("General"));
   25: 
   26:     fireEvent.click(screen.getByText("Crear"));
   27:     
   28:     expect(createProductApi).toHaveBeenCalledWith({
   29:       name: "Coca",
   30:       price: "25",
   31:       type: "General",
   32:     });
   33:   })
   34: });


Analicemos


Línea 6. Creamos un mock, para saber que data se envió al API.


Línea 8-13. Montamos nuestro componente en un navegador simulado y le pasamos el prop “createProductApi”.


Línea 14-16. Escribimos en el campo “Nombre”.


Línea 18-20. Escribimos en el campo “Precio”.


Línea 22-24. Seleccionamos la opción “General” del campo “Tipo”.


Línea 26. Hacemos clic en el botón “Crear”.


Línea 28-32. Validamos que la data enviada al API este llegando en el formato correcto.


Listo, ahora continuemos con la implementación para que la prueba pase.

Implementación para pasar la prueba


    1: import { useState } from "react";
    2: import { TextField, FormControl, InputLabel, Select, MenuItem, Button } from "@mui/material";
    3: 
    4: const ProductForm = ({ createProductApi }) => {
    5:   const [values, setValues] = useState({
    6:     name: "",
    7:     price: "",
    8:     type: "",
    9:   });
   10: 
   11:   const handleChange = (e) => {
   12:     const { name, value } = e.target;
   13:     setValues((prev) => ({ ...prev, [name]: value }));
   14:   };
   15: 
   16:   const handleSubmit = (e) => {
   17:     e.preventDefault();
   18:     createProductApi(values);
   19:   };
   20: 
   21:   return (
   22:     <form onSubmit={handleSubmit}>
   23:       <TextField label="Nombre" name="name" value={values.name} onChange={handleChange} />
   24:       <TextField label="Precio" name="price" value={values.price} onChange={handleChange} />
   25: 
   26:       <FormControl>
   27:         <InputLabel id="type-label">Tipo</InputLabel>
   28:         <Select
   29:           labelId="type-label"
   30:           label="Tipo"
   31:           name="type"
   32:           value={values.type}
   33:           onChange={handleChange}
   34:         >
   35:           {["General", "Mayor de edad"].map((t) => (
   36:             <MenuItem key={t} value={t}>
   37:               {t}
   38:             </MenuItem>
   39:           ))}
   40:         </Select>
   41:       </FormControl>
   42: 
   43:       <Button type="submit">Crear</Button>
   44:     </form>
   45:   );
   46: }
   47: 
   48: export default ProductForm;

Prueba: Mostrar mensaje de error, si el precio no tiene el formato correcto

Cuando usuario llene el formulario y el precio no sea numérico debe mostrarle un mensaje de error. En este caso, vamos a empezar primero por la prueba.


Implementación de la prueba


 . . .
    4: describe('ProductForm', () => {
. . .
   35:   it("mostrar mensaje de error, si el precio no tiene el formato correcto", () => {
   36:     const createProductApi = jest.fn();
   37: 
   38:     render(<ProductForm createProductApi={createProductApi} />);
   39: 
   40:     fireEvent.change(screen.getByLabelText("Nombre"), {
   41:       target: { value: "Coca" },
   42:     });
   43: 
   44:     fireEvent.change(screen.getByLabelText("Precio"), {
   45:       target: { value: "abc" }, // formato inválido
   46:     });
   47: 
   48:     fireEvent.mouseDown(screen.getByLabelText("Tipo"));
   49:     fireEvent.click(screen.getByText("General"));
   50: 
   51:     fireEvent.click(screen.getByText("Crear"));
   52:     
   53:     expect(screen.getByText("Precio inválido").toBeInTheDocument();
   54:     expect(createProductApi).not.toHaveBeenCalled();
   55:   });
   56: });


Analicemos


Línea 44-46. Escribimos un valor inválido para el precio.


Línea 53. Validamos que el mensaje de “Precio inválido” se le muestre al usuario.


Línea 54. Verificamos que no se envía la data al API.


Listo, continuemos con la implementación para que pase la prueba.


Implementación para pasar la prueba


. . .
    4: const ProductForm = ({ createProductApi }) => {
. . .
   11:   const [errors, setErrors] = useState({});
. . .
   18:   const handleSubmit = (e) => {
   19:     e.preventDefault();
   20: 
   21:     // validación mínima
   22:     if (isNaN(Number(values.price))) {
   23:       setErrors({ price: "Precio inválido" });
   24:       return;
   25:     }
   26: 
   27:     setErrors({});
   28:     createProductApi(values);
   29:   };
   30: 
   31:   return (
. . .
   39: 
   40:       <TextField
   41:         label="Precio"
   42:         name="price"
   43:         value={values.price}
   44:         onChange={handleChange}
   45:         error={Boolean(errors.price)}
   46:         helperText={errors.price}
   47:       />
. . .

Prueba: Si el tipo de producto es “Mayor de edad”, asignar la edad mínima requerida

Cuando el usuario selecciona la opción “Mayor de edad” de la lista de tipo de productos, debe mostrarse un campo para asignar la edad mínima requerida.


Implementación de la prueba


. . .
   57:   it('Si el tipo de producto es "Mayor de edad", asignar la edad mínima requerida', () => {
   58:     const createProductApi = jest.fn();
   59: 
   60:     render(<ProductForm createProductApi={createProductApi} />);
   61: 
   62:     fireEvent.change(screen.getByLabelText("Nombre"), {
   63:       target: { value: "Cerveza" },
   64:     });
   65: 
   66:     fireEvent.change(screen.getByLabelText("Precio"), {
   67:       target: { value: "30" },
   68:     });
   69: 
   70:     // seleccionar tipo "Mayor de edad"
   71:     fireEvent.mouseDown(screen.getByLabelText("Tipo"));
   72:     fireEvent.click(screen.getByText("Mayor de edad"));
   73: 
   74:     // debe aparecer el campo de edad mínima
   75:     const minAgeInput = screen.getByLabelText(“Edad mínima”);
   76: 
   77:     fireEvent.change(minAgeInput, {
   78:       target: { value: "18" },
   79:     });
   80: 
   81:     fireEvent.click(screen.getByText("Crear"));
   82: 
   83:     expect(createProductApi).toHaveBeenCalledWith({
   84:       name: "Cerveza",
   85:       price: “30",
   86:       type: "Mayor de edad",
   87:       minAge: "18",
   88:     });
   89:   });
. . .


Analicemos


Línea 71-72. Se selecciona la opción “Mayor de edad” del campo “Tipo”.


Línea 75-79. Escribimos la edad mínima requerida en el campo “Edad mínima”.


Línea 83-88. Validamos que se este enviando la edad mínima correcta al API.


Implementación para que la prueba pase


. . .
    4: const ProductForm = ({ createProductApi }) => {
    5:   const [values, setValues] = useState({
    6:     name: "",
. . .
    9:     minAge: "",
   10:   });
. . .
   30: 
   31:   return (
   32:     <form onSubmit={handleSubmit}>
. . .
   49:       <FormControl>
   50:         <InputLabel id="type-label">Tipo</InputLabel>
   51:         <Select
   52:           labelId="type-label"
   53:           label="Tipo"
   54:           name="type"
   55:           value={values.type}
   56:           onChange={handleChange}
   57:         >
   58:           {["General", "Mayor de edad"].map((t) => (
   59:             <MenuItem key={t} value={t}>
   60:               {t}
   61:             </MenuItem>
   62:           ))}
   63:         </Select>
   64:       </FormControl>
   65: 
   66:       {values.type === "Mayor de edad" && (
   67:         <TextField
   68:           label="Edad mínima"
   69:           name="minAge"
   70:           value={values.minAge}
   71:           onChange={handleChange}
   72:         />
   73:       )}
. . .
   77:   );
   78: };
. . .


Perfecto, si ejecutamos todas las pruebas que llevamos hasta el momento van a pasar. Sin embargo, en nuestros requerimientos la lista de productos se obtiene directamente desde el backend.


Veamos como podemos solucionarlo.


Simular respuestas del API en nuestras pruebas

Para simular una respuesta al API, debemos crear prop de tipo promesa que regresa la lista de productos. Además, al estar realizando una petición asíncrona, debemos controlar cuando termina.

. . .
    5:   it("crear un producto correctamente", async () => {
    6:     const createProductApi = jest.fn();
    7:     const productTypesApi = jest.fn().mockResolvedValue([ // Promesa que regresa lista
    8:       "General", "Mayor de edad"
    9:     ]);
   10: 
   11:     await waitFor(() => { // Controlar cuando termina de ejecutarse la promesa
   12:       render(
   13:         <ProductForm
   14:           createProductApi={createProductApi}
   15:           productTypesApi={productTypesApi}
   16:         />
   17:       );
   18:     })
. . .
   34:     expect(createProductApi).toHaveBeenCalledWith({
   35:       name: "Coca",
   36:       price: "25",
   37:       type: “General",
   39:       minAge: “”, // Al agregar el campo en el value, este se envía.
   40:     });
   41:   })


En la implementación para que la prueba pase, debemos esperar hasta que la petición asíncrona termine y después renderizar la lista.

. . .
    4: const ProductForm = ({ createProductApi, productTypesApi }) => {
    5:   const [productTypes, setProductTypes] = useState([]);
. . .
   15:   useEffect(() => {
   16:     productTypesApi().then((data) => setProductTypes(data))
   17:   }, [])
. . .
   36:   return (
   37:     <form onSubmit={handleSubmit}>
. . .
   54:       <FormControl>
   55:         <InputLabel id="type-label">Tipo</InputLabel>
   56:         <Select
   57:           labelId="type-label"
   58:           label="Tipo"
   59:           name="type"
   60:           value={values.type}
   61:           onChange={handleChange}
   62:         >
   63:           {productTypes.map((t) => (
   64:             <MenuItem key={t} value={t}>
   65:               {t}
   66:             </MenuItem>
   67:           ))}
   68:         </Select>
   69:       </FormControl>
. . .
   82:   );
   83: };
. . .


Listo, ahora cada que montemos el componente debemos controlar el termino de la promesa.


Si estas utilizando Axios para realizar tus peticiones al API, te recomiendo mi artículo Cómo probar peticiones al backend con React Testing Library (axios sin backend).


Conclusión

Una de las ventajas de realizar pruebas en nuestros formularios es tener la confianza de modificarlos sin miedo a romper la funcionalidad existente como vimos con el tema de las lista de productos.


Si quieres aprender más sobre como probar tus aplicaciones React, te recomiendo mi HUB Testing en React.


Hub Testing en React