Найти в Дзене

Правила wb-rules и Jest: добавляем юнит-тесты

Когда нужно создавать тесты? Программист пишет код, представляя ситуации его использования. Все эти ситуации должны быть зафиксированы в тестах. Внутренняя структура функции не проверяется - мы оперируем только значениями на входе и выходе, по принципу «чёрного ящика». Достаточное количество тестов на все сомнительные ситуации в коде позволяет утверждать, что после очередных правок алгоритм будет работать чётко и без отклонений от замысла. В ином случае, система просто не позволит собрать и залить сломанный код на контроллер. В прошлой статье мы рассмотрели настройку статического анализатора ESLint, который применяется и далее Узнать о работе с правилами wb-rules на языке TypeScript можно из другой статьи А как подготовить контроллер к работе с VS Code, расскажет вводная статья серии Прежде достаточно было просто обозначить, что «где-то там» есть глобальные функции и конструкции «примерно вот такого формата». При внедрении в проект юнит-тестов требуется более точная типизация, поскольк
Оглавление

Когда нужно создавать тесты? Программист пишет код, представляя ситуации его использования. Все эти ситуации должны быть зафиксированы в тестах. Внутренняя структура функции не проверяется - мы оперируем только значениями на входе и выходе, по принципу «чёрного ящика».

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

В прошлой статье мы рассмотрели настройку статического анализатора ESLint, который применяется и далее

Узнать о работе с правилами wb-rules на языке TypeScript можно из другой статьи

А как подготовить контроллер к работе с VS Code, расскажет вводная статья серии

Подготавливаем проект

Переработанные файлы определений wb-rules

Прежде достаточно было просто обозначить, что «где-то там» есть глобальные функции и конструкции «примерно вот такого формата».

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

Скачать обновлённые файлы можно из любой ветки репозитория на GitHub - определения wb-rules одинаковы для всех инструментов разработки и обновляются синхронно, по мере развития проекта:

wb-rules-typescript/types at latest · wihome-dev/wb-rules-typescript

Установка Jest

Уже привычным делом, откроем в VS Code терминал и выполним в нём пару команд:

yarn add --dev jest ts-jest ts-node tsconfig-paths @types/jest
yarn add --dev eslint-plugin-jest
jest-mock-extended

Будут установлены пакеты, необходимые для функционирования Jest в варианте TypeScript.

tsconfig-paths понадобится для того, чтобы ts-node смог работать с принятыми нами сокращениями путей, используя определения из конфигурации TypeScript (по умолчанию ts-node не обрабатывает секцию paths).

Оптимизация настроек TypeScript

Зайдём в корневой файл конфигурации tsconfig.json и внесём следующие изменения в секцию compilerOptions:

  • выключим проверку типов в файлах *.d.ts - добавим
    "skipLibCheck": true
  • добавим опцию, разрешающую писать import вместо require. Формальность, но без неё Jest начинает ворчать при запуске тестов - добавим
    "esModuleInterop": true

Отключение проверки типов в файлах *.d.ts обусловлено тем, что после установки библиотеки типов @types/jest, компилятор TypeScript сильно придирается к файлам определений в процессе сборки кода для работы на контроллере.

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

➡️ Примечание: при подготовке данного материала были внесены изменения в первоначальную статью о работе с TypeScript

Так, если раньше мы с лёгкой руки использовали префикс @wb для импорта модулей в правила wb-rules, то теперь вместо него применяется @wbm. Например,

import { sum } from '@wbm/example-module'


Кроме того, из
tsconfig.json была убрана директива compilerOptions.rootDir - волшебным образом, всё работает и без неё.

Отсутствие необходимости указывать rootDir открывает перед нами возможность корректного наследования конфигураций TypeScript. Это пригодится чуть позже, когда доберёмся до типизации файлов с тестами.

Отредактированная конфигурация корневого tsconfig.json
Отредактированная конфигурация корневого tsconfig.json

Создание конфигурации Jest

В корень проекта добавим файл под названием jest.config.ts - здесь будут храниться установки инструмента тестирования.

Двумя строками импортируем нужные нам конструкции:

import type { Config } from 'jest'
import { createDefaultPreset } from 'ts-jest'

