Русский

Освойте React Testing Library (RTL) с помощью этого полного руководства. Узнайте, как писать эффективные, поддерживаемые и ориентированные на пользователя тесты для ваших React-приложений, уделяя внимание лучшим практикам и реальным примерам.

React Testing Library: Полное руководство

В современном быстро меняющемся мире веб-разработки обеспечение качества и надежности ваших React-приложений имеет первостепенное значение. React Testing Library (RTL) стала популярным и эффективным решением для написания тестов, ориентированных на пользователя. Это руководство представляет собой полный обзор RTL, охватывающий все, от фундаментальных концепций до продвинутых техник, и поможет вам создавать надежные и поддерживаемые React-приложения.

Почему стоит выбрать React Testing Library?

Традиционные подходы к тестированию часто полагаются на детали реализации, что делает тесты хрупкими и склонными к поломке при незначительных изменениях кода. RTL, с другой стороны, поощряет тестирование компонентов так, как с ними взаимодействовал бы пользователь, фокусируясь на том, что пользователь видит и испытывает. Этот подход предлагает несколько ключевых преимуществ:

Настройка тестовой среды

Прежде чем вы сможете начать использовать RTL, вам необходимо настроить вашу тестовую среду. Обычно это включает установку необходимых зависимостей и настройку фреймворка для тестирования.

Предварительные требования

Установка

Установите следующие пакеты с помощью npm или yarn:

npm install --save-dev @testing-library/react @testing-library/jest-dom jest babel-jest @babel/preset-env @babel/preset-react

Или, используя yarn:

yarn add --dev @testing-library/react @testing-library/jest-dom jest babel-jest @babel/preset-env @babel/preset-react

Объяснение пакетов:

Конфигурация

Создайте файл `babel.config.js` в корне вашего проекта со следующим содержимым:

module.exports = {
  presets: ['@babel/preset-env', '@babel/preset-react'],
};

Обновите ваш файл `package.json`, чтобы добавить скрипт для тестирования:

{
  "scripts": {
    "test": "jest"
  }
}

Создайте файл `jest.config.js` в корне вашего проекта для настройки Jest. Минимальная конфигурация может выглядеть так:

module.exports = {
  testEnvironment: 'jsdom',
  setupFilesAfterEnv: ['/src/setupTests.js'],
};

Создайте файл `src/setupTests.js` со следующим содержимым. Это гарантирует, что матчеры Jest DOM будут доступны во всех ваших тестах:

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

Написание вашего первого теста

Давайте начнем с простого примера. Предположим, у вас есть React-компонент, который отображает приветственное сообщение:

// src/components/Greeting.js
import React from 'react';

function Greeting({ name }) {
  return <h1>Привет, {name}!</h1>;
}

export default Greeting;

Теперь давайте напишем тест для этого компонента:

// src/components/Greeting.test.js
import React from 'react';
import { render, screen } from '@testing-library/react';
import Greeting from './Greeting';

test('отображает приветственное сообщение', () => {
  render(<Greeting name="Мир" />);
  const greetingElement = screen.getByText(/Привет, Мир!/i);
  expect(greetingElement).toBeInTheDocument();
});

Объяснение:

Чтобы запустить тест, выполните следующую команду в вашем терминале:

npm test

Если все настроено правильно, тест должен пройти.

Основные запросы RTL

RTL предоставляет различные методы запросов для поиска элементов в DOM. Эти запросы разработаны так, чтобы имитировать взаимодействие пользователей с вашим приложением.

`getByRole`

Этот запрос находит элемент по его ARIA-роли. Рекомендуется использовать `getByRole` везде, где это возможно, так как это способствует доступности и обеспечивает устойчивость ваших тестов к изменениям в базовой структуре DOM.

<button role="button">Нажми меня</button>
const buttonElement = screen.getByRole('button');
expect(buttonElement).toBeInTheDocument();

`getByLabelText`

Этот запрос находит элемент по тексту связанной с ним метки. Он полезен для тестирования элементов форм.

<label htmlFor="name">Имя:</label>
<input type="text" id="name" />
const nameInputElement = screen.getByLabelText('Имя:');
expect(nameInputElement).toBeInTheDocument();

`getByPlaceholderText`

Этот запрос находит элемент по его плейсхолдеру.

<input type="text" placeholder="Введите ваш email" />
const emailInputElement = screen.getByPlaceholderText('Введите ваш email');
expect(emailInputElement).toBeInTheDocument();

`getByAltText`

Этот запрос находит элемент изображения по его alt-тексту. Важно предоставлять осмысленный alt-текст для всех изображений, чтобы обеспечить доступность.

