learning-front

Nivel 8 · Calidad: que no se rompa en producción

Testing de hooks y estado

Probar custom hooks aislados con renderHook, verificar las transiciones de estado y entender act. Cuándo testear un hook por separado y cuándo a través del componente que lo usa.

En el capítulo anterior probaste componentes a través de su interfaz: renderizar y mirar la pantalla. Pero buena parte de la lógica de una app de React no vive en el JSX, sino en un custom hook: un useFavoritos, un usePaginacion, un useFiltro. Esa lógica también se prueba, y tienes dos caminos: a través de un componente que use el hook, o aislando el hook con renderHook.

renderHook: probar un hook sin componente#

renderHook monta un hook por su cuenta, sin que tengas que escribir un componente de adorno para usarlo. Te devuelve un objeto { result }, y result.current es lo que el hook devuelve ahora mismo: su estado y sus funciones.

Fíjate en el patrón: renderHook(() => useContador(3)) monta el hook, result.current.valor lee su estado, y para cambiarlo llamas a su función dentro de act(...).

act: por qué las transiciones se envuelven#

Aquí está la pieza nueva. Cuando llamas a result.current.incrementar(), estás disparando una actualización de estado de React. Y las actualizaciones de React no son instantáneas: se procesan en lote. act(...) envuelve ese cambio y espera a que React lo aplique del todo (incluidos los efectos) antes de devolverte el control.

¿Y qué pasa si te saltas act? Dos cosas, y ninguna buena. Primero, React te avisa por consola: “An update to TestComponent inside a test was not wrapped in act(…)”. Y segundo, lo importante: result.current puede quedarse con el estado viejo cuando afirmas, porque el cambio aún no se había procesado. Tu expect correría sobre el valor anterior y el test fallaría en falso (o pasaría por casualidad, que es peor). Envolver en act sincroniza tu aserción con el estado ya asentado.

El otro camino: a través del componente#

El mismo useContador podría probarse sin renderHook: montando un componente que lo use y actuando sobre él como en el capítulo anterior.

Aquí no escribes act a mano: render y user-event ya lo aplican por dentro. Por eso, probando a través del componente, casi nunca lo verás. Lo escribes tú solo cuando disparas una transición a mano (result.current.algo()) en un test de renderHook.

¿Cuándo cada uno?#

No es que uno sea correcto y el otro no: resuelven cosas distintas.

  • A través del componente cuando el hook es trivial o está pegado a una sola UI. Es lo más cercano al uso real (el principio del capítulo anterior) y, de paso, comprueba el cableado: que el componente usa el hook bien.
  • Con renderHook cuando el hook es una pieza reutilizable con lógica propia (un useFavoritos que usan varios componentes). Pruebas su contrato una vez, directo y completo, sin atarlo a ningún componente concreto: si el hook funciona, funciona en cualquiera que lo monte.

La regla práctica: en la duda, a través del componente (más realista). Saca renderHook cuando el hook se sostiene solo y quieres cubrir su lógica sin el ruido de una UI alrededor.

Pruébalo#

Toca el useContador de arriba: quita el act de uno de los tests que incrementan y mira la consola. Verás el aviso de React sobre act y, según el caso, una aserción que ya no cuadra. Vuelve a ponerlo. Esa es la señal de por qué las transiciones van envueltas.

Comprueba lo que sabes#

Pregunta 1 de 5

¿Qué te da renderHook que no tendrías probando solo componentes?

Tu turno#

Prueba el hook useFavoritos con renderHook. No toques el hook: comprueba su contrato (estado inicial y transiciones), envolviendo cada cambio en act. Cuando lo tengas (o si te atascas), despliega las soluciones y fíjate en el salto de un tier al siguiente.

Ejercicio · en esta página

Prueba el hook useFavoritos con renderHook

Tienes useFavoritos, un custom hook del Team Builder que gestiona la lista de héroes favoritos (alternar, esFavorito, limpiar). No tocas el hook: pruébalo aislado con renderHook. Comprueba el estado inicial y las transiciones, envolviendo cada cambio en act, y afirma sobre lo que el hook devuelve.

Paso 1: El contrato básico

  • Con renderHook, compruebas que el hook arranca con la lista de favoritos vacía.
  • Pruebas una transición: al marcar un héroe (dentro de act), aparece en favoritos y esFavorito lo confirma.
Ver soluciones
// Tier OK: el contrato básico del hook con renderHook: estado inicial y una transición.
import { describe, it, expect } from "vitest";
// renderHook monta el hook aislado; act envuelve las actualizaciones de estado.
import { renderHook, act } from "@testing-library/react";
import { useFavoritos } from "./useFavoritos";

describe("useFavoritos", () => {
  it("arranca con la lista vacía", () => {
    // result.current es lo que el hook devuelve ahora mismo.
    const { result } = renderHook(() => useFavoritos());
    // Estado inicial: ningún favorito.
    expect(result.current.favoritos).toEqual([]);
  });

  it("añade un héroe al marcarlo", () => {
    const { result } = renderHook(() => useFavoritos());
    // La transición va DENTRO de act: así result.current refleja el nuevo estado al afirmar.
    act(() => result.current.alternar("Genji"));
    // La lista contiene el héroe y esFavorito lo confirma.
    expect(result.current.favoritos).toEqual(["Genji"]);
    expect(result.current.esFavorito("Genji")).toBe(true);
  });
});

Por qué este nivel

  • Prueba el contrato básico con renderHook: el estado inicial (vacío) y una transición (marcar un favorito). La actualización va dentro de act, que es lo que hace que result.current refleje el nuevo estado al afirmar.
  • Su límite: solo añade. No comprueba quitar (el toggle de vuelta), ni varios favoritos, ni limpiar; ahí es donde un hook de toggle suele esconder bugs.