Первая типизирует объект конфигурации, после чего становится возможным подсветка содержащихся в нём свойств. Вторая помогает подключить к Jest препроцессор ts-jest, реализующий работу с файлами TypeScript.

Используя функцию createDefaultPreset, сформируем стандартную конфигурацию препроцессора. При необходимости, на вход функции может быть передан объект настроек препроцессора.

const tsJestTransformCfg = createDefaultPreset().transform

Далее создадим экземпляр конфигурации Jest и экспортируем его:

const config: Config = {
transform: {
...tsJestTransformCfg
},
moduleNameMapper: {
"@wb/(.*)": "<rootDir>/src/wb-rules/$1",
"@wbm/(.*)": "<rootDir>/src/wb-rules-modules/$1"
},
setupFiles: ['<rootDir>/tests/wb-engine-setup.ts']
}
export default config


В секции
moduleNameMapper прописаны собственные шаблоны путей к файлам с исходными кодами. Здесь для обращения к файлам модулей wb-rules применяется префикс @wbm, а для правил - @wb. Обратите внимание, что в отличие от модулей, файлы правил wb-rules разрешено импортировать только в тесты, но не в другие правила.

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

Созданная конфигурация Jest
Созданная конфигурация Jest

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

Настройка конфигурации ESLint

Откроем файл esling.config.mjs и добавим импорт плагина eslint-plugin-jest следующей строкой:

import pluginJest from 'eslint-plugin-jest'

Теперь подключим его, поместив после конфигурации Prettier:

{
files: ['tests/**/*.{js,ts}'],
plugins: { jest: pluginJest },
languageOptions: {
globals: pluginJest.environments.globals.globals
},
rules: {
'jest/no-disabled-tests': 'warn',
'jest/no-focused-tests': 'error',
'jest/no-identical-title': 'error',
'jest/prefer-to-have-length': 'warn',
'jest/valid-expect': 'error'
}
}

Кроме того, добавим файл jest.config.ts в исключения globalIgnores.

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

Отредактированный файл настроек ESLint
Отредактированный файл настроек ESLint

Для соответствия обновлённым определениям типов wb-rules, потребуется выключить пару правил:

'@typescript-eslint/dot-notation': 'off',
'@typescript-eslint/no-unsafe-member-access': 'off'

Это позволит корректно использовать конструкции вида dev['deviceId']['control'], не сталкиваясь с ворчанием ESLint (между прочим, вполне обоснованным). Отключение правил рассмотрено в статье о работе с ESLint:

Создание каталога с тестами

Если настроить тесты в главном файле tsconfig.json, то компилятор TypeScript при сборке добавит их в папку build, а Babel задорно подхватит эстафету и, выполнив преобразование в формат ES5, отправит результат в папку dist.

Нас такое поведение абсолютно не устраивает, тесты не должны попадать на контроллер. Поэтому сначала создадим в корне проекта папку tests, а затем поместим туда собственный файл tsconfig.json следующего содержания, наследующий главную конфигурацию через свойство extends:

{
"extends": "../tsconfig.json",
"compilerOptions": {
"paths": {
"@wb*": ["../src/wb-rules*"],
"@wbm*": ["../src/wb-rules-modules*"]
},
"module": "NodeNext",
"skipLibCheck": false
},
"include": [
"./wb-engine
-setup.ts",
"**/*.test.ts",
"../types/*"
],
"ts-node": {
"require": ["tsconfig-paths/register"],
"files": true
}
}

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

Изначально ts-node, применяющийся для запуска тестов на языке TypeScript, не смотрит ни на paths, ни на files, include или exclude. Отдельная секция по названию инструмента открывает ему «глаза» на происходящее, добавляет поддержку псевдонимов путей и активирует обнаружение файлов с определениями wb-rules.

Создадим в каталоге tests файл wb-engine-setup.ts для настройки тестового окружения и две папки - wb-rules и wb-rules-modules, куда будем складывать тесты для правил и модулей правил соответственно.

Готовая конфигурация tsconfig.json для папки с тестами
Готовая конфигурация tsconfig.json для папки с тестами

Имитация API движка правил wb-rules

