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