Эта статья не о том, как проводить модульное тестирование приложения 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 предлагает услуги по разработке, обучению, консультированию, а также платформу, которая быстро превращает бумажный контент в цифровой интерактивный опыт.