Тестовая среда выполнения правил отличается от реальной и в ней, конечно же, отсутствует реализация таких функций, как log, defineRule, defineVirtualDevice и т.п. - попытка обращения к ним в процессе прохождения теста приведёт к ошибке.

Чтобы устранить этот недостаток, добавим в файл tests/wb-engine-setup.ts специальные конструкции, которые выдают себя за настоящие функции, но на самом деле ничего не делают. Код выполняется перед запуском любого файла с расширением *.test.ts, тем самым сбрасывая состояние.

Скачать можно из ветки репозитория jest на GitHub:

wb-rules-typescript/tests/wb-engine-setup.ts at intro/jest · wihome-dev/wb-rules-typescript

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

Добавление команды для запуска тестов

В секцию scripts файла package.json добавим короткую строчку

"test": "ts-node -P tests/tsconfig.json node_modules/jest/bin/jest.js"

Это позволит выполнять тесты без запуска процесса сборки - иногда бывает полезно. Также на команду с названием test смотрит расширение Jest для VS Code.

Конечно, можно было бы оставить словечко jest, как в оригинальном руководстве. Но в таком случае мы не задействуем конфигурацию TypeScript, лежащую в папке с тестами.

ℹ️ Примечание: проверить, какую конфигурацию видит ts-node, можно путём запуска в терминале VS Code команды

yarn ts-node -P tests/tsconfig.json --showConfig


Чтобы препятствовать сборке при выявлении нарушений в тестах, встроим вызов созданной нами команды
test ещё и в команду-конвейер:

"build": "eslint && yarn test && tsc && tsc-alias && babel build -d dist"

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

Пишем простые тесты

Откроем файл модуля src/wb-rules-modules/example-module.ts и посмотрим, что там есть. Строковую константу в примере тестировать не интересно, поэтому добавим экспорт стрелочной функции сложения двух чисел:

export const sum = (a: number, b: number) => a + b

ℹ️ На заметку: стрелочные функции также называют лямбда-выражениями.

Перейдём в каталог tests/wb-rules-modules и создадим в нём файл example-module.test.ts - здесь будут содержаться тесты для модуля правил с одноимённым названием.

ℹ️ Примечание: чтобы не запутаться, тесты каждого модуля располагаются в отдельном файле.

Первой строкой импортируем функцию sum:

import { sum } from '@wbm/example-module'

Далее напишем свой первый тест:

test('sum 1 + 2 to be 3', () => {
expect(sum(1, 2)).toBe(3)
})

Сохраним файл и перейдём к настройке визуализации тестов.

Файл модуля и файл с его тестами
Файл модуля и файл с его тестами

Визуализация тестов

Оставим консольные запуски для процесса непрерывной интеграции (Continuous Integration или CI), с которым познакомимся в следующих статьях - вы ведь уже подписались на канал, чтобы не пропустить интересное?

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

Установка расширения для VS Code

Чтобы осуществить задумку, понадобится установить специальное расширение с одноимённым названием Jest, его исходный код доступен на GitHub:

GitHub - jest-community/vscode-jest: The optimal flow for Jest based testing in VS Code

На панели инструментов VS Code перейдём на вкладку расширений и в строку поиска введём orta.vscode-jest

Установка расширения Jest в Visul Studio Code
Установка расширения Jest в Visul Studio Code

ℹ️ Примечание: Facebook* запрещена в РФ как экстремистская.

Реакция Jest на изменения в файлах

Сразу после установки расширения, на панели инструментов появится дополнительная вкладка с изображением конической колбы. Но перед тем, как её открыть, вернёмся в обозреватель проекта и, для наглядности, откроем два файла: example-module.ts и example-module.test.ts - так, чтобы один располагался над другим.

Перейдя на вкладку тестов, увидим ситуацию как на рисунке ниже. Расширение уже изучило структуру проекта, нашло и выполнило все тесты, а отчёт собрало в аккуратный список.

Вкладка со списком тестов
Вкладка со списком тестов

Чтобы оценить мощь инструмента, в модуле правил example-module.ts поменяем операцию внутри функции sum со сложения на умножение и сохраним файл. Процесс тестирования автоматически перезапустится.

Ошибка прохождения теста
Ошибка прохождения теста

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

