Português

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:

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

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:

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:

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:

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:

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:

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:

Melhores Práticas para Escrever Testes Eficazes

Aqui estão algumas das melhores práticas a seguir ao escrever testes com RTL:

Conclusão

A React Testing Library é uma ferramenta poderosa para escrever testes eficazes, sustentáveis e centrados no usuário para suas aplicações React. Ao seguir os princípios e técnicas descritos neste guia, você pode construir aplicações robustas e confiáveis que atendam às necessidades de seus usuários. Lembre-se de se concentrar em testar da perspectiva do usuário, evitar testar detalhes de implementação e escrever testes claros e concisos. Ao adotar a RTL e adotar as melhores práticas, você pode melhorar significativamente a qualidade e a sustentabilidade de seus projetos React, independentemente de sua localização ou dos requisitos específicos de seu público global.