Cómo probar los hooks en React usando Jest y React Testing Library

Gabriel Jiménez | Hace alrededor de 1 mes

El objetivo del artículo es que aprendas a probar los hooks en React, utilizando herramientas como Jest y React Testing Library.


Introducción a las pruebas en React

Hoy en día, gracias a la inteligencia artificial, agregar funcionalidades a nuestra aplicaciones React se ha vuelto más sencillo. Sin embargo, esto ha provocado que, probarlas manualmente sea lento y tedioso. Además que suelen ser aplicaciones difíciles de escalar y mantener. 


Una de las estrategias para mantener nuestras aplicaciones escalables y mantenibles en el desarrollo de software es utilizar pruebas automatizadas.


¿Qué son las pruebas?

Las pruebas automatizadas son bloques de código que se encargan de validar que otro bloque de código cumpla con lo esperado.

Si quieres saber más sobre las pruebas, te invito a leer estos dos artículos:




¿Qué son los hooks en React?

A grandes rasgos, los hooks en React son funciones que encapsulan funcionalidad específica para poder reutilizarse a través de nuestra aplicación. 


Existen diferentes tipos de hooks:


  • Hooks integrados. Estos ya vienen integrados en las librerías. Por ejemplo, React tiene los hooks useState, useEffect, useReducer, etcétera.
  • Hooks personalizados. Son creados por nosotros para satisfacer alguna necesidad muy puntual del negocio.

¿Por qué es importante probar los hooks en React?

Los hooks al ser código reutilizable en diferentes partes de nuestra aplicación. Si falla puede provocar que la experiencia del usuario sea vea afectada: Por ejemplo, no poder agregar un producto a su carrito. 


Probarlos asegura que la funcionalidad cumple con lo esperado y no más. Además, hace el código más escalable y mantenible a lo largo del tiempo.


Cómo probar hooks integrados de React

Los hooks integrados implícitamente se prueban en conjunto con la lógica de nuestro negocio. Para entenderlo mejor, veamos un ejemplo práctico de como probar un hook useState y useEffect de React.

Ejemplo práctico con useState

Como ejemplo práctico, utilizaremos el siguiente caso: como usuario quiero agregar productos a mi carrito de compras. Donde vamos a tener un estado “cart” para ir almacenando los productos.


Componente de carrito de compras

    1: import React, { useState } from "react"
    2: 
    3: const products = [
    4:   { id: 1, name: "Producto A" },
    5:   { id: 2, name: "Producto B" },
    6: ]
    7: 
    8: export function MiTiendita() {
    9:   const [cart, setCart] = useState([])
   10: 
   11:   const addToCart = (product) => {
   12:     setCart([...cart, product])
   13:   }
   14: 
   15:   return (
   16:     <div>
   17:       <h2>Productos</h2>
   18:       <ul>
   19:         {products.map((p) => (
   20:           <li key={p.id}>
   21:             {p.name}
   22:             <button onClick={() => addToCart(p)}>
   23:               Agregar {p.name}
   24:             </button>
   25:           </li>
   26:         ))}
   27:       </ul>
   28: 
   29:       <h2>Carrito</h2>
   30:       <p>Artículos en el carrito: {cart.length}</p>
   31:       <ul>
   32:         {cart.map((p) => (
   33:           <li key={p.id}>
   34:             {p.name} (ID: {p.id})
   35:           </li>
   36:         ))}
   37:       </ul>
   38:     </div>
   39:   )
   40: }
   41: 
   42: export default MiTiendita


Ahora para probar que los productos se han agregado, la prueba debería revisar que los productos se están listando correctamente en la sección Artículos en el carrito — línea 29 a 37.


Prueba del componente carrito de compras

    1: import React from "react"
    2: import { render, screen, fireEvent } from "@testing-library/react"
    3: import MiTiendita from "./MiTiendita";
    4: 
    5: describe("MiTiendita", () => {
    6:   it("agregar productos al carrito", () => {
    7:     render(<MiTiendita />)
    8: 
    9:     fireEvent.click(screen.getByText(/Agregar Producto A/i))
   10:    fireEvent.click(screen.getByText(/Agregar Producto B/i))
   11: 
   12:     expect(screen.getByText(/Artículos en el carrito: 2/i)).toBeInTheDocument()
   13:     expect(screen.getByText(/Producto A \(ID: 1\)/i)).toBeInTheDocument()
   14:     expect(screen.getByText(/Producto B \(ID: 2\)/i)).toBeInTheDocument()
   15:   })
   16: })


