Освойте React Testing Library (RTL) с помощью этого полного руководства. Узнайте, как писать эффективные, поддерживаемые и ориентированные на пользователя тесты для ваших React-приложений, уделяя внимание лучшим практикам и реальным примерам.
React Testing Library: Полное руководство
В современном быстро меняющемся мире веб-разработки обеспечение качества и надежности ваших React-приложений имеет первостепенное значение. React Testing Library (RTL) стала популярным и эффективным решением для написания тестов, ориентированных на пользователя. Это руководство представляет собой полный обзор RTL, охватывающий все, от фундаментальных концепций до продвинутых техник, и поможет вам создавать надежные и поддерживаемые React-приложения.
Почему стоит выбрать React Testing Library?
Традиционные подходы к тестированию часто полагаются на детали реализации, что делает тесты хрупкими и склонными к поломке при незначительных изменениях кода. RTL, с другой стороны, поощряет тестирование компонентов так, как с ними взаимодействовал бы пользователь, фокусируясь на том, что пользователь видит и испытывает. Этот подход предлагает несколько ключевых преимуществ:
- Тестирование, ориентированное на пользователя: RTL способствует написанию тестов, которые отражают точку зрения пользователя, гарантируя, что ваше приложение функционирует так, как ожидается конечным пользователем.
- Снижение хрупкости тестов: Избегая тестирования деталей реализации, тесты RTL с меньшей вероятностью ломаются при рефакторинге кода, что приводит к более поддерживаемым и надежным тестам.
- Улучшенный дизайн кода: RTL поощряет написание компонентов, которые доступны и просты в использовании, что ведет к улучшению общего дизайна кода.
- Фокус на доступности: RTL упрощает тестирование доступности ваших компонентов, обеспечивая возможность использования вашего приложения всеми.
- Упрощенный процесс тестирования: RTL предоставляет простой и интуитивно понятный API, что облегчает написание и поддержку тестов.
Настройка тестовой среды
Прежде чем вы сможете начать использовать RTL, вам необходимо настроить вашу тестовую среду. Обычно это включает установку необходимых зависимостей и настройку фреймворка для тестирования.
Предварительные требования
- Node.js и npm (или yarn): Убедитесь, что на вашей системе установлены Node.js и npm (или yarn). Вы можете скачать их с официального сайта Node.js.
- Проект на React: У вас должен быть существующий проект на React или вы можете создать новый с помощью Create React App или подобного инструмента.
Установка
Установите следующие пакеты с помощью 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
Объяснение пакетов:
- @testing-library/react: Основная библиотека для тестирования React-компонентов.
- @testing-library/jest-dom: Предоставляет пользовательские матчеры Jest для утверждений о DOM-узлах.
- Jest: Популярный фреймворк для тестирования JavaScript.
- babel-jest: Трансформер Jest, который использует Babel для компиляции вашего кода.
- @babel/preset-env: Пресет Babel, который определяет плагины и пресеты Babel, необходимые для поддержки ваших целевых сред.
- @babel/preset-react: Пресет Babel для 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();
});
Объяснение:
- `render`: Эта функция рендерит компонент в DOM.
- `screen`: Этот объект предоставляет методы для запросов к DOM.
- `getByText`: Этот метод находит элемент по его текстовому содержимому. Флаг `/i` делает поиск нечувствительным к регистру.
- `expect`: Эта функция используется для утверждений о поведении компонента.
- `toBeInTheDocument`: Этот матчер утверждает, что элемент присутствует в DOM.
Чтобы запустить тест, выполните следующую команду в вашем терминале:
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);
});
Объяснение:
- `renderHook`: Эта функция рендерит хук и возвращает объект, содержащий результат хука.
- `act`: Эта функция используется для обертывания любого кода, который вызывает обновления состояния. Это гарантирует, что React сможет правильно сгруппировать и обработать обновления.
Продвинутые техники тестирования
Как только вы освоите основы 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);
});
Объяснение:
- `jest.mock('../api/dataService')`: Эта строка мокирует модуль `dataService`.
- `dataService.fetchData.mockResolvedValue({ message: 'Имитированные данные!' })`: Эта строка настраивает мокированную функцию `fetchData` так, чтобы она возвращала Promise, который разрешается с указанными данными.
- `expect(dataService.fetchData).toHaveBeenCalledTimes(1)`: Эта строка утверждает, что мокированная функция `fetchData` была вызвана один раз.
Провайдеры контекста
Если ваш компонент зависит от провайдера контекста, вам нужно будет обернуть ваш компонент в провайдер во время тестирования. Это гарантирует, что компонент будет иметь доступ к значениям контекста.
// 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();
});
Объяснение:
- Мы оборачиваем `MyComponent` в `ThemeProvider`, чтобы предоставить необходимый контекст во время тестирования.
Тестирование с роутером
При тестировании компонентов, использующих 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');
});
Объяснение:
- Мы оборачиваем `MyComponent` в `MemoryRouter`, чтобы предоставить мок-контекст роутера.
- Мы утверждаем, что элемент ссылки имеет правильный атрибут `href`.
Лучшие практики написания эффективных тестов
Вот некоторые лучшие практики, которым следует следовать при написании тестов с помощью RTL:
- Фокусируйтесь на взаимодействиях с пользователем: Пишите тесты, которые симулируют, как пользователи взаимодействуют с вашим приложением.
- Избегайте тестирования деталей реализации: Не тестируйте внутреннее устройство ваших компонентов. Вместо этого сосредоточьтесь на наблюдаемом поведении.
- Пишите понятные и лаконичные тесты: Делайте ваши тесты легкими для понимания и поддержки.
- Используйте осмысленные имена для тестов: Выбирайте имена тестов, которые точно описывают тестируемое поведение.
- Держите тесты изолированными: Избегайте зависимостей между тестами. Каждый тест должен быть независимым и самодостаточным.
- Тестируйте крайние случаи: Не тестируйте только "счастливый путь". Убедитесь, что вы также тестируете крайние случаи и условия ошибок.
- Пишите тесты перед кодом: Рассмотрите возможность использования разработки через тестирование (TDD), чтобы писать тесты до написания кода.
- Следуйте паттерну "AAA": Arrange, Act, Assert (Подготовка, Действие, Проверка). Этот паттерн помогает структурировать ваши тесты и делает их более читабельными.
- Обеспечьте быстрое выполнение тестов: Медленные тесты могут отбить у разработчиков желание запускать их часто. Оптимизируйте скорость ваших тестов, мокируя сетевые запросы и минимизируя манипуляции с DOM.
- Используйте описательные сообщения об ошибках: Когда утверждения не проходят, сообщения об ошибках должны предоставлять достаточно информации для быстрого определения причины сбоя.
Заключение
React Testing Library — это мощный инструмент для написания эффективных, поддерживаемых и ориентированных на пользователя тестов для ваших React-приложений. Следуя принципам и техникам, изложенным в этом руководстве, вы сможете создавать надежные и стабильные приложения, отвечающие потребностям ваших пользователей. Не забывайте фокусироваться на тестировании с точки зрения пользователя, избегать тестирования деталей реализации и писать понятные и лаконичные тесты. Используя RTL и придерживаясь лучших практик, вы сможете значительно улучшить качество и поддерживаемость ваших React-проектов, независимо от вашего местоположения или конкретных требований вашей глобальной аудитории.