Укрепите свою кодовую базу с помощью надежного набора модульных тестов

Модульное тестирование имеет решающее значение для разработки, независимо от размера приложения. В этой статье блога я расскажу, как я писал модульные тесты для этого примера приложения:



Страница Github: https://xiongemi.github.io/white-label-airline

Он объяснит некоторые из моих стратегий модульного тестирования для различных частей приложения React:

  • Услуги
  • Презентационные компоненты
  • Смарт / контейнер / подключенные компоненты
  • Файлы Redux
  • Крючки

Стек технологий

Вот список библиотек, которые я использовал:

Плагины ESLint

Добавление линтинга поможет разработчикам избежать ошибок при написании модульных тестов. Чтобы установить плагины ESLint lint-plugin-testing-library и eslint-plugin-jest-dom, введите следующие команды:

# npm
npm install --save-dev lint-plugin-testing-library eslint-plugin-jest-dom
# yarn
yarn add lint-plugin-testing-library --dev
yarn add eslint-plugin-jest-dom --dev

В файле .eslintrc добавьте нужные вам правила. Например, добавьте рекомендуемые правила в extends:

"extends": [
  "plugin:jest-dom/recommended",
  "plugin:testing-library/react",
],

Модульные тесты на доступность

Модульные тесты также могут проверять доступность, мы могли бы использовать библиотеку jest-ax:

# npm
npm install --save-dev jest-axe
# yarn
yarn add jest-axe --dev

Чтобы настроить его, создайте в корне файл с именем jest.setup.js со следующим содержимым:

import { toHaveNoViolations } from 'jest-axe';
expect.extend(toHaveNoViolations);

Укажите этот файл в качестве файла установки в файле конфигурации Jest (jest.config.js или jest.config.ts):

setupFilesAfterEnv: ['./jest.setup.js']

Ниже приведен модульный тест для проверки нарушений доступности для компонента React:

Мок-импорт

Иногда мы не можем настроить библиотеку или сервис для тестирования, но мы всегда можем имитировать это. Он будет иметь детальный контроль над тем, что было возвращено из этой библиотеки или службы.

Библиотека макетов

Пример: react-i18next

В качестве примера я использовал библиотеку react-i18next, и у меня есть этот импорт в моем компоненте: import { useTranslation } from ‘react-i18next’;. Тем не менее, мне не нужно настраивать его для тестирования - я могу просто поиздеваться над ним:

В моих модульных тестах мне просто нужно импортировать этот фиктивный файл в файл jest.setup.js.

Пример: react-router-dom

Я использовал библиотеку react-router-dom, а компонент <Link /> из этой библиотеки: import { Link as RouterLink, useLocation } from ‘react-router-dom’. Я получил эту ошибку при попытке запустить модульный тест: [Error: Invariant failed: You should not use <Link> outside a <Router>].

Чтобы издеваться над этим:

jest.mock('react-router-dom', () => ({
  ...jest.requireActual('react-router-dom'),
  Link: ({ children }) => <div>{children}</div>,
  useLocation: jest.fn().mockReturnValue({ pathanme: '/outbound' }),
}));

Мок сервис

В качестве примера у меня есть функция-служба с именем countriesService. Мне нужно издеваться над этой службой в моем модульном тесте. Вот как выглядит этот служебный файл:

Вот как я издеваюсь над этим в своем файле модульного теста:

countriesService.getCountries = jest
        .fn()
        .mockImplementation(() => Promise.resolve(mockCountriesResponse));

Услуги

Например, для countriesService я использую fetch для выполнения сетевых запросов:

Имитация fetch

Чтобы протестировать этот файл, мне нужно создать модель функции fetch. Для этого устанавливаю библиотеку jest-fetch-mock:

# npm
npm install jest-fetch-mock --save-dev
# yarn
yarn add jest-fetch-mock --dev

Чтобы настроить его, добавьте эти строки в jest.setup.js:

require('jest-fetch-mock').enableMocks()

Модульный тест

В файле модульного теста, чтобы имитировать успешный вызов fetch, введите:

fetchMock.mockResponseOnce(JSON.stringify(<mock response>));

Чтобы имитировать неудачный вызов fetch, введите:

const response = new Response(null, {
  status: 401,
});
fetchMock.mockReturnValueOnce(Promise.resolve(response));

Вот фактический файл модульного теста для вышеуказанной службы:

Презентационные компоненты

Компоненты презентации отображают данные только в зависимости от свойств, переданных от их родителей - они не связаны ни с каким состоянием.

На мой взгляд, для презентационных компонентов мне не нужно проверять, отображаются ли переданные реквизиты соответствующим образом. В большинстве случаев у меня просто юнит-тест для проверки доступности.

