Опануйте 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`: Ця функція використовується для створення тверджень (assertions) щодо поведінки компонента.
- `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`
Цей запит знаходить елемент за його текстом-заповнювачем (placeholder).
<input type="text" placeholder="Введіть ваш email" />
const emailInputElement = screen.getByPlaceholderText('Введіть ваш email');
expect(emailInputElement).toBeInTheDocument();
`getByAltText`
Цей запит знаходить елемент зображення за його альтернативним текстом (alt text). Важливо надавати змістовний 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('симулює введення тексту в поле вводу', () => {
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`, починаючи з версії 17) для тестування кастомних хуків в ізоляції.
// 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}</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` була викликана один раз.
Провайдери контексту
Якщо ваш компонент покладається на Провайдер контексту (Context Provider), вам потрібно буде обгорнути ваш компонент у провайдер під час тестування. Це гарантує, що компонент має доступ до значень контексту.
// 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>Поточна тема: {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(/Поточна тема: light/i);
const toggleButton = screen.getByRole('button', { name: /Перемкнути тему/i });
expect(themeParagraph).toBeInTheDocument();
fireEvent.click(toggleButton);
expect(screen.getByText(/Поточна тема: dark/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, незалежно від вашого місцезнаходження чи специфічних вимог вашої глобальної аудиторії.