React Testing Library: Uma Aula Completa

Testando Componentes React de Forma Moderna e Eficaz

O que vamos aprender hoje?

  1. A Filosofia do RTL: Por que ele é diferente?
  2. Setup e Configuração: Começando do zero.
  3. Queries (Consultas): Encontrando elementos da forma certa.
  4. getBy, findBy e queryBy: Entendendo as diferenças.
  5. Simulando Interações: user-event em ação.
  6. Testes Assíncronos: Lidando com o inesperado (ou esperado).
  7. Mocks: Isolando componentes para testes puros.
  8. Boas Práticas: Escrevendo testes que agregam valor.
  9. Demonstração Prática: Vamos escrever alguns testes!

1. A Filosofia do RTL

"The more your tests resemble the way your software is used, the more confidence they can give you."

"Quanto mais seus testes se assemelharem à forma como seu software é usado, mais confiança eles podem lhe dar." - Kent C. Dodds

  • Foco no Comportamento, não na Implementação: Testamos o que o usuário vê e interage, não os detalhes internos do componente (props, state, etc.).
  • Acessibilidade em Primeiro Lugar: As queries do RTL nos incentivam a criar aplicações mais acessíveis.
  • Confiança na Refatoração: Seus testes não quebram quando você refatora o código, desde que o comportamento do componente permaneça o mesmo.

2. Setup e Configuração

A maioria dos setups modernos de React (Create React App, Vite) já vem com o React Testing Library configurado.

Instalação manual (se necessário):

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

Configuração do jest-dom:
Para ter acesso a matchers úteis como toBeInTheDocument(), crie um arquivo setupTests.js:

// src/setupTests.js
import '@testing-library/jest-dom';

E configure no seu jest.config.js:

{
  "setupFilesAfterEnv": ["<rootDir>/src/setupTests.js"]
}

3. Queries: A Arte de Encontrar Elementos

O RTL fornece várias queries para encontrar elementos no DOM. A chave é escolher a query que mais se assemelha a como um usuário encontraria aquele elemento.

Hierarquia de Prioridade das Queries (A Melhor Prática):

  1. getByRole: A mais recomendada. Baseada na role de acessibilidade.
    Ex: button, heading, link, textbox
  2. getByLabelText: Para elementos de formulário.
  3. getByPlaceholderText: Útil para inputs sem um label visível.
  4. getByText: Encontra pelo conteúdo de texto.
  5. getByDisplayValue: Para encontrar inputs com um valor atual.
  6. getByAltText: Para imagens com alt text.
  7. getByTitle: Para elementos com o atributo title.
  8. getByTestId: O último recurso. Use data-testid quando nenhuma das queries acima for suficiente.

Estrutura de um Teste Básico

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

test('deve renderizar o título principal', () => {
  // 1. Arrange (Organizar)
  render(<MeuComponente />);

  // 2. Act (Agir) - (Opcional aqui)
  // ... nenhuma ação necessária para este teste

  // 3. Assert (Afirmar)
  const titulo = screen.getByRole('heading', { name: /bem-vindo/i });
  expect(titulo).toBeInTheDocument();
});
  • render: Renderiza o componente em um container no jsdom.
  • screen: Um objeto que contém todas as queries pré-vinculadas ao document.body.

4. getBy, findBy e queryBy

Prefixo Elemento Não Encontrado Múltiplos Elementos Uso Principal
getBy... Lança um erro Lança um erro Para afirmar que um elemento está presente.
queryBy... Retorna null Lança um erro Para afirmar que um elemento não está presente.
findBy... Retorna uma Promise Retorna uma Promise Para esperar por um elemento que aparecerá assincronamente.
  • getAllBy..., queryAllBy..., findAllBy...: Variantes que retornam um array de elementos e não lançam erro se múltiplos forem encontrados.

Exemplos: getBy, findBy, queryBy

// getBy: O elemento DEVE estar lá
test('botão de login deve estar visível', () => {
  render(<Login />);
  expect(screen.getByRole('button', { name: /entrar/i })).toBeInTheDocument();
});

// queryBy: O elemento NÃO DEVE estar lá
test('mensagem de erro não deve ser exibida inicialmente', () => {
  render(<Login />);
  expect(screen.queryByText(/usuário ou senha inválida/i)).not.toBeInTheDocument();
});

// findBy: Esperando por algo assíncrono
test('deve exibir o nome do usuário após o login', async () => {
  render(<UserProfile />);
  const nomeUsuario = await screen.findByText(/joão silva/i);
  expect(nomeUsuario).toBeInTheDocument();
});

5. Simulando Interações com user-event

Enquanto fireEvent é uma opção, @testing-library/user-event é a biblioteca recomendada. Ela simula interações do usuário de forma mais realista, disparando todos os eventos que um navegador dispararia.

Setup:

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

test('...', async () => {
  const user = userEvent.setup();
  // ... seu teste
});

Principais Ações:

  • await user.click(element)
  • await user.keyboard('texto a ser digitado')
  • await user.type(inputElement, 'texto')
  • await user.hover(element)
  • await user.selectOptions(selectElement, 'valor')

Exemplo Prático: user-event

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

