Testing Library: testes de integração no React

1. Introdução aos Testes de Integração com Testing Library

No ecossistema React, os testes podem ser classificados em três grandes categorias: testes unitários, que verificam funções ou componentes isolados; testes de integração, que validam a interação entre múltiplos componentes e serviços; e testes end-to-end (E2E), que simulam o fluxo completo do usuário no navegador. A Testing Library se destaca como a escolha padrão para testes de integração porque incentiva uma abordagem centrada no usuário: em vez de testar detalhes de implementação, você testa o comportamento visível e acessível da interface.

A filosofia da Testing Library é simples: quanto mais seus testes se parecem com a forma como o usuário interage com a aplicação, mais confiança eles podem lhe dar. Para começar, instale os pacotes necessários:

npm install --save-dev @testing-library/react @testing-library/jest-dom @testing-library/user-event

2. Configuração do Ambiente de Teste

Em projetos criados com Create React App (CRA), a configuração já vem pronta. Para Vite ou Next.js, você precisa configurar o Jest manualmente. Crie um arquivo jest.config.js:

module.exports = {
  testEnvironment: 'jsdom',
  setupFilesAfterSetup: ['<rootDir>/src/setupTests.js'],
  moduleNameMapper: {
    '\\.(css|less|scss)$': 'identity-obj-proxy',
  },
};

No arquivo setupTests.js, importe as extensões do jest-dom:

import '@testing-library/jest-dom';

Para mockar módulos como React Router ou Redux, use jest.mock() no início do arquivo de teste ou crie providers customizados.

3. Renderização e Consultas (Queries)

A função render() do Testing Library renderiza um componente no DOM virtual. Use screen para acessar os elementos renderizados:

import { render, screen } from '@testing-library/react';
import MeuComponente from './MeuComponente';

test('renderiza o título', () => {
  render(<MeuComponente />);
  const titulo = screen.getByText('Bem-vindo');
  expect(titulo).toBeInTheDocument();
});

As queries disponíveis são: getBy, findBy e queryBy. A principal diferença está no comportamento quando o elemento não é encontrado: getBy lança erro, queryBy retorna null e findBy retorna uma Promise (útil para elementos assíncronos). Sempre priorize getByRole e outras queries baseadas em acessibilidade:

const botao = screen.getByRole('button', { name: /enviar/i });
const campo = screen.getByLabelText('Nome');

Evite data-testid a menos que seja estritamente necessário, pois ele não representa como o usuário enxerga a interface.

4. Simulação de Interações do Usuário

Para simular eventos do usuário, use @testing-library/user-event em vez do fireEvent nativo. O user-event simula interações mais realistas, como digitação caractere por caractere e cliques completos:

import userEvent from '@testing-library/user-event';

test('preenche formulário e submete', async () => {
  const user = userEvent.setup();
  render(<Formulario />);

  await user.type(screen.getByLabelText('Email'), 'teste@email.com');
  await user.click(screen.getByRole('button', { name: /enviar/i }));

  expect(screen.getByText('Enviado com sucesso')).toBeInTheDocument();
});

Para testar navegação entre rotas, envolva o componente com MemoryRouter:

import { MemoryRouter } from 'react-router-dom';

render(
  <MemoryRouter initialEntries={['/']}>
    <App />
  </MemoryRouter>
);

5. Testando Estados Assíncronos e Chamadas de API

Para testar chamadas assíncronas, use waitFor ou findBy. O findBy é uma combinação de getBy com waitFor implícito:

test('carrega dados da API', async () => {
  render(<ListaUsuarios />);

  // Aguarda o elemento aparecer
  const usuario = await screen.findByText('João');
  expect(usuario).toBeInTheDocument();
});

Para mockar requisições HTTP, utilize o Mock Service Worker (MSW), que intercepta requisições no nível do service worker:

import { rest } from 'msw';
import { setupServer } from 'msw/node';

const server = setupServer(
  rest.get('/api/usuarios', (req, res, ctx) => {
    return res(ctx.json([{ id: 1, nome: 'João' }]));
  })
);

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

Teste os três estados: loading, sucesso e erro. Para simular erro, basta modificar o handler:

server.use(
  rest.get('/api/usuarios', (req, res, ctx) => {
    return res(ctx.status(500));
  })
);

6. Testando Componentes com Contexto e Estado Global

Componentes que dependem de contexto (React Context, Redux, Zustand) precisam ser renderizados dentro de seus providers. Crie uma função render customizada para evitar repetição:

import { render } from '@testing-library/react';
import { ThemeProvider } from './contexts/ThemeContext';
import { Provider } from 'react-redux';
import { store } from './store';

const AllTheProviders = ({ children }) => {
  return (
    <Provider store={store}>
      <ThemeProvider>{children}</ThemeProvider>
    </Provider>
  );
};

const customRender = (ui, options) =>
  render(ui, { wrapper: AllTheProviders, ...options });

export { customRender as render };

Agora, nos testes, importe seu render customizado:

import { render, screen } from './test-utils';

test('exibe informações do usuário logado', () => {
  render(<Perfil />);
  expect(screen.getByText('Bem-vindo, Admin')).toBeInTheDocument();
});

7. Depuração e Boas Práticas em Testes de Integração

Quando um teste falha, use screen.debug() para inspecionar o DOM renderizado:

test('debugando', () => {
  render(<Componente />);
  screen.debug(); // Mostra o HTML completo
});

Use logRoles para listar os papéis acessíveis no componente:

import { logRoles } from '@testing-library/dom';

const { container } = render(<Componente />);
logRoles(container);

Estruture seus testes com describe e it para criar uma narrativa clara:

describe('Formulário de cadastro', () => {
  it('exibe mensagem de erro quando email é inválido', async () => {
    // ...
  });

  it('envia dados corretamente quando campos são válidos', async () => {
    // ...
  });
});

Lembre-se: teste comportamento, não implementação. Evite testar estados internos ou métodos privados. Foque no que o usuário vê e faz.

8. Integração com Ferramentas de CI e Cobertura

Para executar testes em pipelines CI (GitHub Actions, GitLab CI), adicione ao seu package.json:

{
  "scripts": {
    "test": "jest --coverage"
  }
}

No GitHub Actions, um workflow básico seria:

name: Testes
on: [push]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-node@v3
      - run: npm ci
      - run: npm test

O relatório de cobertura (--coverage) gera uma pasta coverage/ com dados do Istanbul. Configure thresholds no jest.config.js:

module.exports = {
  coverageThreshold: {
    global: {
      branches: 80,
      functions: 80,
      lines: 80,
      statements: 80,
    },
  },
};

Para manter testes rápidos, evite mockar bibliotecas desnecessárias e use jest.useFakeTimers() quando apropriado. Testes de integração bem escritos são a base para uma aplicação React confiável e de fácil manutenção.


Referências