Овладейте 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 matchers за потвърждаване на DOM възли.
- Jest: Популярна JavaScript рамка за тестване.
- babel-jest: Jest transformer, който използва Babel за компилиране на вашия код.
- @babel/preset-env: Babel preset, който определя Babel плъгините и presets, необходими за поддръжка на вашите целеви среди.
- @babel/preset-react: 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: ['<rootDir>/src/setupTests.js'],
};
Създайте файл `src/setupTests.js` със следното съдържание. Това гарантира, че Jest DOM matchers са налични във всички ваши тестове:
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="World" />);
const greetingElement = screen.getByText(/Здравей, World!/i);
expect(greetingElement).toBeInTheDocument();
});
Обяснение:
- `render`: Тази функция извежда компонента в DOM.
- `screen`: Този обект предоставя методи за заявка на DOM.
- `getByText`: Този метод намира елемент по неговото текстово съдържание. Флагът `/i` прави търсенето нечувствително към главни и малки букви.
- `expect`: Тази функция се използва за правене на твърдения за поведението на компонента.
- `toBeInTheDocument`: Този matcher твърди, че елементът присъства в 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="Въведете вашия имейл" />
const emailInputElement = screen.getByPlaceholderText('Въведете вашия имейл');
expect(emailInputElement).toBeInTheDocument();
`getByAltText`
Тази заявка намира елемент на изображение по неговия алтернативен текст. Важно е да предоставите смислен алтернативен текст за всички изображения, за да осигурите достъпност.
<img src="logo.png" alt="Лого на компанията" />
const logoImageElement = screen.getByAltText('Лого на компанията');
expect(logoImageElement).toBeInTheDocument();
`getByTitle`
Тази заявка намира елемент по неговия атрибут title.
<span title="Close">X</span>
const closeElement = screen.getByTitle('Close');
expect(closeElement).toBeInTheDocument();
`getByDisplayValue`
Тази заявка намира елемент по неговата показана стойност. Това е полезно за тестване на входни данни на формуляри с предварително попълнени стойности.
<input type="text" value="Първоначална стойност" />
const inputElement = screen.getByDisplayValue('Първоначална стойност');
expect(inputElement).toBeInTheDocument();
`getAllBy*` Queries
В допълнение към заявките `getBy*`, RTL предоставя и заявки `getAllBy*`, които връщат масив от съвпадащи елементи. Те са полезни, когато трябва да потвърдите, че в DOM присъстват множество елементи със същите характеристики.
<li>Елемент 1</li>
<li>Елемент 2</li>
<li>Елемент 3</li>
const listItems = screen.getAllByRole('listitem');
expect(listItems).toHaveLength(3);
`queryBy*` Queries
Заявките `queryBy*` са подобни на заявките `getBy*`, но връщат `null`, ако не бъде намерен съвпадащ елемент, вместо да генерират грешка. Това е полезно, когато искате да потвърдите, че елемент *не* присъства в DOM.
const missingElement = screen.queryByText('Несъществуващ текст');
expect(missingElement).toBeNull();
`findBy*` Queries
Заявките `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*` Queries
Както беше споменато по-рано, заявките `findBy*` са асинхронни и връщат Promise, който се разрешава, когато бъде намерен съвпадащият елемент. Те са полезни за тестване на асинхронни операции, които водят до промени в DOM.
Тестване на Hooks
React Hooks са функции за многократна употреба, които капсулират логика със състояние. RTL предоставя помощната програма `renderHook` от `@testing-library/react-hooks` (която е прекратена в полза на `@testing-library/react` директно от v17) за тестване на потребителски Hooks изолирано.
// 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`: Тази функция изобразява Hook и връща обект, съдържащ резултата от Hook.
- `act`: Тази функция се използва за обгръщане на всеки код, който причинява актуализации на състоянието. Това гарантира, че React може правилно да партидира и обработва актуализациите.
Напреднали техники за тестване
След като овладеете основите на RTL, можете да проучите по-напреднали техники за тестване, за да подобрите качеството и поддържането на вашите тестове.
Mocking Модули
Понякога може да се наложи да имитирате външни модули или зависимости, за да изолирате вашите компоненти и да контролирате тяхното поведение по време на тестване. 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: 'Mocked data!' });
render(<MyComponent />);
await waitFor(() => screen.getByText('Mocked data!'));
expect(screen.getByText('Mocked data!')).toBeInTheDocument();
expect(dataService.fetchData).toHaveBeenCalledTimes(1);
});
Обяснение:
- `jest.mock('../api/dataService')`: Този ред имитира модула `dataService`.
- `dataService.fetchData.mockResolvedValue({ message: 'Mocked data!' })`: Този ред конфигурира имитираната функция `fetchData` да връща Promise, който се разрешава със зададените данни.
- `expect(dataService.fetchData).toHaveBeenCalledTimes(1)`: Този ред твърди, че имитираната функция `fetchData` е била извикана веднъж.
Context Providers
Ако вашият компонент разчита на 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`, за да предоставим необходимия контекст по време на тестване.
Тестване с Router
Когато тествате компоненти, които използват React Router, ще трябва да предоставите имитиран контекст на 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`, за да предоставим имитиран контекст на Router.
- Потвърждаваме, че елементът на връзката има правилния атрибут `href`.
Най-добри практики за писане на ефективни тестове
Ето някои най-добри практики, които трябва да следвате при писането на тестове с RTL:
- Фокусирайте се върху потребителските взаимодействия: Напишете тестове, които симулират начина, по който потребителите взаимодействат с вашето приложение.
- Избягвайте тестване на подробности за внедряване: Не тествайте вътрешната работа на вашите компоненти. Вместо това, фокусирайте се върху наблюдаваното поведение.
- Пишете ясни и кратки тестове: Направете вашите тестове лесни за разбиране и поддържане.
- Използвайте смислени имена на тестове: Изберете имена на тестове, които точно описват тестваното поведение.
- Поддържайте тестовете изолирани: Избягвайте зависимостите между тестовете. Всеки тест трябва да е независим и самосъдържащ се.
- Тествайте граничните случаи: Не само тествайте щастливия път. Уверете се, че тествате и граничните случаи и условията за грешки.
- Пишете тестове, преди да кодирате: Помислете за използването на Test-Driven Development (TDD), за да пишете тестове, преди да напишете своя код.
- Следвайте шаблона "AAA": Arrange, Act, Assert (Подреждане, Действие, Твърдение). Този шаблон помага за структурирането на вашите тестове и ги прави по-четливи.
- Поддържайте вашите тестове бързи: Бавните тестове могат да обезкуражат разработчиците да ги изпълняват често. Оптимизирайте вашите тестове за скорост, като имитирате мрежови заявки и минимизирате количеството DOM манипулация.
- Използвайте описателни съобщения за грешки: Когато твърденията се провалят, съобщенията за грешки трябва да предоставят достатъчно информация, за да се идентифицира бързо причината за неуспеха.
Заключение
React Testing Library е мощен инструмент за писане на ефективни, поддържани и ориентирани към потребителя тестове за вашите React приложения. Следвайки принципите и техниките, описани в това ръководство, можете да изградите стабилни и надеждни приложения, които отговарят на нуждите на вашите потребители. Не забравяйте да се съсредоточите върху тестването от гледна точка на потребителя, избягвайте тестването на подробности за внедряване и пишете ясни и кратки тестове. Като прегърнете RTL и приемете най-добрите практики, можете значително да подобрите качеството и поддръжката на вашите React проекти, независимо от вашето местоположение или конкретните изисквания на вашата глобална аудитория.