<img src="logo.png" alt="Логотип компании" />
const logoImageElement = screen.getByAltText('Логотип компании');
expect(logoImageElement).toBeInTheDocument();

`getByTitle`

Этот запрос находит элемент по его атрибуту title.

<span title="Закрыть">X</span>
const closeElement = screen.getByTitle('Закрыть');
expect(closeElement).toBeInTheDocument();

`getByDisplayValue`

Этот запрос находит элемент по его отображаемому значению. Это полезно для тестирования полей ввода форм с предварительно заполненными значениями.

<input type="text" value="Начальное значение" />
const inputElement = screen.getByDisplayValue('Начальное значение');
expect(inputElement).toBeInTheDocument();

Запросы `getAllBy*`

В дополнение к запросам `getBy*`, RTL также предоставляет запросы `getAllBy*`, которые возвращают массив совпадающих элементов. Они полезны, когда вам нужно утверждать, что в DOM присутствует несколько элементов с одинаковыми характеристиками.

<li>Элемент 1</li>
<li>Элемент 2</li>
<li>Элемент 3</li>
const listItems = screen.getAllByRole('listitem');
expect(listItems).toHaveLength(3);

Запросы `queryBy*`

Запросы `queryBy*` похожи на запросы `getBy*`, но они возвращают `null`, если соответствующий элемент не найден, вместо того чтобы выбрасывать ошибку. Это полезно, когда вы хотите утверждать, что элемент *не* присутствует в DOM.

const missingElement = screen.queryByText('Несуществующий текст');
expect(missingElement).toBeNull();

Запросы `findBy*`

Запросы `findBy*` являются асинхронными версиями запросов `getBy*`. Они возвращают Promise, который разрешается, когда найден соответствующий элемент. Они полезны для тестирования асинхронных операций, таких как получение данных из API.

// Симуляция асинхронной загрузки данных
const fetchData = () => new Promise(resolve => {
  setTimeout(() => resolve('Данные загружены!'), 1000);
});

function MyComponent() {
  const [data, setData] = React.useState(null);

  React.useEffect(() => {
    fetchData().then(setData);
  }, []);

  return <div>{data}</div>;
}
test('асинхронно загружает данные', async () => {
  render(<MyComponent />);
  const dataElement = await screen.findByText('Данные загружены!');
  expect(dataElement).toBeInTheDocument();
});

Симуляция взаимодействий с пользователем

RTL предоставляет API `fireEvent` и `userEvent` для симуляции взаимодействий пользователя, таких как нажатие кнопок, ввод текста в поля и отправка форм.

`fireEvent`

`fireEvent` позволяет программно вызывать события DOM. Это низкоуровневый API, который дает вам детальный контроль над вызываемыми событиями.

<button onClick={() => alert('Кнопка нажата!')}>Нажми меня</button>
import { fireEvent } from '@testing-library/react';

test('симулирует нажатие кнопки', () => {
  const alertMock = jest.spyOn(window, 'alert').mockImplementation(() => {});
  render(<button onClick={() => alert('Кнопка нажата!')}>Нажми меня</button>);
  const buttonElement = screen.getByRole('button');
  fireEvent.click(buttonElement);
  expect(alertMock).toHaveBeenCalledTimes(1);
  alertMock.mockRestore();
});

`userEvent`

`userEvent` — это API более высокого уровня, который более реалистично симулирует взаимодействия пользователя. Он обрабатывает такие детали, как управление фокусом и порядок событий, делая ваши тесты более надежными и менее хрупкими.

<input type="text" onChange={e => console.log(e.target.value)} />
import userEvent from '@testing-library/user-event';

test('симулирует ввод текста в поле', () => {
  render(<input type="text" />);
  const inputElement = screen.getByRole('textbox');
  userEvent.type(inputElement, 'Привет, мир!');
  expect(inputElement).toHaveValue('Привет, мир!');
});

Тестирование асинхронного кода

Многие React-приложения включают асинхронные операции, такие как получение данных из API. RTL предоставляет несколько инструментов для тестирования асинхронного кода.

`waitFor`

`waitFor` позволяет вам дождаться, пока условие не станет истинным, прежде чем делать утверждение. Это полезно для тестирования асинхронных операций, которые занимают некоторое время для завершения.

function MyComponent() {
  const [data, setData] = React.useState(null);

  React.useEffect(() => {
    setTimeout(() => {
      setData('Данные загружены!');
    }, 1000);
  }, []);

  return <div>{data}</div>;
}
import { waitFor } from '@testing-library/react';

