React
Testes em React com Testing Library: Do Básico ao Avançado
5 de junho de 2024·Paulo de Paula
Testing Library parte de uma premissa simples: teste o que o usuário vê e faz, não como o componente é implementado. Isso significa buscar elementos pelo texto visível ou papel semântico, não por classes CSS ou nomes de função.
Queries: encontrando elementos
import { render, screen } from '@testing-library/react';
render(<button disabled>Salvar</button>);
// getBy* — falha se não encontrar (ou encontrar mais de um)
screen.getByRole('button', { name: 'Salvar' });
screen.getByText('Salvar');
screen.getByLabelText('E-mail'); // para inputs com <label>
screen.getByPlaceholderText('Nome');
screen.getByTestId('submit-btn'); // último recurso
// queryBy* — retorna null se não encontrar (não lança erro)
// Útil para assertions de ausência:
expect(screen.queryByText('Erro')).not.toBeInTheDocument();
// findBy* — assíncrono, retorna Promise (aguarda o elemento aparecer)
const modal = await screen.findByRole('dialog');Prioridade de queries (mais semântico → menos semântico):
getByRole— mais próximo de como leitores de tela enxergamgetByLabelText— para campos de formuláriogetByText— para conteúdo visívelgetByTestId— só quando não há alternativa semântica
userEvent vs fireEvent
import userEvent from '@testing-library/user-event';
// ❌ fireEvent: simula o evento mas não o comportamento real
fireEvent.click(button);
fireEvent.change(input, { target: { value: 'texto' } });
// ✅ userEvent: simula o usuário real (foco, teclas, ponteiro)
const user = userEvent.setup();
await user.click(button);
await user.type(input, 'Paulo'); // dispara keydown, keypress, keyup, input
await user.clear(input);
await user.selectOptions(select, 'SP');
await user.upload(fileInput, file);userEvent é mais lento mas mais fiel ao comportamento real. Use-o sempre que possível.
Testando componentes simples
// components/Badge.tsx
function Badge({ count }: { count: number }) {
if (count === 0) return null;
return <span aria-label={`${count} notificações`}>{count}</span>;
}
// Badge.test.tsx
describe('Badge', () => {
it('renderiza a contagem quando maior que zero', () => {
render(<Badge count={5} />);
expect(screen.getByText('5')).toBeInTheDocument();
});
it('não renderiza quando count é zero', () => {
render(<Badge count={0} />);
expect(screen.queryByRole('status')).not.toBeInTheDocument();
});
});Testando formulários
describe('LoginForm', () => {
it('exibe erro quando senha está vazia', async () => {
const user = userEvent.setup();
render(<LoginForm onLogin={jest.fn()} />);
await user.type(screen.getByLabelText('E-mail'), 'paulo@example.com');
await user.click(screen.getByRole('button', { name: 'Entrar' }));
expect(await screen.findByText('Senha obrigatória')).toBeInTheDocument();
});
it('chama onLogin com as credenciais corretas', async () => {
const onLogin = jest.fn().mockResolvedValue(undefined);
const user = userEvent.setup();
render(<LoginForm onLogin={onLogin} />);
await user.type(screen.getByLabelText('E-mail'), 'paulo@example.com');
await user.type(screen.getByLabelText('Senha'), 'senha123');
await user.click(screen.getByRole('button', { name: 'Entrar' }));
await waitFor(() => {
expect(onLogin).toHaveBeenCalledWith({
email: 'paulo@example.com',
password: 'senha123',
});
});
});
});Testando comportamento assíncrono
// Componente que busca dados
function UserCard({ userId }: { userId: string }) {
const [user, setUser] = useState<User | null>(null);
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(r => r.json())
.then(setUser);
}, [userId]);
if (!user) return <p>Carregando...</p>;
return <h2>{user.name}</h2>;
}
// Teste com mock de fetch
global.fetch = jest.fn();
it('exibe nome do usuário após carregar', async () => {
(fetch as jest.Mock).mockResolvedValueOnce({
ok: true,
json: async () => ({ id: '1', name: 'Paulo de Paula' }),
});
render(<UserCard userId="1" />);
// Estado de loading
expect(screen.getByText('Carregando...')).toBeInTheDocument();
// Aguarda o conteúdo assíncrono
expect(await screen.findByText('Paulo de Paula')).toBeInTheDocument();
expect(screen.queryByText('Carregando...')).not.toBeInTheDocument();
});Mockando React Query
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
function renderWithQuery(ui: React.ReactElement) {
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false } }, // sem retry em testes
});
return render(
<QueryClientProvider client={queryClient}>{ui}</QueryClientProvider>
);
}
// Mock do módulo de API
jest.mock('../api/users', () => ({
fetchUser: jest.fn(),
}));
import { fetchUser } from '../api/users';
it('exibe dados do usuário via React Query', async () => {
(fetchUser as jest.Mock).mockResolvedValue({ name: 'Paulo' });
renderWithQuery(<UserProfile userId="1" />);
expect(await screen.findByText('Paulo')).toBeInTheDocument();
});Mockando React Router
import { MemoryRouter, Route, Routes } from 'react-router-dom';
function renderWithRouter(
ui: React.ReactElement,
{ route = '/' }: { route?: string } = {},
) {
return render(
<MemoryRouter initialEntries={[route]}>
<Routes>
<Route path="*" element={ui} />
</Routes>
</MemoryRouter>
);
}
it('redireciona para login quando não autenticado', async () => {
renderWithRouter(<ProtectedPage />, { route: '/dashboard' });
expect(await screen.findByText('Faça login para continuar')).toBeInTheDocument();
});Antipadrões comuns
// ❌ Testando implementação: se renomear a função, o teste quebra
expect(component.instance().handleClick).toBeDefined();
// ❌ Snapshot gigante: qualquer mudança de layout quebra o teste
expect(container).toMatchSnapshot(); // evite em componentes grandes
// ❌ Seletor por classe CSS: frágil, não reflete comportamento
document.querySelector('.btn-primary').click();
// ❌ act() manual desnecessário: findBy* já lida com async
await act(async () => {
await screen.findByText('Resultado'); // findBy já faz isso
});
// ✅ Prefira sempre
screen.getByRole('button', { name: /salvar/i }); // case-insensitive regex
await screen.findByRole('alert'); // aguarda automaticamente
O sinal de que seu teste está bem escrito: você pode refatorar a implementação do componente completamente (mudar estado para context, hooks para Zustand, etc.) e o teste continua passando sem mudanças.