Introdução ao testing-library e sua filosofia de testes centrados no usuário

1. O que é a testing-library e por que ela foi criada

A testing-library surgiu para resolver um problema comum no desenvolvimento front-end: testes frágeis que quebram com frequência por dependerem de detalhes internos de implementação. Tradicionalmente, muitos testes verificavam se uma classe CSS específica estava presente, se um estado interno do componente era atualizado corretamente ou se uma função foi chamada com os argumentos exatos. Esses testes, embora funcionais, tornavam-se quebradiços — qualquer refatoração visual ou de estrutura interna exigia reescrita dos testes.

O projeto começou com a React Testing Library, criada por Kent C. Dodds, como uma alternativa ao Enzyme. A ideia central era simples: em vez de testar detalhes de implementação, teste o que o usuário realmente vê e com o que ele interage. Rapidamente, a filosofia se expandiu para outros frameworks, dando origem ao ecossistema testing-library, que hoje inclui versões para Vue, Angular, Svelte, React Native e até mesmo Node.js.

A diferença fundamental está no foco: testes tradicionais muitas vezes verificam "se o estado interno mudou", enquanto a testing-library pergunta "se o usuário consegue ver e interagir com o resultado esperado".

2. Filosofia central: testes centrados no usuário

O princípio que norteia toda a biblioteca é: "Quanto mais seus testes se parecem com a forma como o software é usado, mais confiança eles podem te dar". Isso significa que um teste deve simular ações reais de um usuário — clicar em botões, preencher formulários, ler textos na tela — e não acessar propriedades internas do componente.

Para evitar testes frágeis, a testing-library recomenda não depender de:
- Nomes de classes CSS
- IDs de elementos
- Estado interno do componente
- Estrutura exata do DOM

Em vez disso, busca-se elementos por aquilo que o usuário percebe: textos visíveis, papéis semânticos (roles), labels de formulários e placeholders.

Exemplo prático:

// ❌ Teste frágil: depende de classe CSS
const button = container.querySelector('.btn-submit');
fireEvent.click(button);

// ✅ Teste robusto: busca pelo texto que o usuário vê
const button = screen.getByRole('button', { name: /enviar/i });
await user.click(button);

3. Principais queries e sua hierarquia de prioridade

A testing-library define uma hierarquia clara de queries, priorizando aquelas que simulam a experiência do usuário:

Queries acessíveis (prioridade máxima):
- getByRole — busca por papéis semânticos (button, heading, link)
- getByLabelText — ideal para campos de formulário com label
- getByPlaceholderText — para campos com placeholder
- getByText — busca por conteúdo textual visível

Queries semânticas:
- getByAltText — para imagens com texto alternativo
- getByTitle — para elementos com atributo title

Queries de último recurso:
- getByTestId — deve ser usado com moderação, apenas quando não há alternativa semântica

Exemplo prático de cada query:

// getByRole: buscar um botão de envio
const submitBtn = screen.getByRole('button', { name: /enviar/i });

// getByLabelText: buscar campo de email pelo label
const emailInput = screen.getByLabelText(/e-mail/i);

// getByPlaceholderText: buscar campo pelo placeholder
const searchField = screen.getByPlaceholderText('Digite sua busca...');

// getByText: buscar um título visível
const title = screen.getByText('Bem-vindo ao sistema');

// getByAltText: buscar imagem com descrição
const logo = screen.getByAltText('Logotipo da empresa');

// getByTestId: apenas quando necessário
const customElement = screen.getByTestId('custom-wrapper');

4. Como a testing-library promove acessibilidade nos testes

Um dos maiores benefícios da testing-library é que ela naturalmente incentiva boas práticas de acessibilidade. Ao usar getByRole, você está verificando que os elementos possuem papéis semânticos corretos — um botão deve ser um <button>, não um <div> com evento de clique. Da mesma forma, getByLabelText garante que labels estão associados corretamente aos inputs.

Isso significa que testes escritos com a testing-library funcionam como uma ferramenta de detecção precoce de problemas de acessibilidade. Se um desenvolvedor esquecer de adicionar um aria-label em um ícone clicável, o teste que usa getByRole falhará, alertando para o problema.

Exemplo prático:

// Componente: formulário de login
render(<LoginForm />);

