<h2>Por que Testar Hooks Customizados?</h2>
<p>Hooks customizados são um dos pilares da arquitetura moderna em React. Eles encapsulam lógica complexa de estado, efeitos colaterais e comportamentos reutilizáveis, permitindo que você compartilhe esse código entre componentes sem duplicação. No entanto, muitos desenvolvedores cometem o erro de testar hooks apenas através dos componentes que os utilizam, o que resulta em testes frágeis, lentos e difíceis de manter.</p>
<p>Testar hooks customizados de forma isolada oferece vários benefícios concretos: você consegue validar a lógica do hook independentemente de qualquer interface visual, os testes executam mais rápido, e você obtém feedback mais claro quando algo quebra. A biblioteca <code>@testing-library/react</code> fornece duas ferramentas específicas para isso: <code>renderHook</code> e <code>act</code>. Essas ferramentas não apenas facilitam o teste, como também o tornam mais intuitivo e alinhado com como os hooks realmente funcionam.</p>
<h2>Entendendo renderHook e act</h2>
<h3>O que é renderHook?</h3>
<p><code>renderHook</code> é uma função utilitária que permite você renderizar um hook customizado em um ambiente de teste isolado. Diferente de renderizar um componente inteiro, <code>renderHook</code> cria um componente wrapper invisível especificamente para executar seu hook. O retorno de <code>renderHook</code> é um objeto que contém a propriedade <code>result</code>, onde você acessa o valor retornado pelo hook, além de outras utilidades como <code>rerender</code> e <code>unmount</code>.</p>
<p>Sem <code>renderHook</code>, você seria forçado a criar um componente de teste apenas para chamar seu hook, o que complicaria significativamente seus testes e misturaria responsabilidades. Com <code>renderHook</code>, você testa o hook de forma limpa e direta, focando apenas na lógica que importa.</p>
<h3>O que é act?</h3>
<p><code>act</code> é uma função que envolve qualquer ação que cause mudança de estado no seu hook. Quando você chama uma função de estado, simula um evento do usuário, ou aguarda uma promise, essas ações devem estar dentro de um <code>act</code>. Isso garante que o React aplique todas as atualizações de estado e execute todos os efeitos antes de você fazer suas asserções no teste.</p>
<p>Sem <code>act</code>, você pode ter comportamentos impredizíveis onde o estado não foi atualizado no tempo esperado, resultando em testes que passam ou falham aleatoriamente. O <code>act</code> sincroniza seu teste com o ciclo de vida real do React.</p>
<h2>Estrutura Básica e Primeiro Teste</h2>
<h3>Instalação e Importações</h3>
<p>Para começar, você precisa das dependências corretas instaladas:</p>
<pre><code class="language-bash">npm install --save-dev @testing-library/react @testing-library/jest-dom</code></pre>
<p>Agora vamos criar nosso primeiro hook customizado simples. Imagine um hook que gerencia um contador:</p>
<pre><code class="language-javascript">// useCounter.js
import { useState } from 'react';
export function useCounter(initialValue = 0) {
const [count, setCount] = useState(initialValue);
const increment = () => setCount(c => c + 1);
const decrement = () => setCount(c => c - 1);
const reset = () => setCount(initialValue);
return { count, increment, decrement, reset };
}</code></pre>
<h3>Primeiro Teste Funcional</h3>
<p>Agora vamos testar este hook usando <code>renderHook</code> e <code>act</code>:</p>
<pre><code class="language-javascript">// useCounter.test.js
import { renderHook, act } from '@testing-library/react';
import { useCounter } from './useCounter';
describe('useCounter', () => {
it('deve inicializar com o valor padrão', () => {
const { result } = renderHook(() => useCounter());
expect(result.current.count).toBe(0);
});
it('deve inicializar com um valor customizado', () => {
const { result } = renderHook(() => useCounter(10));
expect(result.current.count).toBe(10);
});
it('deve incrementar o contador', () => {
const { result } = renderHook(() => useCounter());
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
it('deve decrementar o contador', () => {
const { result } = renderHook(() => useCounter());
act(() => {
result.current.decrement();
});
expect(result.current.count).toBe(-1);
});
it('deve resetar o contador', () => {
const { result } = renderHook(() => useCounter(5));
act(() => {
result.current.increment();
result.current.increment();
});
expect(result.current.count).toBe(7);
act(() => {
result.current.reset();
});
expect(result.current.count).toBe(5);
});
});</code></pre>
<p>Observe que toda mudança de estado está envolvida em <code>act()</code>. Mesmo que seja apenas uma chamada a <code>increment()</code>, essa ação modifica o estado interno do hook, portanto deve estar dentro de <code>act</code>. O <code>result.current</code> sempre reflete o estado mais recente do hook após cada <code>act</code>.</p>
<h2>Testando Hooks com Efeitos Colaterais</h2>
<h3>Hooks que Dependem de useEffect</h3>
<p>Muitos hooks precisam lidar com efeitos colaterais como requisições HTTP, inscrições em eventos ou manipulação de temporizadores. Testar esses comportamentos requer cuidado especial. Vamos criar um hook que busca dados de uma API:</p>
<pre><code class="language-javascript">// useFetchUser.js
import { useState, useEffect } from 'react';
export function useFetchUser(userId) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let mounted = true;
const fetchUser = async () => {
try {
setLoading(true);
const response = await fetch(/api/users/${userId});
if (!response.ok) throw new Error('Erro ao buscar usuário');
const data = await response.json();
if (mounted) {
setUser(data);
setError(null);
}
} catch (err) {
if (mounted) {
setError(err.message);
setUser(null);
}
} finally {
if (mounted) {
setLoading(false);
}
}
};
fetchUser();
return () => {
mounted = false;
};
}, [userId]);
return { user, loading, error };
}</code></pre>
<h3>Testando com Mock de API</h3>
<p>Para testar este hook, precisamos mockar a função <code>fetch</code> e gerenciar as promises corretamente usando <code>act</code> e <code>waitFor</code>:</p>
<pre><code class="language-javascript">// useFetchUser.test.js
import { renderHook, act, waitFor } from '@testing-library/react';
import { useFetchUser } from './useFetchUser';
global.fetch = jest.fn();
describe('useFetchUser', () => {
beforeEach(() => {
fetch.mockClear();
});
it('deve estar em estado de carregamento inicialmente', () => {
fetch.mockResolvedValueOnce({
ok: true,
json: async () => ({ id: 1, name: 'João' })
});
const { result } = renderHook(() => useFetchUser(1));
expect(result.current.loading).toBe(true);
expect(result.current.user).toBeNull();
});
it('deve buscar e exibir os dados do usuário', async () => {
const mockUser = { id: 1, name: 'João Silva' };
fetch.mockResolvedValueOnce({
ok: true,
json: async () => mockUser
});
const { result } = renderHook(() => useFetchUser(1));
// Aguarde até que o loading seja false
await waitFor(() => {
expect(result.current.loading).toBe(false);
});
expect(result.current.user).toEqual(mockUser);
expect(result.current.error).toBeNull();
});
it('deve lidar com erros de API', async () => {
fetch.mockResolvedValueOnce({
ok: false,
json: async () => ({})
});
const { result } = renderHook(() => useFetchUser(1));
await waitFor(() => {
expect(result.current.loading).toBe(false);
});
expect(result.current.error).toBe('Erro ao buscar usuário');
expect(result.current.user).toBeNull();
});
it('deve fazer refetch quando userId muda', async () => {
const mockUser1 = { id: 1, name: 'João' };
const mockUser2 = { id: 2, name: 'Maria' };
fetch.mockResolvedValueOnce({
ok: true,
json: async () => mockUser1
});
const { result, rerender } = renderHook(
({ userId }) => useFetchUser(userId),
{ initialProps: { userId: 1 } }
);
await waitFor(() => {
expect(result.current.loading).toBe(false);
});
expect(result.current.user).toEqual(mockUser1);
// Agora mude o userId
fetch.mockResolvedValueOnce({
ok: true,
json: async () => mockUser2
});
act(() => {
rerender({ userId: 2 });
});
await waitFor(() => {
expect(result.current.user).toEqual(mockUser2);
});
});
});</code></pre>
<p>Note a diferença crucial aqui: quando você envolve uma operação assíncrona, não coloca a promise dentro de <code>act</code> diretamente. Em vez disso, use <code>waitFor</code> para aguardar uma condição. O <code>act</code> é apenas para ações síncronas ou promises que você controla completamente. O <code>waitFor</code> cuida de chamar <code>act</code> internamente enquanto aguarda.</p>
<h2>Cenários Avançados e Boas Práticas</h2>
<h3>Testing Library e Renderização com Providers</h3>
<p>Às vezes, seus hooks dependem de contexto ou providers (como Redux, tema, etc.). Para isso, <code>renderHook</code> aceita uma opção <code>wrapper</code>:</p>
<pre><code class="language-javascript">// useAuthHook.js
import { useContext } from 'react';
import { AuthContext } from './AuthContext';
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth deve ser usado dentro de AuthProvider');
}
return context;
}</code></pre>
<pre><code class="language-javascript">// useAuth.test.js
import { renderHook, act } from '@testing-library/react';
import { useAuth } from './useAuth';
import { AuthProvider, AuthContext } from './AuthContext';
describe('useAuth', () => {
it('deve retornar o contexto de autenticação', () => {
const wrapper = ({ children }) => (
<AuthProvider>{children}</AuthProvider>
);
const { result } = renderHook(() => useAuth(), { wrapper });
expect(result.current.user).toBeDefined();
expect(result.current.login).toBeDefined();
expect(result.current.logout).toBeDefined();
});
it('deve fazer login e atualizar o estado', async () => {
const wrapper = ({ children }) => (
<AuthProvider>{children}</AuthProvider>
);
const { result } = renderHook(() => useAuth(), { wrapper });
act(() => {
result.current.login('user@example.com', 'senha123');
});
await waitFor(() => {
expect(result.current.user).toBe('user@example.com');
expect(result.current.isAuthenticated).toBe(true);
});
});
it('deve lançar erro se usado fora do provider', () => {
expect(() => {
renderHook(() => useAuth());
}).toThrow('useAuth deve ser usado dentro de AuthProvider');
});
});</code></pre>
<h3>Testando Hooks com Temporizadores</h3>
<p>Hooks que usam <code>setTimeout</code> ou <code>setInterval</code> exigem gerenciamento especial de tempo nos testes:</p>
<pre><code class="language-javascript">// useDebounce.js
import { useState, useEffect } from 'react';
export function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(handler);
}, [value, delay]);
return debouncedValue;
}</code></pre>
<pre><code class="language-javascript">// useDebounce.test.js
import { renderHook, act } from '@testing-library/react';
import { useDebounce } from './useDebounce';
jest.useFakeTimers();
describe('useDebounce', () => {
it('deve debounce o valor após o delay', () => {
const { result, rerender } = renderHook(
({ value, delay }) => useDebounce(value, delay),
{ initialProps: { value: 'inicial', delay: 500 } }
);
expect(result.current).toBe('inicial');
act(() => {
rerender({ value: 'novo', delay: 500 });
});
// Ainda não passou o delay
expect(result.current).toBe('inicial');
act(() => {
jest.advanceTimersByTime(500);
});
// Agora passou
expect(result.current).toBe('novo');
});
it('deve cancelar o debounce se o valor mudar antes do delay', () => {
const { result, rerender } = renderHook(
({ value, delay }) => useDebounce(value, delay),
{ initialProps: { value: 'primeiro', delay: 500 } }
);
act(() => {
rerender({ value: 'segundo', delay: 500 });
});
act(() => {
jest.advanceTimersByTime(250);
});
act(() => {
rerender({ value: 'terceiro', delay: 500 });
});
act(() => {
jest.advanceTimersByTime(500);
});
expect(result.current).toBe('terceiro');
});
});
afterAll(() => {
jest.useRealTimers();
});</code></pre>
<p>Quando usamos <code>jest.useFakeTimers()</code>, os temporizadores não executam realmente. Você controla o tempo com <code>jest.advanceTimersByTime()</code>, sempre dentro de <code>act</code>. Isso torna os testes de timing determinísticos e rápidos.</p>
<h3>Validação de Dependências</h3>
<p>É uma boa prática validar que seu hook responde corretamente a mudanças nas dependências:</p>
<pre><code class="language-javascript">// useCustomHook.test.js - exemplo genérico
it('deve atualizar quando a dependência muda', () => {
const { result, rerender } = renderHook(
({ dep }) => useMyHook(dep),
{ initialProps: { dep: 'valor1' } }
);
const firstResult = result.current.data;
act(() => {
rerender({ dep: 'valor2' });
});
const secondResult = result.current.data;
expect(firstResult).not.toEqual(secondResult);
});</code></pre>
<h2>Conclusão</h2>
<p>Dominar testes de hooks customizados com <code>renderHook</code> e <code>act</code> transforma completamente sua capacidade de criar código confiável em React. <strong>Primeiro ponto importante</strong>: <code>renderHook</code> elimina a necessidade de componentes wrapper de teste, permitindo que você teste lógica de forma isolada e clara. <strong>Segundo ponto</strong>: <code>act</code> não é apenas uma ferramenta, é seu meio de comunicação com o React — ele sincroniza suas ações com o ciclo de vida real do framework, garantindo que você teste comportamentos reais e não fluk aleatórios. <strong>Terceiro ponto</strong>: combinar <code>renderHook</code> com <code>rerender</code>, <code>waitFor</code> e jest.useFakeTimers cobre praticamente todos os cenários que você encontrará na prática, desde hooks simples até aqueles com efeitos colaterais complexos e temporizadores.</p>
<h2>Referências</h2>
<ul>
<li><a href="https://testing-library.com/docs/react-testing-library/intro/" target="_blank" rel="noopener noreferrer">Testing Library - Documentação Oficial</a></li>
<li><a href="https://testing-library.com/docs/react-testing-library/api/#renderhook" target="_blank" rel="noopener noreferrer">React Testing Library - renderHook API</a></li>
<li><a href="https://testing-library.com/docs/queries/about/#default-behavior" target="_blank" rel="noopener noreferrer">Testing Library - act</a></li>
<li><a href="https://react.dev/reference/rules/rules-of-hooks" target="_blank" rel="noopener noreferrer">React - Rules of Hooks (Oficial)</a></li>
<li><a href="https://jestjs.io/docs/getting-started" target="_blank" rel="noopener noreferrer">Jest - Manual de Testes</a></li>
</ul>
<p><!-- FIM --></p>