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