// Teste verifica se o label está associado corretamente
const emailInput = screen.getByLabelText('E-mail');
await user.type(emailInput, 'usuario@exemplo.com');

// Se o label estiver ausente ou mal associado, o teste falha
// Isso força o desenvolvedor a usar HTML semântico correto
expect(emailInput).toHaveValue('usuario@exemplo.com');

5. Eventos e interações do usuário: o papel do @testing-library/user-event

A testing-library oferece duas formas de simular eventos: fireEvent (nativo) e userEvent (do pacote @testing-library/user-event). A recomendação oficial é usar userEvent sempre que possível, pois ele simula interações mais realistas.

Enquanto fireEvent apenas dispara um evento no DOM, userEvent simula a sequência completa de eventos que um usuário real geraria. Por exemplo, ao digitar em um campo, userEvent.type dispara eventos de foco, tecla pressionada, tecla solta e desfoque — exatamente como aconteceria em um navegador.

Exemplo prático:

import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

test('envio de formulário com interação realista', async () => {
  const user = userEvent.setup();
  render(<LoginForm />);

  // Simula digitação realista no campo de email
  const emailInput = screen.getByLabelText('E-mail');
  await user.type(emailInput, 'usuario@exemplo.com');

  // Simula clique realista no botão
  const submitButton = screen.getByRole('button', { name: /entrar/i });
  await user.click(submitButton);

  // Verifica o resultado visível para o usuário
  const successMessage = await screen.findByText('Login realizado com sucesso');
  expect(successMessage).toBeInTheDocument();
});

6. Estratégias para consultas assíncronas e esperas inteligentes

Aplicações modernas frequentemente carregam dados de APIs, exibem spinners de carregamento e atualizam o DOM de forma assíncrona. A testing-library oferece ferramentas específicas para lidar com esses cenários sem usar setTimeout ou sleep fixos, que são frágeis e lentos.

Principais ferramentas assíncronas:
- findBy* — combina getBy* com waitFor, retornando uma Promise
- waitFor — aguarda até que uma condição seja satisfeita
- waitForElementToBeRemoved — aguarda até que um elemento desapareça

Exemplo prático:

test('carregamento de dados da API', async () => {
  render(<UserList />);

  // Enquanto carrega, mostra um spinner
  expect(screen.getByText('Carregando...')).toBeInTheDocument();

  // Aguarda o carregamento dos dados (findBy* já faz o waitFor internamente)
  const userItem = await screen.findByText('João Silva');
  expect(userItem).toBeInTheDocument();

  // Aguarda o spinner desaparecer
  await waitForElementToBeRemoved(() => screen.queryByText('Carregando...'));
});

7. Limitações e boas práticas ao usar testing-library

O que não testar com testing-library:
- Lógica de estado interno de componentes
- Chamadas de API isoladas (use mocks para isso)
- Detalhes de renderização que o usuário não vê
- Implementação específica de bibliotecas de estado

Boas práticas:

  1. Use screen em vez de desestruturar queries — o objeto screen já vem configurado e evita problemas com renderização múltipla.
// ✅ Prefira screen
const button = screen.getByRole('button');

// ❌ Evite desestruturação
const { getByRole } = render(<Component />);
  1. Mantenha testes focados no comportamento do usuário — pergunte-se: "O que o usuário vê e faz?" antes de escrever cada asserção.

  2. Integre com outras ferramentas — a testing-library funciona perfeitamente com Jest e Vitest para testes unitários, e com Cypress e Playwright para testes de integração e e2e.

  3. Escreva testes legíveis — nomes descritivos e estrutura clara ajudam na manutenção.

test('deve exibir mensagem de erro quando email é inválido', async () => {
  const user = userEvent.setup();
  render(<LoginForm />);

  await user.type(screen.getByLabelText('E-mail'), 'email-invalido');
  await user.click(screen.getByRole('button', { name: /entrar/i }));

  expect(screen.getByText('E-mail inválido')).toBeInTheDocument();
});

A testing-library não é uma solução mágica para todos os problemas de teste, mas oferece uma base sólida para criar testes que realmente validam a experiência do usuário. Ao adotar sua filosofia, você reduz testes frágeis, melhora a acessibilidade do seu código e ganha mais confiança na entrega de software funcional.

Referências