test('deve incrementar o contador ao clicar no botão', async () => {
  // Arrange
  const user = userEvent.setup();
  render(<Counter />);

  // Act
  const botaoIncrementar = screen.getByRole('button', { name: /incrementar/i });
  await user.click(botaoIncrementar);
  await user.click(botaoIncrementar);

  // Assert
  const contador = screen.getByText(/contagem: 2/i);
  expect(contador).toBeInTheDocument();
});

6. Testes Assíncronos

A UI é frequentemente assíncrona (chamadas de API, timers, etc.). O RTL tem ferramentas para lidar com isso.

  • findBy...: A forma mais comum e preferida. Ela já usa waitFor por baixo dos panos.
  • waitFor: Uma função utilitária para esperar que uma asserção dentro de um callback passe.
import { waitFor } from '@testing-library/react';

test('deve remover a mensagem de loading após a chamada da API', async () => {
  render(<DataFetcher />);

  // A mensagem de loading está presente inicialmente
  expect(screen.getByText(/carregando.../i)).toBeInTheDocument();

  // Usando waitFor para esperar que a mensagem de loading desapareça
  await waitFor(() => {
    expect(screen.queryByText(/carregando.../i)).not.toBeInTheDocument();
  });

  // Afirmar que os dados foram carregados
  expect(screen.getByText(/dados carregados/i)).toBeInTheDocument();
});

7. Mocks: Isolando seus Testes

Para testar um componente de forma isolada, frequentemente precisamos "mockar" (simular) suas dependências, como chamadas de API, módulos ou outros componentes.

Exemplo: Mockando uma chamada fetch com Jest:

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

// Mocka o fetch globalmente para este arquivo de teste
global.fetch = jest.fn(() =>
  Promise.resolve({
    json: () => Promise.resolve({ name: 'Usuário Mockado' }),
  })
);

test('deve buscar e exibir dados do usuário', async () => {
  render(<UserProfile />);
  const user = userEvent.setup();

  const botaoBuscar = screen.getByRole('button', { name: /buscar usuário/i });
  await user.click(botaoBuscar);

  // Espera o nome do usuário mockado aparecer
  const nomeUsuario = await screen.findByText(/usuário mockado/i);
  expect(nomeUsuario).toBeInTheDocument();

  // Verifica se o fetch foi chamado
  expect(global.fetch).toHaveBeenCalledTimes(1);
});

8. Boas Práticas

  1. Siga a Hierarquia de Queries: Use getByRole sempre que possível. Evite getByTestId.
  2. Teste como um Usuário: Não teste detalhes de implementação. Se o usuário não pode "ver" ou "interagir" com algo, seu teste provavelmente não deveria também.
  3. Use screen: É mais limpo e consistente do que desestruturar as queries do render.
  1. Prefira user-event a fireEvent: Para testes de interação mais robustos.
  2. Use waitFor para Esperar por Asserções, não Ações: A ação (ex: click) deve acontecer fora do waitFor. O waitFor serve para esperar pelo resultado da ação.
  3. Escreva Testes Claros e Descritivos: O nome do teste (test('...')) deve descrever claramente o que está sendo testado.

9. Demonstração Prática: Vamos Testar!

Vamos testar um componente de formulário de login.

Componente LoginForm.js:

import React, { useState } from 'react';

function LoginForm() {
  const [error, setError] = useState('');
  const [loading, setLoading] = useState(false);

  const handleSubmit = (event) => {
    event.preventDefault();
    setLoading(true);
    setError('');
    // Simula chamada de API
    setTimeout(() => {
      const { email, password } = event.target.elements;
      if (email.value === 'user@test.com' && password.value === 'password') {
        // sucesso
      } else {
        setError('Email ou senha inválidos.');
      }
      setLoading(false);
    }, 1000);
  };

  return (
    <form onSubmit={handleSubmit}>
      {error && <div role="alert">{error}</div>}
      <label htmlFor="email">Email</label>
      <input type="email" id="email" />
      <label htmlFor="password">Senha</label>
      <input type="password" id="password" />
      <button type="submit" disabled={loading}>
        {loading ? 'Entrando...' : 'Entrar'}
      </button>
    </form>
  );
}
export default LoginForm;

Teste do LoginForm.js

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

test('deve exibir mensagem de erro com credenciais inválidas', async () => {
  const user = userEvent.setup();
  render(<LoginForm />);

  // Digita no campo de email
  await user.type(screen.getByLabelText(/email/i), 'wrong@test.com');
  // Digita no campo de senha
  await user.type(screen.getByLabelText(/senha/i), 'wrongpassword');
  // Clica no botão de entrar
  await user.click(screen.getByRole('button', { name: /entrar/i }));

  // O botão deve ficar em estado de loading
  expect(screen.getByRole('button', { name: /entrando.../i })).toBeDisabled();

  // Espera a mensagem de erro aparecer
  const errorMessage = await screen.findByRole('alert');
  expect(errorMessage).toHaveTextContent(/email ou senha inválidos/i);

  // O botão deve voltar ao estado normal
  expect(screen.getByRole('button', { name: /entrar/i })).not.toBeDisabled();
});

Conclusão

  • React Testing Library muda o paradigma para testes de comportamento.
  • Resulta em testes mais resilientes, fáceis de manter e que inspiram confiança.
  • Incentiva o desenvolvimento de aplicações mais acessíveis.
  • O trio render, screen e user-event é a base para a maioria dos seus testes de UI.

Perguntas?

end list