test('ожидает загрузки данных', async () => {
  render(<MyComponent />);
  await waitFor(() => screen.getByText('Данные загружены!'));
  const dataElement = screen.getByText('Данные загружены!');
  expect(dataElement).toBeInTheDocument();
});

Запросы `findBy*`

Как уже упоминалось, запросы `findBy*` являются асинхронными и возвращают Promise, который разрешается, когда найден соответствующий элемент. Они полезны для тестирования асинхронных операций, которые приводят к изменениям в DOM.

Тестирование хуков

Хуки React — это повторно используемые функции, инкапсулирующие логику с состоянием. RTL предоставляет утилиту `renderHook` из `@testing-library/react-hooks` (которая устарела в пользу `@testing-library/react` напрямую начиная с v17) для тестирования пользовательских хуков в изоляции.

// 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('увеличивает счетчик', () => {
  const { result } = renderHook(() => useCounter());

  act(() => {
    result.current.increment();
  });

  expect(result.current.count).toBe(1);
});

Объяснение:

Продвинутые техники тестирования

Как только вы освоите основы RTL, вы можете изучить более продвинутые техники тестирования для улучшения качества и поддерживаемости ваших тестов.

Мокирование модулей

Иногда вам может потребоваться мокировать (имитировать) внешние модули или зависимости, чтобы изолировать ваши компоненты и контролировать их поведение во время тестирования. Jest предоставляет для этого мощный API для мокирования.

// 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 ? data.message : null}</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('получает данные из API', async () => {
  dataService.fetchData.mockResolvedValue({ message: 'Имитированные данные!' });

  render(<MyComponent />);

  await waitFor(() => screen.getByText('Имитированные данные!'));

  expect(screen.getByText('Имитированные данные!')).toBeInTheDocument();
  expect(dataService.fetchData).toHaveBeenCalledTimes(1);
});

Объяснение:

Провайдеры контекста

Если ваш компонент зависит от провайдера контекста, вам нужно будет обернуть ваш компонент в провайдер во время тестирования. Это гарантирует, что компонент будет иметь доступ к значениям контекста.

// src/contexts/ThemeContext.js
import React, { createContext, useState } from 'react';

export const ThemeContext = createContext();

export function ThemeProvider({ children }) {
  const [theme, setTheme] = useState('светлая');

  const toggleTheme = () => {
    setTheme(prevTheme => (prevTheme === 'светлая' ? 'темная' : 'светлая'));
  };

  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 === 'светлая' ? '#fff' : '#000', color: theme === 'светлая' ? '#000' : '#fff' }}>
      <p>Текущая тема: {theme}</p>
      <button onClick={toggleTheme}>Сменить тему</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('переключает тему', () => {
  render(
    <ThemeProvider>
      <MyComponent />
    </ThemeProvider>
  );

  const themeParagraph = screen.getByText(/Текущая тема: светлая/i);
  const toggleButton = screen.getByRole('button', { name: /Сменить тему/i });

  expect(themeParagraph).toBeInTheDocument();

  fireEvent.click(toggleButton);

  expect(screen.getByText(/Текущая тема: темная/i)).toBeInTheDocument();
});

Объяснение:

Тестирование с роутером

При тестировании компонентов, использующих React Router, вам потребуется предоставить мок-контекст роутера. Вы можете достичь этого, используя компонент `MemoryRouter` из `react-router-dom`.

// src/components/MyComponent.js
import React from 'react';
import { Link } from 'react-router-dom';

function MyComponent() {
  return (
    <div>
      <Link to="/about">Перейти на страницу "О нас"</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('отображает ссылку на страницу \"О нас\"', () => {
  render(
    <MemoryRouter>
      <MyComponent />
    </MemoryRouter>
  );

  const linkElement = screen.getByRole('link', { name: /Перейти на страницу "О нас"/i });
  expect(linkElement).toBeInTheDocument();
  expect(linkElement).toHaveAttribute('href', '/about');
});

Объяснение:

Лучшие практики написания эффективных тестов

Вот некоторые лучшие практики, которым следует следовать при написании тестов с помощью RTL:

Заключение

React Testing Library — это мощный инструмент для написания эффективных, поддерживаемых и ориентированных на пользователя тестов для ваших React-приложений. Следуя принципам и техникам, изложенным в этом руководстве, вы сможете создавать надежные и стабильные приложения, отвечающие потребностям ваших пользователей. Не забывайте фокусироваться на тестировании с точки зрения пользователя, избегать тестирования деталей реализации и писать понятные и лаконичные тесты. Используя RTL и придерживаясь лучших практик, вы сможете значительно улучшить качество и поддерживаемость ваших React-проектов, независимо от вашего местоположения или конкретных требований вашей глобальной аудитории.