Эта статья не о том, как проводить модульное тестирование приложения React, а о том, что модульно тестировать в приложении React.
Я думаю, что одно из ключевых преимуществ модульного тестирования заключается в том, что оно заставляет разработчика писать чистый код с надлежащей абстракцией и разделением задач. Откровенно говоря, беспорядочный код не подлежит модульному тестированию.
Сохраняйте компоненты небольшими для правильного модульного тестирования
Когда я консультирую команду разработчиков, которая только начинает модульное тестирование в React, я бы посоветовал им написать приложение таким образом, чтобы оно состояло из небольших, индивидуально тестируемых частей. Сохраняйте компоненты небольшими:
- Разбивка крупного компонента на более мелкие компоненты
- Абстрагирование логики, зависящей от состояния, с помощью настраиваемых хуков
- Абстрагирование логики, НЕ зависящей от состояния, с помощью чистых функций
- Управляйте глобальным состоянием приложения с помощью Redux
Теперь давайте рассмотрим разные части, увеличивая уровень сложности теста.
Уровень 1 — Чистые функции
Чистые функции — это самые простые элементы для тестирования в приложении. Чистая функция — это функция, которая является детерминированной. При задании определенных входных данных чистая функция всегда будет каждый раз возвращать один и тот же результат без каких-либо побочных эффектов. Чистые функции можно протестировать без каких-либо специальных библиотек или инструментов.
Поскольку чистые функции очень легко тестировать, я часто рекомендую разработчикам писать как можно больше таких функций. В контексте приложения React разработчики могут писать чистые функции для обработки более сложных вычислений или бизнес-логики и импортировать такие функции в компоненты.
Примеры:
- Пользовательские функции, импортированные в компоненты или модули
- Редукторы Redux
// Testing pure function is dead simple
// Do as many of this as possible
describe('add', () => {
it('Should return 4 given 1 and 3', () => {
expect(add(1, 3)).toBe(4);
});
});
Уровень 2 — Функции с побочными эффектами
Неизбежно у нас будут функции, которые возвращают результаты, основанные не только на входных параметрах. В приложении React у нас будут функции, которые выполняют вызовы API. Такие функции можно тестировать только путем «моделирования» реальных вызовов API. Имейте в виду, что модульные тесты должны выполняться полностью в памяти, то есть они не должны зависеть от внешних систем или Интернета, если уж на то пошло.
Побочные эффекты также относятся к изменениям, которые функция может производить за пределами своих входных и выходных свойств. Например, функция может вызывать другую функцию для внесения изменений во внешнюю переменную. В таком случае мы можем использовать методы шпионажа — также доступные через фиктивные функции — чтобы подтвердить, что ожидаемые изменения сделаны.
Примеры:
- Пользовательские хуки React
В этом примере мы будем использовать react-testing-library для рендеринга хука и использовать функцию act (), чтобы убедиться, что изменение состояния завершено до утверждения.
import { renderHook, act } from '@testing-library/react-hooks'
import useCounter from './useCounter'
describe("(Hook) useCounter", () => {
it('should increment counter', () => {
const { result } = renderHook(() => useCounter());
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
});
- Функции, которые выполняют вызовы API
В этом примере мы имитируем почтовую функцию «axios», чтобы она возвращала токен.
import login from '../login'; import axios from 'axios';jest.mock("axios");describe('login', () => { it('should call POST and return token', () => { const token = { token: "xyz" }; axios.post.mockResolvedValueOnce(token); const result = await login(); expect(axios.post).toHaveBeenCalledWith(`${BASE_URL}/login`); expect(result).toEqual(token); }); });
Уровень 3 — Компоненты
При модульном тестировании компонентов React мы должны сосредоточиться только на ключевых элементах пользовательского интерфейса и взаимодействиях. Утверждать внешний вид стилей очень сложно, поэтому я бы вообще советовал сосредоточиться на функциональности.
Проверить первоначальный рендеринг
Набор модульных тестов для компонента должен включать тестовый пример, подтверждающий ключевые элементы, возвращенные начальным циклом рендеринга. Например, если у вас есть компонент формы входа, мы должны утверждать, что поле адреса электронной почты, поле пароля и кнопка отправки отображаются. Для простоты предположим, что этот компонент формы вызывает свойство onSubmit при нажатии кнопки «Войти» и сам по себе не вызывает API.
Мы будем использовать react-testing-library для примера:
import { screen } from "@testing-library/react";
// Mount the Login component and test if key elements are rendered
describe("(Component) Login", () => {
it("Should render all key form elements", () => {
render( <LogIn /> );
const emailInput = screen.getByTestId("email-input");
const passwordInput = screen.getByTestId("password-input");
const logInButton = screen.getByTestId("log-in-button");
expect(emailInput).toBeInstanceOf(HTMLInputElement);
expect(passwordInput).toBeInstanceOf(HTMLInputElement);
expect(logInButton).toBeInstanceOf(HTMLButtonElement);
});
});
Проверить состояния ключевых компонентов
У нас должен быть тестовый пример для каждого состояния компонента. Например, когда пользователь вводит неверный адрес электронной почты, наш тестовый пример должен утверждать, что компонент отображает правильное сообщение об ошибке.
import { fireEvent, screen } from "@testing-library/react";
// Test that an error message is returned when invalid email is
// entered
describe("(Component) Login", () => {
it("Should render error message if invalid email entered", () => {
render( <LogIn /> );
const emailInput = screen.getByTestId("email-input");
const passwordInput = screen.getByTestId("password-input");
const logInButton = screen.getByTestId("log-in-button");
fireEvent.change(emailInput, {
target: { value: "invalid.email" },
});
fireEvent.change(passwordInput, {
target: { value: "password" },
});
fireEvent.click(logInButton);
await screen.findByText("Invalid email address");
});
});
Протестируйте побочные эффекты и уточнения
У нас должен быть тестовый пример для каждого взаимодействия компонента с внешним миром. Мы делаем это, заглушая импорт или в качестве реквизита компонента со шпионскими функциями. Например, мы можем заменить импортированную функцию шпионской функцией, чтобы утверждать, что импортированная функция вызывается, когда компонент монтируется на экране. В качестве другого примера, мы можем передать компоненту шпионскую функцию в качестве реквизита «onSubmit», чтобы утверждать, что «onSubmit» вызывается при нажатии кнопки «Submit».
import { fireEvent, screen } from "@testing-library/react";
// Test that onSubmit is called when form submits
describe("(Component) Login", () => {
it("Should call onSubmit when Login button is pressed", () => {
const onSubmit = jest.fn();
render( <LogIn onSubmit={onSubmit} /> );
const emailInput = screen.getByTestId("email-input");
const passwordInput = screen.getByTestId("password-input");
const logInButton = screen.getByTestId("log-in-button");
fireEvent.change(emailInput, {
target: { value: "[email protected]" },
});
fireEvent.change(emailInput, {
target: { value: "password" },
});
fireEvent.click(logInButton);
expect(onSubmit).toHaveBeenCalledWith("[email protected]", "password");
});
});
Finnovate.io — технологическая компания, помогающая организациям создавать уникальные цифровые возможности для Интернета, мобильных устройств и блокчейна. Finnovate.io предлагает услуги по разработке, обучению, консультированию, а также платформу, которая быстро превращает бумажный контент в цифровой интерактивный опыт.