De esta forma, validamos el uso del hook “useState” en conjunto con la lógica de nuestro negocio.




Ejemplo práctico con useEffect

Para este caso, utilizaremos el mismo ejemplo, pero ahora la lista de productos se obtiene desde un API.


Componente de carrito de compras con listado de productos desde API

    1: import React, {useEffect, useState} from "react"
    2: 
    3: export function MiTiendita({ productosApi }) {
    4:   const [cart, setCart] = useState([])
    5:   const [products, setProducts] = useState([])
    6: 
    7:   useEffect(() => {
    8:     productosApi().then((data) => setProducts(data))
    9:   }, [productosApi])
   10: 
   11:   const addToCart = (product) => {
   12:     setCart([...cart, product])
   13:   }
   14: 
   15:   return (
   16:     <div>
   17:       <h2>Productos</h2>
   18:       <ul>
   19:         {products.map((p) => (
   20:           <li key={p.id}>
   21:             {p.name}
   22:             <button onClick={() => addToCart(p)}>
   23:               Agregar {p.name}
   24:             </button>
   25:           </li>
   26:         ))}
   27:       </ul>
   28: 
   29:       <h2>Carrito</h2>
   30:       <p>Artículos en el carrito: {cart.length}</p>
   31:       <ul>
   32:         {cart.map((p) => (
   33:           <li key={p.id}>
   34:             {p.name} (ID: {p.id})
   35:           </li>
   36:         ))}
   37:       </ul>
   38:     </div>
   39:   )
   40: }
   41: 
   42: export default MiTiendita


Para probar el comportamiento del useEffect, basta con validar que los productos se estén listando.


Prueba del componente carrito de compras con listado de productos desde API

    1: import React from "react"
    2: import { render, screen, fireEvent } from "@testing-library/react"
    3: import MiTiendita from "./MiTiendita"
    4: 
    5: describe("MiTiendita", () => {
. . .
   26:   it("listado de productos", async () => {
   27:     const productsApi = () =>
   28:       Promise.resolve([
   29:         { id: 1, name: "Producto A" },
   30:         { id: 2, name: "Producto B" },
   31:       ])
   32: 
   33:     render(<MiTiendita productsApi={productsApi} />)
   34: 
   35:     expect(await screen.findByText(/Agregar Producto A/)).toBeInTheDocument()
   36:     expect(await screen.findByText(/Agregar Producto B/)).toBeInTheDocument()
   37:   })
   38: })

Cómo probar hooks personalizados de React

Los hooks personalizados, hay que probarlos en su mayoría de forma aislada para evitar que la experiencia del usuario falle.


Estructura de un hook personalizado

Vamos a crear un hook llamado useCart, ya que también se desea poder agregar productos en otras secciones

    1: import { useState, useCallback } from "react"
    2: 
    3: export function useCart() {
    4:   const [cart, setCart] = useState([])
    5: 
    6:   const add = useCallback((product) => {
    7:     setCart((prev) => [...prev, product])
    8:   }, [])
    9: 
   10:   return { cart, add }
   11: }

Testing de un hook personalizado

Para probarlo, React Testing Library tiene una función “renderHook” encargada de facilitar las pruebas de los hooks de forma aislada.

    1: import { renderHook, act } from "@testing-library/react"
    2: import { useCart } from "./useCart"
    3: 
    4: describe("useCart", () => {
    5:   it("inicia vacío y agrega productos", () => {
    6:     const { result } = renderHook(() => useCart())
    7: 
    8:     act(() => {
    9:       result.current.add({ id: 1, name: "Producto A" })
   10:      result.current.add({ id: 2, name: "Producto B" })
   11:     })
   12: 
   13:     expect(result.current.cart).toEqual([
   14:       { id: 1, name: "Producto A" },
   15:       { id: 2, name: "Producto B" },
   16:     ])
   17:   })
   18: })


De forma muy general, “renderHook” monta el hook en un componente de pruebas “invisible” y nos devuelve un objeto “result”, donde tiene una propiedad “current” que contiene lo que retorna el hook (por ejemplo, { cart, add }).


Conclusión

Los hooks integrados implícitamente se prueban con la lógica de negocio. En cambio, los hooks personalizados, hay que probarlos de forma aislada para evitarle una mala experiencia al usuario final.

Si quieres aprender más sobre pruebas, te invito a leer mi libro: Testing en React: Guía práctica usando Jest y React Testing Library