Testes de componentes com React Testing Library
1. Introdução ao React Testing Library
O React Testing Library (RTL) revolucionou a forma como testamos componentes React ao adotar uma filosofia centrada no usuário. Diferentemente do Enzyme, que permite testar detalhes internos de implementação (como estado e métodos de ciclo de vida), o RTL foca em testar o comportamento visível ao usuário — o que ele vê, clica e digita.
Essa abordagem torna os testes mais resilientes a refatorações e mais próximos da experiência real do usuário. Em projetos Node.js, configuramos o ambiente com Jest e RTL:
// Instalação
npm install --save-dev @testing-library/react @testing-library/jest-dom jest
// jest.config.js
module.exports = {
testEnvironment: 'jsdom',
setupFilesAfterSetup: ['@testing-library/jest-dom'],
};
2. Renderizando e consultando elementos
A renderização de componentes é feita com render() e screen:
import { render, screen } from '@testing-library/react';
import Button from './Button';
test('renderiza botão com texto', () => {
render(<Button>Clique aqui</Button>);
expect(screen.getByText('Clique aqui')).toBeInTheDocument();
});
As consultas seguem uma hierarquia de prioridade baseada em acessibilidade:
- getByRole (recomendado para elementos semânticos)
- getByLabelText (para formulários)
- getByPlaceholderText (para campos com placeholder)
- getByText (para elementos de texto)
- getByTestId (último recurso)
// Exemplo com getByRole
test('navegação tem links', () => {
render(<Navigation />);
const homeLink = screen.getByRole('link', { name: /home/i });
expect(homeLink).toHaveAttribute('href', '/');
});
3. Simulando interações do usuário
Para simular interações reais, @testing-library/user-event é preferível ao fireEvent por ser mais próximo do comportamento humano:
import userEvent from '@testing-library/user-event';
import { render, screen } from '@testing-library/react';
import Form from './Form';
test('submete formulário com dados do usuário', async () => {
const user = userEvent.setup();
const onSubmit = jest.fn();
render(<Form onSubmit={onSubmit} />);
const nomeInput = screen.getByLabelText('Nome:');
await user.type(nomeInput, 'João Silva');
const submitButton = screen.getByRole('button', { name: /enviar/i });
await user.click(submitButton);
expect(onSubmit).toHaveBeenCalledWith({ nome: 'João Silva' });
});
Para testar validações:
test('mostra erro quando campo obrigatório está vazio', async () => {
const user = userEvent.setup();
render(<Form />);
await user.click(screen.getByRole('button', { name: /enviar/i }));
expect(screen.getByText('Nome é obrigatório')).toBeInTheDocument();
});
4. Testando comportamentos assíncronos
Elementos que aparecem após requisições assíncronas exigem waitFor ou findBy:
import { render, screen, waitFor } from '@testing-library/react';
import UserList from './UserList';
test('carrega e exibe usuários', async () => {
render(<UserList />);
// Usando findBy (que retorna uma Promise)
const users = await screen.findAllByRole('listitem');
expect(users).toHaveLength(3);
// Ou com waitFor
await waitFor(() => {
expect(screen.getByText('João')).toBeInTheDocument();
});
});
Para simular chamadas HTTP, o MSW (Mock Service Worker) é a ferramenta ideal:
import { setupServer } from 'msw/node';
import { handlers } from './handlers';
const server = setupServer(...handlers);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
test('exibe erro ao falhar requisição', async () => {
server.use(
rest.get('/api/users', (req, res, ctx) => {
return res(ctx.status(500));
})
);
render(<UserList />);
expect(await screen.findByText('Erro ao carregar')).toBeInTheDocument();
});
5. Testando hooks e contexto
Para testar hooks customizados, usamos renderHook:
import { renderHook, act } from '@testing-library/react';
import useCounter from './useCounter';
test('incrementa contador', () => {
const { result } = renderHook(() => useCounter());
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
Para testar componentes que dependem de contexto:
import { render, screen } from '@testing-library/react';
import { ThemeProvider } from './ThemeContext';
import ThemedButton from './ThemedButton';
const renderWithTheme = (component) => {
return render(
<ThemeProvider value={{ theme: 'dark' }}>
{component}
</ThemeProvider>
);
};
test('botão usa tema escuro', () => {
renderWithTheme(<ThemedButton />);
expect(screen.getByRole('button')).toHaveClass('dark');
});
6. Boas práticas e padrões avançados
A estrutura AAA (Arrange-Act-Assert) organiza os testes de forma clara:
test('calcula total do carrinho', () => {
// Arrange
const itens = [{ preco: 10 }, { preco: 20 }];
render(<Carrinho itens={itens} />);
// Act
const total = screen.getByTestId('total');
// Assert
expect(total).toHaveTextContent('R$ 30,00');
});
Para testar acessibilidade com jest-axe:
import { axe } from 'jest-axe';
test('componente não tem violações de acessibilidade', async () => {
const { container } = render(<Navigation />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});
7. Integração com Next.js API Routes
Para testar componentes que consomem API Routes locais:
import { rest } from 'msw';
import { setupServer } from 'msw/node';
const server = setupServer(
rest.get('/api/products', (req, res, ctx) => {
return res(ctx.json([{ id: 1, name: 'Produto A' }]));
})
);
test('exibe produtos carregados da API', async () => {
render(<ProductList />);
expect(await screen.findByText('Produto A')).toBeInTheDocument();
});
Para Server Components do Next.js, os testes são mais limitados, mas podemos testar a lógica de renderização:
// Testando Server Component
test('ServerComponent renderiza dados', async () => {
const data = await fetchData();
const { container } = render(await ServerComponent({ data }));
expect(container).toHaveTextContent('Dados carregados');
});
8. Depuração e cobertura de testes
O screen.debug() é essencial para inspecionar o DOM durante os testes:
test('debug do componente', () => {
render(<ComplexForm />);
screen.debug(); // Exibe o HTML renderizado no console
});
Configuração de cobertura no Jest:
// jest.config.js
module.exports = {
collectCoverage: true,
coverageDirectory: 'coverage',
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80,
},
},
};
Para manter testes rápidos em pipelines CI/CD:
- Use --maxWorkers=2 para limitar paralelismo
- Configure cache de dependências
- Execute testes em ordem aleatória (--shard)
- Separe testes unitários de integração
# Comando otimizado para CI
npx jest --ci --maxWorkers=2 --coverage --shard=1/4
Referências
- Documentação oficial do React Testing Library — Guia completo com exemplos de consultas, eventos e boas práticas
- Testing JavaScript com Kent C. Dodds — Curso aprofundado sobre testes em React, incluindo RTL e Jest
- Mock Service Worker (MSW) Documentation — Documentação oficial do MSW para mock de APIs em testes
- jest-axe: Accessibility Testing for Jest — Biblioteca para testar acessibilidade automaticamente com RTL
- Next.js Testing Documentation — Guia oficial do Next.js sobre testes, incluindo Server Components e API Routes
- Common Testing Mistakes with React Testing Library — Artigo de Kent C. Dodds sobre erros frequentes e como evitá-los
- React Testing Library Cheatsheet — Resumo rápido de todas as consultas e utilitários disponíveis