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.
Para este caso práctico, simularemos la creación de producto con las siguientes características:
Ya teniendo los requerimientos, podemos escribir nuestra primera 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.
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.
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;
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.
. . .
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.
. . .
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: />
. . .
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.
. . .
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.
. . .
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.
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).
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.