Domine a React Testing Library (RTL) com este guia completo. Aprenda a escrever testes eficazes, sustentáveis e centrados no utilizador para as suas aplicações React.
React Testing Library: Um Guia Abrangente
No cenário de desenvolvimento web acelerado de hoje, garantir a qualidade e a confiabilidade de suas aplicações React é fundamental. A React Testing Library (RTL) surgiu como uma solução popular e eficaz para escrever testes que se concentram na perspectiva do usuário. Este guia fornece uma visão geral completa da RTL, abrangendo desde os conceitos fundamentais até as técnicas avançadas, capacitando você a construir aplicações React robustas e sustentáveis.
Por que Escolher a React Testing Library?
As abordagens de teste tradicionais geralmente dependem de detalhes de implementação, tornando os testes frágeis e propensos a quebrar com pequenas alterações de código. A RTL, por outro lado, incentiva você a testar seus componentes como um usuário interagiria com eles, concentrando-se no que o usuário vê e experimenta. Essa abordagem oferece várias vantagens importantes:
- Testes Centrados no Usuário: A RTL promove a escrita de testes que refletem a perspectiva do usuário, garantindo que sua aplicação funcione como esperado do ponto de vista do usuário final.
- Redução da Fragilidade dos Testes: Ao evitar testar detalhes de implementação, os testes RTL são menos propensos a quebrar quando você refatora seu código, levando a testes mais sustentáveis e robustos.
- Design de Código Aprimorado: A RTL incentiva você a escrever componentes acessíveis e fáceis de usar, levando a um design de código geral melhor.
- Foco na Acessibilidade: A RTL facilita o teste da acessibilidade de seus componentes, garantindo que sua aplicação seja utilizável por todos.
- Processo de Teste Simplificado: A RTL fornece uma API simples e intuitiva, facilitando a escrita e a manutenção de testes.
Configurando Seu Ambiente de Teste
Antes de começar a usar a RTL, você precisa configurar seu ambiente de teste. Isso normalmente envolve a instalação das dependências necessárias e a configuração de sua estrutura de teste.
Pré-requisitos
- Node.js e npm (ou yarn): Certifique-se de ter o Node.js e o npm (ou yarn) instalados em seu sistema. Você pode baixá-los no site oficial do Node.js.
- Projeto React: Você deve ter um projeto React existente ou criar um novo usando o Create React App ou uma ferramenta semelhante.
Instalação
Instale os seguintes pacotes usando npm ou yarn:
npm install --save-dev @testing-library/react @testing-library/jest-dom jest babel-jest @babel/preset-env @babel/preset-react
Ou, usando yarn:
yarn add --dev @testing-library/react @testing-library/jest-dom jest babel-jest @babel/preset-env @babel/preset-react
Explicação dos Pacotes:
- @testing-library/react: A biblioteca principal para testar componentes React.
- @testing-library/jest-dom: Fornece matchers Jest personalizados para asserções sobre nós DOM.
- Jest: Uma estrutura popular de teste JavaScript.
- babel-jest: Um transformador Jest que usa Babel para compilar seu código.
- @babel/preset-env: Um preset Babel que determina os plugins e presets Babel necessários para suportar seus ambientes de destino.
- @babel/preset-react: Um preset Babel para React.
Configuração
Crie um arquivo `babel.config.js` na raiz do seu projeto com o seguinte conteúdo:
module.exports = {
presets: ['@babel/preset-env', '@babel/preset-react'],
};
Atualize seu arquivo `package.json` para incluir um script de teste:
{
"scripts": {
"test": "jest"
}
}
Crie um arquivo `jest.config.js` na raiz do seu projeto para configurar o Jest. Uma configuração mínima pode ser assim:
module.exports = {
testEnvironment: 'jsdom',
setupFilesAfterEnv: ['/src/setupTests.js'],
};
Crie um arquivo `src/setupTests.js` com o seguinte conteúdo. Isso garante que os matchers Jest DOM estejam disponíveis em todos os seus testes:
import '@testing-library/jest-dom/extend-expect';
Escrevendo Seu Primeiro Teste
Vamos começar com um exemplo simples. Suponha que você tenha um componente React que exibe uma mensagem de saudação:
// src/components/Greeting.js
import React from 'react';
function Greeting({ name }) {
return <h1>Hello, {name}!</h1>;
}
export default Greeting;
Agora, vamos escrever um teste para este componente:
// src/components/Greeting.test.js
import React from 'react';
import { render, screen } from '@testing-library/react';
import Greeting from './Greeting';
test('renders a greeting message', () => {
render(<Greeting name="World" />);
const greetingElement = screen.getByText(/Hello, World!/i);
expect(greetingElement).toBeInTheDocument();
});
Explicação:
- `render`: Esta função renderiza o componente no DOM.
- `screen`: Este objeto fornece métodos para consultar o DOM.
- `getByText`: Este método encontra um elemento pelo seu conteúdo de texto. A flag `/i` torna a pesquisa insensível a maiúsculas e minúsculas.
- `expect`: Esta função é usada para fazer asserções sobre o comportamento do componente.
- `toBeInTheDocument`: Este matcher afirma que o elemento está presente no DOM.
Para executar o teste, execute o seguinte comando no seu terminal:
npm test
Se tudo estiver configurado corretamente, o teste deverá passar.
Consultas RTL Comuns
A RTL fornece vários métodos de consulta para encontrar elementos no DOM. Essas consultas são projetadas para imitar como os usuários interagem com sua aplicação.`getByRole`
Esta consulta encontra um elemento por sua função ARIA. É uma boa prática usar `getByRole` sempre que possível, pois promove a acessibilidade e garante que seus testes sejam resilientes a alterações na estrutura DOM subjacente.
<button role="button">Click me</button>
const buttonElement = screen.getByRole('button');
expect(buttonElement).toBeInTheDocument();
`getByLabelText`
Esta consulta encontra um elemento pelo texto de seu rótulo associado. É útil para testar elementos de formulário.
<label htmlFor="name">Name:</label>
<input type="text" id="name" />
const nameInputElement = screen.getByLabelText('Name:');
expect(nameInputElement).toBeInTheDocument();
`getByPlaceholderText`
Esta consulta encontra um elemento pelo seu texto de espaço reservado.
<input type="text" placeholder="Enter your email" />
const emailInputElement = screen.getByPlaceholderText('Enter your email');
expect(emailInputElement).toBeInTheDocument();
`getByAltText`
Esta consulta encontra um elemento de imagem pelo seu texto alternativo. É importante fornecer texto alternativo significativo para todas as imagens para garantir a acessibilidade.
<img src="logo.png" alt="Company Logo" />
const logoImageElement = screen.getByAltText('Company Logo');
expect(logoImageElement).toBeInTheDocument();
`getByTitle`
Esta consulta encontra um elemento pelo seu atributo de título.
<span title="Close">X</span>
const closeElement = screen.getByTitle('Close');
expect(closeElement).toBeInTheDocument();
`getByDisplayValue`
Esta consulta encontra um elemento pelo seu valor de exibição. Isso é útil para testar entradas de formulário com valores pré-preenchidos.
<input type="text" value="Initial Value" />
const inputElement = screen.getByDisplayValue('Initial Value');
expect(inputElement).toBeInTheDocument();
Consultas `getAllBy*`
Além das consultas `getBy*`, a RTL também fornece consultas `getAllBy*`, que retornam um array de elementos correspondentes. Elas são úteis quando você precisa afirmar que vários elementos com as mesmas características estão presentes no DOM.
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
const listItems = screen.getAllByRole('listitem');
expect(listItems).toHaveLength(3);
Consultas `queryBy*`
As consultas `queryBy*` são semelhantes às consultas `getBy*`, mas retornam `null` se nenhum elemento correspondente for encontrado, em vez de lançar um erro. Isso é útil quando você deseja afirmar que um elemento *não* está presente no DOM.
const missingElement = screen.queryByText('Non-existent text');
expect(missingElement).toBeNull();
Consultas `findBy*`
As consultas `findBy*` são versões assíncronas das consultas `getBy*`. Elas retornam uma Promise que é resolvida quando o elemento correspondente é encontrado. Elas são úteis para testar operações assíncronas, como buscar dados de uma API.
// Simulating an asynchronous data fetch
const fetchData = () => new Promise(resolve => {
setTimeout(() => resolve('Data Loaded!'), 1000);
});
function MyComponent() {
const [data, setData] = React.useState(null);
React.useEffect(() => {
fetchData().then(setData);
}, []);
return <div>{data}</div>;
}
test('loads data asynchronously', async () => {
render(<MyComponent />);
const dataElement = await screen.findByText('Data Loaded!');
expect(dataElement).toBeInTheDocument();
});
Simulando Interações do Usuário
A RTL fornece as APIs `fireEvent` e `userEvent` para simular interações do usuário, como clicar em botões, digitar em campos de entrada e enviar formulários.`fireEvent`
`fireEvent` permite que você acione programaticamente eventos DOM. É uma API de nível inferior que oferece controle refinado sobre os eventos que são acionados.
<button onClick={() => alert('Button clicked!')}>Click me</button>
import { fireEvent } from '@testing-library/react';
test('simulates a button click', () => {
const alertMock = jest.spyOn(window, 'alert').mockImplementation(() => {});
render(<button onClick={() => alert('Button clicked!')}>Click me</button>);
const buttonElement = screen.getByRole('button');
fireEvent.click(buttonElement);
expect(alertMock).toHaveBeenCalledTimes(1);
alertMock.mockRestore();
});
`userEvent`
`userEvent` é uma API de nível superior que simula interações do usuário de forma mais realista. Ela lida com detalhes como gerenciamento de foco e ordenação de eventos, tornando seus testes mais robustos e menos frágeis.
<input type="text" onChange={e => console.log(e.target.value)} />
import userEvent from '@testing-library/user-event';
test('simulates typing in an input field', () => {
const inputElement = screen.getByRole('textbox');
userEvent.type(inputElement, 'Hello, world!');
expect(inputElement).toHaveValue('Hello, world!');
});
Testando Código Assíncrono
Muitas aplicações React envolvem operações assíncronas, como buscar dados de uma API. A RTL fornece várias ferramentas para testar código assíncrono.`waitFor`
`waitFor` permite que você espere que uma condição se torne verdadeira antes de fazer uma asserção. É útil para testar operações assíncronas que levam algum tempo para serem concluídas.
function MyComponent() {
const [data, setData] = React.useState(null);
React.useEffect(() => {
setTimeout(() => {
setData('Data loaded!');
}, 1000);
}, []);
return <div>{data}</div>;
}
import { waitFor } from '@testing-library/react';
test('waits for data to load', async () => {
render(<MyComponent />);
await waitFor(() => screen.getByText('Data loaded!'));
const dataElement = screen.getByText('Data loaded!');
expect(dataElement).toBeInTheDocument();
});
Consultas `findBy*`
Como mencionado anteriormente, as consultas `findBy*` são assíncronas e retornam uma Promise que é resolvida quando o elemento correspondente é encontrado. Elas são úteis para testar operações assíncronas que resultam em alterações no DOM.
Testando Hooks
React Hooks são funções reutilizáveis que encapsulam a lógica stateful. A RTL fornece a utilidade `renderHook` de `@testing-library/react-hooks` (que foi descontinuada em favor de `@testing-library/react` diretamente a partir da v17) para testar Hooks personalizados isoladamente.
// src/hooks/useCounter.js
import { useState } from 'react';
function useCounter(initialValue = 0) {
const [count, setCount] = useState(initialValue);
const increment = () => {
setCount(prevCount => prevCount + 1);
};
const decrement = () => {
setCount(prevCount => prevCount - 1);
};
return { count, increment, decrement };
}
export default useCounter;
// src/hooks/useCounter.test.js
import { renderHook, act } from '@testing-library/react';
import useCounter from './useCounter';
test('increments the counter', () => {
const { result } = renderHook(() => useCounter());
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
Explicação:
- `renderHook`: Esta função renderiza o Hook e retorna um objeto contendo o resultado do Hook.
- `act`: Esta função é usada para envolver qualquer código que cause atualizações de estado. Isso garante que o React possa agrupar e processar adequadamente as atualizações.
Técnicas Avançadas de Teste
Depois de dominar o básico da RTL, você pode explorar técnicas de teste mais avançadas para melhorar a qualidade e a sustentabilidade de seus testes.
Mocking de Módulos
Às vezes, pode ser necessário simular módulos ou dependências externos para isolar seus componentes e controlar seu comportamento durante o teste. O Jest fornece uma API de mocking poderosa para esse fim.
// src/api/dataService.js
export const fetchData = async () => {
const response = await fetch('/api/data');
const data = await response.json();
return data;
};
// src/components/MyComponent.js
import React, { useState, useEffect } from 'react';
import { fetchData } from '../api/dataService';
function MyComponent() {
const [data, setData] = useState(null);
useEffect(() => {
fetchData().then(setData);
}, []);
return <div>{data}</div>;
}
// src/components/MyComponent.test.js
import { render, screen, waitFor } from '@testing-library/react';
import MyComponent from './MyComponent';
import * as dataService from '../api/dataService';
jest.mock('../api/dataService');
test('fetches data from the API', async () => {
dataService.fetchData.mockResolvedValue({ message: 'Mocked data!' });
render(<MyComponent />);
await waitFor(() => screen.getByText('Mocked data!'));
expect(screen.getByText('Mocked data!')).toBeInTheDocument();
expect(dataService.fetchData).toHaveBeenCalledTimes(1);
});
Explicação:
- `jest.mock('../api/dataService')`: Esta linha simula o módulo `dataService`.
- `dataService.fetchData.mockResolvedValue({ message: 'Mocked data!' })`: Esta linha configura a função `fetchData` simulada para retornar uma Promise que é resolvida com os dados especificados.
- `expect(dataService.fetchData).toHaveBeenCalledTimes(1)`: Esta linha afirma que a função `fetchData` simulada foi chamada uma vez.
Provedores de Contexto
Se o seu componente depender de um Provedor de Contexto, você precisará envolver seu componente no provedor durante o teste. Isso garante que o componente tenha acesso aos valores do contexto.
// src/contexts/ThemeContext.js
import React, { createContext, useState } from 'react';
export const ThemeContext = createContext();
export function ThemeProvider({ children }) {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(prevTheme => (prevTheme === 'light' ? 'dark' : 'light'));
};
return (
<ThemeContext.Provider value={{ theme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
// src/components/MyComponent.js
import React, { useContext } from 'react';
import { ThemeContext } from '../contexts/ThemeContext';
function MyComponent() {
const { theme, toggleTheme } = useContext(ThemeContext);
return (
<div style={{ backgroundColor: theme === 'light' ? '#fff' : '#000', color: theme === 'light' ? '#000' : '#fff' }}>
<p>Current theme: {theme}</p>
<button onClick={toggleTheme}>Toggle Theme</button>
</div>
);
}
// src/components/MyComponent.test.js
import { render, screen, fireEvent } from '@testing-library/react';
import MyComponent from './MyComponent';
import { ThemeProvider } from '../contexts/ThemeContext';
test('toggles the theme', () => {
render(
<ThemeProvider>
<MyComponent />
</ThemeProvider>
);
const themeParagraph = screen.getByText(/Current theme: light/i);
const toggleButton = screen.getByRole('button', { name: /Toggle Theme/i });
expect(themeParagraph).toBeInTheDocument();
fireEvent.click(toggleButton);
expect(screen.getByText(/Current theme: dark/i)).toBeInTheDocument();
});
Explicação:
- Envolvemos o `MyComponent` em `ThemeProvider` para fornecer o contexto necessário durante o teste.
Testando com Router
Ao testar componentes que usam React Router, você precisará fornecer um contexto Router simulado. Você pode conseguir isso usando o componente `MemoryRouter` de `react-router-dom`.
// src/components/MyComponent.js
import React from 'react';
import { Link } from 'react-router-dom';
function MyComponent() {
return (
<div>
<Link to="/about">Go to About Page</Link>
</div>
);
}
// src/components/MyComponent.test.js
import { render, screen } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import MyComponent from './MyComponent';
test('renders a link to the about page', () => {
render(
<MemoryRouter>
<MyComponent />
</MemoryRouter>
);
const linkElement = screen.getByRole('link', { name: /Go to About Page/i });
expect(linkElement).toBeInTheDocument();
expect(linkElement).toHaveAttribute('href', '/about');
});
Explicação:
- Envolvemos o `MyComponent` em `MemoryRouter` para fornecer um contexto Router simulado.
- Afirmamos que o elemento de link tem o atributo `href` correto.
Melhores Práticas para Escrever Testes Eficazes
Aqui estão algumas das melhores práticas a seguir ao escrever testes com RTL:
- Concentre-se nas Interações do Usuário: Escreva testes que simulem como os usuários interagem com sua aplicação.
- Evite Testar Detalhes de Implementação: Não teste o funcionamento interno de seus componentes. Em vez disso, concentre-se no comportamento observável.
- Escreva Testes Claros e Concisos: Torne seus testes fáceis de entender e manter.
- Use Nomes de Teste Significativos: Escolha nomes de teste que descrevam com precisão o comportamento que está sendo testado.
- Mantenha os Testes Isolados: Evite dependências entre os testes. Cada teste deve ser independente e autocontido.
- Teste Casos Limite: Não teste apenas o caminho feliz. Certifique-se de testar também os casos limite e as condições de erro.
- Escreva Testes Antes de Codificar: Considere usar o Desenvolvimento Orientado a Testes (TDD) para escrever testes antes de escrever seu código.
- Siga o Padrão "AAA": Arrange, Act, Assert. Esse padrão ajuda a estruturar seus testes e torná-los mais legíveis.
- Mantenha seus testes rápidos: Testes lentos podem desencorajar os desenvolvedores de executá-los com frequência. Otimize seus testes para velocidade, simulando solicitações de rede e minimizando a quantidade de manipulação do DOM.
- Use mensagens de erro descritivas: Quando as asserções falham, as mensagens de erro devem fornecer informações suficientes para identificar rapidamente a causa da falha.