Смарт / Контейнер / Подключенные компоненты

Смарт / контейнер / подключенные компоненты подключены к состоянию. В моем случае я использую Redux для управления состоянием. Вместо того, чтобы создавать настоящий магазин Redux, я также имитирую его с помощью библиотеки redux-mock-store:

# npm
npm install redux-mock-store @types/redux-mock-store --save-dev
# yarn
yarn add redux-mock-store --dev
yarn add @types/redux-mock-store --dev

Чтобы создать макет магазина:

Вот тесты, которые я обычно пишу:

  • Перейдите в различные типы состояний хранилища и посмотрите, правильно ли отображаются данные:
  • Проверить, запускается ли отправка правильным действием при наступлении определенного события
expect(store.dispatch).toBeCalledWith(<expected action>);

Вот пример реального файла модульного теста, который я написал для интеллектуального компонента:

Файлы Redux

Примечание. В следующих примерах для тестирования файлов Redux я использую Redux Toolkit и redux-observable.

Редукторы

Редукторы - это чистые функции, которые легко протестировать, обычно следуя этому шаблону:

const action = <any action>;
const state = reducer(<initial state value>, action);
expect(state).toEqual(<expected state value>);

Вот пример модульного теста для редуктора:

Селекторы

Подобно редукторам, селекторы - это чистые функции, обычно следуют этому шаблону:

const actual = selectorFn(<mock state value>);
const expected = <expected selector return value>;
expect(actual).toEqual(expected);

Вот пример файла модульного теста для селекторов:

Эпос

Я использую redux-observable как промежуточное ПО для обработки асинхронных действий. Конечно, есть и другие способы, например, Thunk или redux-saga.

В этом примере модульный тест следует этой схеме:

describe('<epic name>', () => {
  let action$: ActionsObservable<Action>;
  beforeEach(() => {
    action$ = new ActionsObservable(
     of(<triggered action>)
    );
  });
  it('should map to certain action', (done) => {
    epicFn(action$).subscribe({
      next: (action) => {
        expect(action).toEqual(<expected action>);
        done();
      },
    });
  });
});

Вот пример файла модульного теста для эпоса:

Крючки

Для тестирования кастомных хуков React нам понадобится библиотека @testing-library/react-hooks:

# npm
npm install --save-dev @testing-library/react-hooks react-test-renderer
# yarn
yarn add @testing-library/react-hooks --dev
yarn add react-test-renderer --dev

Я использовал хуки из другой библиотеки внутри своего кастомного хука:

import { useFormikContext, getIn } from 'formik';
const { touched, errors, isSubmitting } = useFormikContext();

Вот как я издеваюсь над своим модульным тестом:

jest.mock('formik', () => ({
  useFormikContext: () => {
    return {
      touched: { fieldName: true },
      errors: { fieldName: 'random error' },
      isSubmitting: true,
    };
  },
  getIn: (context: Record<string, unknown>, fieldName: string) => {
    return context[fieldName];
  },
}));

Вот пример моего пользовательского хука и его модульного теста:

Порог покрытия

Теперь, когда я написал несколько модульных тестов, мне нужно установить порог покрытия. В моем jest.config файле я добавил следующие строки:

  collectCoverage: true,
  coverageReporters: ['text', 'html'],
  coverageThreshold: {
    global: {
      branches: 50,
      functions: 60,
      lines: 60,
      statements: 60,
    },
  },

В терминале он должен вывести отчет о покрытии:

На мой взгляд, для приложения нет необходимости пытаться достичь сверхвысокого покрытия. Чем выше число, тем лучше код может не быть. Я просто добавил сюда несколько цифр в качестве напоминания о необходимости писать содержательные модульные тесты.

Хуки перед фиксацией

Тем не менее, мне также нужно настроить ловушку перед фиксацией, чтобы гарантировать, что модульные тесты запускаются и проходят перед каждой фиксацией.

  • Установить husky
  • В вашем package.json добавьте следующие строки:
  "husky": {
    "hooks": {
      "pre-push": "npm run lint && npm run test"
    }
  }

Заключение

Спасибо, что дочитали до конца. Вот стратегии тестирования, которые я использовал для одного из своих приложений React. Это может не соответствовать потребностям каждого проекта.

Иногда большее количество модульных тестов не означает лучший код. Я видел несколько нестабильных юнит-тестов, которые постоянно ломались. Модульный тест должен быть достаточно маленьким, чтобы тестировать только один файл, и не должен превращаться в интеграционный тест. Хорошие модульные тесты легко поддерживать и читать, а также служат хорошей документацией по коду.

Удачного тестирования.