Тестируем приватные функции модулей

Задача со звёздочкой - тестирование внутренних функций, к которым в обычной ситуации нельзя обратиться снаружи модуля. Однако, контроль за стабильностью алгоритмов обязан вторгаться в каждый «тёмный уголок» исходного кода и на него не должны распространяться типовые ограничения.

Преодолеть «барьер» можно по-разному, сегодня мы рассмотрим один из самых популярных способов - при помощи инструмента rewire.

Установка rewire

Откроем терминал VS Code и выполним команду, которая установит пакеты, необходимые для работы rewire

yarn add --dev rewire @types/rewire

Первый пакет - это сам инструмент, а второй - определения типов для TypeScript, которые при добавлении пакета в проект подхватываются автоматически.

Доработка примера

Откроем файл модуля src/wb-rules-modules/example-module.ts и добавим ещё одну функцию, на этот раз - умножения двух чисел:

// Приватная стрелочная функция.
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const mult = (a: number, b: number) => a * b

К mult нельзя обратиться явно, просто импортировав модуль в правило или тест - функция внутренняя. Специальный комментарий является указанием ESLint, что мы в курсе про незадействованную константу и на неё можно не ворчать.

ℹ️ Примечание: локальное выключение правил ESLint лучше не использовать, т.к. в зависимости от серьёзности правила, это способно привести к трудно диагностируемым ошибкам. Единственное корректное место для управления поведением анализатора - его файл конфигурации esling.config.mjs.

Чтобы не смешивать два разных подхода к работе с тестами, создадим в папке tests/wb-rules-modules файл example-module.rewired.test.ts - в нём расположим более сложные конструкции.

Первым делом, импортируем сам rewire:

import rewire from 'rewire'

Затем обернём в него вызов нашего модуля правил wb-rules так, словно это обычный вызов require:

const rewired = rewire('@wbm/example-module')

Поскольку мы работаем с TypeScript, а rewire не предоставляет никакой информации об импортируемых из модулей типах, потребуется вручную создать определение для функции математических операций над двумя числами:

declare type MathFunc = (a: number, b: number) => number

Извлечение приватной функции из модуля осуществляется через особый метод __get__:

const mult = rewired.__get__<MathFunc>('mult')

Дальше с константой mult можно работать как обычно (вы ведь часто обращаетесь к константам так, словно это функции, правда?):

test('mult 2 * 2 to be 4', () => {
expect(mult(2, 2)).toBe(4)
})

ℹ️ Примечание: в данном случае использование const означает, что функция не может быть заменена другим определением, случайно или намеренно. В JavaScript потом всё равно прилетит var, но в исходном коде защитит от ошибок.

Проверка выполнения тестов с применением rewire
Проверка выполнения тестов с применением rewire

Существующие ограничения

В настоящее время, rewire не получится использовать совместно с имитаторами API - по какой-то причине, globals не попадают внутрь модулей, обёрнутых утилитой.

Код инструментов лежит в папке node_modules и полностью доступен для изучения. На днях углубился в исходные коды rewire, но там всё в порядке - причина лежит где-то ещё глубже, в библиотеке module от NodeJs.

Вполне вероятно, что пропущена какая-то хитрая и неочевидная опция конфигурации, которая сразу всё нормализует.

Пока решение не найдено, можно тестировать следующим образом:

  • только открытые части модулей - без rewire и с имитаторами API (в коде тестируемого модуля допустимо использование конструкции log, defineRule, defineAlias и пр.);
  • открытые и приватные части модулей - через rewire, но без имитаторов API.

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

Шаблон проекта на GitHub

Настроенный проект для разработки правил wb-rules доступен на GitHub, для соответствия материалам статьи выделена ветка jest. В ней код меняется только при значительных изменениях в подходах к работе с инструментом.

GitHub - wihome-dev/wb-rules-typescript at intro/jest

Послесловие

Рассмотрев процесс тестирования модулей, мы намеренно обошли стороной тестирование файлов с правилами wb-rules - по большей части, там используются внутренние конструкции совместно с вызовами API, достучаться до которых можно только при помощи rewire.

В следующем выпуске: автоматическое удаление лишнего кода