Testando hooks e estado
TL;DR
Hooks só rodam dentro de componentes React — então, para testar um custom hook isoladamente, a Testing Library dá o
renderHook, que o monta num componente mínimo e expõe seu retorno emresult.current. Qualquer atualização de estado disparada fora de um evento precisa ser envolvida emact(...)(ou usar ouser-event/waitFor, que já embrulham) — senão o React avisa “not wrapped in act(…)“. Para hooks que dependem de contexto (umuseAuthque lê um Provider), passe umwrapper. E lembre: teste o comportamento do hook (o que ele retorna e faz), não sua implementação.
O problema: um hook não roda sozinho
Você extraiu a lógica de um formulário para um custom hook useFormulario() — validação, estado, submit. Quer testá-lo direto, sem montar um componente inteiro só para exercitá-lo. Mas se você chamar useFormulario() num teste, o React explode: “Hooks can only be called inside the body of a function component”. Hooks dependem do runtime do React (o “dispatcher” que gerencia useState, useEffect); fora de um componente, não há esse runtime.
A saída ingênua — criar um componente de teste que usa o hook e inspecionar o DOM — funciona, mas é verboso e indireto. A Testing Library resolve com uma ferramenta dedicada que monta o hook num componente mínimo e te dá acesso direto ao que ele retorna.
renderHook: montar o hook isolado
import { renderHook, act } from '@testing-library/react';
import { expect, test } from 'vitest';
import { useContador } from './useContador';
test('incrementa o contador', () => {
const { result } = renderHook(() => useContador(10));
expect(result.current.valor).toBe(10); // estado inicial
act(() => {
result.current.incrementar(); // dispara a atualização
});
expect(result.current.valor).toBe(11); // result.current reflete o novo estado
});Duas peças-chave:
result.currenté o valor mais recente retornado pelo hook. Ele é atualizado a cada re-render — por isso você sempre lêresult.current.valorna hora da asserção, nunca guarda uma referência antiga (ela ficaria congelada no valor velho).renderHooktambém retornarerender(para re-renderizar com novas props e testar reação a mudanças) eunmount(para testar cleanup deuseEffect).
const { result, rerender } = renderHook(({ id }) => useUsuario(id), {
initialProps: { id: 1 },
});
rerender({ id: 2 }); // re-renderiza com nova prop; testa a reaçãoact: por que e quando
O act(...) diz ao React “estou prestes a causar uma atualização; processe-a completamente antes de eu verificar”. Sem ele, uma mudança de estado pode ficar pela metade quando a asserção roda, e o React emite o famoso aviso:
Warning: An update to X inside a test was not wrapped in act(…)
graph LR A[disparo update fora de evento] --> B{envolvido em act?} B -->|não| C["⚠ warning + estado inconsistente"] B -->|sim| D["React processa tudo<br/>→ asserção confiável"] style C fill:#D0021B,color:#fff style D fill:#4A90D9,color:#fff
Preciso de
actem todo teste? Ouvi dizer que "quase nunca".As duas coisas são verdade, e a chave é: muita coisa já embrulha
actpor você.render, os métodos douserEvent,findByewaitForjá rodam dentro deactinternamente — então em testes de componente você raramente escreveactà mão. Oactexplícito aparece sobretudo ao testar hooks comrenderHook, quando você chama uma função retornada pelo hook (result.current.incrementar()) que disparasetStatefora de um evento de usuário — aí você mesmo envolve emact. Regra: se você vê o warning “not wrapped in act”, é sinal de uma atualização de estado assíncrona ou manual que escapou; envolva-a emact, ou (melhor, se for UI) useuserEvent/waitForque já cuidam disso. Não saia salpicandoactpreventivamente.
Hooks que dependem de contexto: wrapper
Um hook como useAuth() que lê um AuthContext precisa do Provider para funcionar. O renderHook aceita um wrapper — um componente que envolve o hook:
function wrapper({ children }: { children: React.ReactNode }) {
return <AuthProvider usuario={{ nome: 'Ana' }}>{children}</AuthProvider>;
}
const { result } = renderHook(() => useAuth(), { wrapper });
expect(result.current.usuario.nome).toBe('Ana');O mesmo padrão de wrapper serve para qualquer provider (React Query, tema, i18n, store) — e é reaproveitável entre testes de hook e de componente (você pode extrair um renderComProviders que aplica todos os providers de uma vez).
Guardar
result.currentnuma variável antes da atualizaçãoO que acontece:
const valor = result.current.valor;antes doact, e depois você afirma sobrevalor— o teste vê o valor antigo, mesmo após a atualização. Por quê:result.currenté substituído a cada re-render; a variável que você guardou aponta para o snapshot anterior, congelado. A atualização criou um novocurrent, mas sua variável não o acompanha. Como evitar: sempre leiaresult.current.xna hora da asserção, depois doact. Nunca desestruture/guarde valores deresult.currentpara reusar após uma atualização.
Testando hooks em uma frase: renderHook monta o hook num componente mínimo e expõe seu retorno em result.current (sempre lido na hora, nunca guardado), atualizações manuais de estado vão dentro de act (mas render/userEvent/waitFor já embrulham), e hooks que dependem de contexto recebem um wrapper com os providers.
Em entrevista
“Hooks only run inside components, so to test a custom hook in isolation I use
renderHook, which mounts it in a minimal component and exposes its return value inresult.current. I always readresult.currentat assertion time — it’s replaced on every render, so a saved reference goes stale. State updates I trigger manually go insideact, thoughrender,userEvent, andwaitForalready wrapactfor me, so I rarely write it by hand outside hook tests. And for hooks that depend on context, like auseAuthreading a provider, I pass awrapper. As always, I test what the hook returns and does, not how it’s implemented.”
| PT | EN |
|---|---|
| Custom hook | Custom hook |
| Renderizar o hook | Render the hook |
| Valor atual | Current value |
Envolver em act | Wrap in act |
| Componente-embrulho | Wrapper component |
| Valor congelado (stale) | Stale value |
O que vem a seguir
Você cobriu lógica, componentes, rede e hooks. Uma técnica transversal merece atenção pelo tanto que é mal usada: o snapshot testing — poderoso para pegar mudanças inesperadas, perigoso quando vira ruído.
- 11 — Snapshot testing — quando usar e quando evitar.
- 08 — Testando componentes React — o teste de componente que usa os mesmos providers, como base.
Fontes
- Testing Library —
renderHook— API,result.current,rerender,wrapper. - React —
act(...)— por que e quando envolver atualizações. - Kent C. Dodds — How to test custom React hooks — testar comportamento, não implementação, de hooks.