Модульные тесты на JavaScript и Jest

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

Что такое Jest?

Jest — это среда тестирования JavaScript, разработанная, чтобы максимально упростить тестирование. Он предоставляет все необходимые инструменты для запуска тестов, создания утверждений, макетирования реализаций и многого другого в одном пакете.

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

Jest также поддерживает TypeScript через Babel.

Как установить Jest?

Установите jest пакет (и дополнительные типы) в новый или существующий файл проекта, package.json используя выбранный вами менеджер пакетов:

# For NPM users
npm install --save-dev jest @types/jest

# Yarn users
yarn add --dev jest @types/jest

Вот и все! Теперь мы готовы запускать тесты с помощью Jest.

Рекомендуется устанавливать Jest и любые другие инструменты тестирования в качестве зависимостей разработки. Это ускоряет установку в средах, где установлены только зависимости, необходимые для сборки и запуска проекта.

Как запускать тесты с помощью Jest?

Чтобы запустить тесты с помощью Jest, вызовите jest команду в корне папки проекта.

Мы обновим проект package.json тестовым скриптом, который вызывает jest для нас команду:

{
    // ... package.json contents
    "scripts": {
        // ... existing scripts
        "test": "jest"
    }
}

Теперь мы можем запустить только что созданный test скрипт:

# NPM users
npm run test

# Yarn users
yarn run test

Если все настроено правильно, Jest предоставит нам результаты всех тестов, которые он нашел и выполнил.

Jest завершает работу с кодом состояния 1, если тестовый пример не пройден. В этом случае ожидается появление npm ERR! ошибок в консоли.

Как создать тест с помощью Jest?

Чтобы создать тест для использования с Jest, мы создаем файл *.spec.js или *.test.js, который будет содержать наши тестовые примеры.

Jest настроен на поиск файлов .js, .jsx, .ts, и .tsx внутри папок, а также любых файлов с суффиксом __tests__ или .spec (включая файлы с именами test или spec).

Поскольку isInteger.js это имя тестируемого модуля, мы будем записывать наши тесты в файл, isInteger.spec.js созданный в той же папке, что и модуль:

// isInteger.spec.js
test("Sanity check", () => {
    expect(true).toBe(true);
});
Независимо от того, решите ли вы писать тесты в отдельной папке или рядом с модулями, не существует правильного или неправильного способа структурировать тесты внутри проекта. Jest достаточно гибок, чтобы работать с большинством архитектур проектов без настройки.

Описание теста — «Проверка работоспособности». Проверки работоспособности — это базовые тесты, гарантирующие, что система ведет себя рационально. Тест подтвердит, что мы ожидаем, что значение true будет true.

Запустите тест, и если он пройден, все настроено правильно.

Поздравляем! Мы только что написали наш первый тест!

Как написать тестовый пример в Jest?

Чтобы написать тестовый пример, мы сначала определяем результаты, которые мы должны проверить, чтобы убедиться, что система работает правильно.

Модуль isInteger.js — это функция, которая принимает один параметр и возвращает значение true, является ли параметр целочисленным значением или false нет. Мы можем создать два тестовых примера из этого определения:

  1. isInteger() проходит для целочисленного значения;
  2. isInteger() не работает для нецелого значения.

Чтобы создать тестовый пример в Jest, мы используем test() функцию . В качестве первых двух аргументов он принимает строку имени теста и функцию-обработчик.

Функцию test() также можно вызывать под псевдонимом - it(). Выбирайте тот или иной вариант в зависимости от читабельности или личных предпочтений.

Тесты основаны на утверждениях. Утверждения состоят из ожиданий и соответствий . Самое простое и распространенное утверждение предполагает, что проверяемое значение соответствует определенному значению.

Ожидание создается с помощью expect() функции . Он возвращает объект методов сопоставления, с помощью которого мы утверждаем что-то ожидаемое относительно проверяемого значения.

Метод matcher toBe() проверяет, соответствует ли ожидание заданному значению.

В наших тестах мы можем ожидать isInteger() целочисленное true значение 1 и false нецелое значение 1,23.

// isInteger.spec.js
const isInteger = require("./isInteger");

test("isInteger passes for integer value", () => {
    expect(isInteger(1)).toBe(true);
});

test("isInteger fails for non-integer value", () => {
    expect(isInteger(1.23)).toBe(false);
});

Запуск Jest теперь должен предоставить нам отчет о том, какие тесты пройдены, а какие не пройдены.

Как использовать фикстуры в Jest?

Чтобы использовать фикстуры в Jest, мы можем использовать test.each() функцию . Он выполняет тест для каждого прибора в массиве приборов.

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

В Jest фикстура может представлять собой одно значение или массив значений. Фикстура доступна в функции обработчика теста через параметры. Значение или значения фикстуры можно ввести в описание посредством форматирования printf.

// isInteger.spec.js
const isInteger = require("./isInteger");

const integerNumbers = [-1, 0, 1];

test.each(integerNumbers)(
    "isInteger passes for integer value %j",
    (fixture) => expect(isInteger(fixture)).toBe(true)
);

// ... or...
const integerNumbers = [
  [-1, true],
  [-0, true],
  [1, true]
];

test.each(integerNumbers)(
    "isInteger passes for integer value %j with result %j",
    (fixture, result) => expect(isInteger(fixture)).toBe(result)
);

Запуск Jest теперь должен предоставить нам отчет о том, какие тесты пройдены, а какие не пройдены, причем каждый тест соответствует фикстуре из нашего массива фикстур.

%j — это спецификатор форматирования printf , который печатает значение в формате JSON. Это хороший выбор для приборов, содержащих значения разных типов.

Как сгруппировать тестовые случаи в Jest в набор тестов?

Чтобы сгруппировать тестовые случаи в Jest в набор тестов, мы можем использовать describe() функцию. В качестве первых двух аргументов он принимает строку имени набора и функцию-обработчик.

Набор тестов — это набор тестовых примеров, сгруппированных вместе для целей выполнения.

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

// isInteger.spec.js
const isInteger = require("./isInteger");

describe("isInteger", () => {
    const integerNumbers = [-10, -1, 0, 1, 10];

    test.each(integerNumbers)(
        "passes for integer value %j",
        (fixture) => expect(isInteger(fixture)).toBe(true)
    );

    const floatNumbers = [-10.1, -1.1, 0.1, 1.1, 10.1];

    test.each(floatNumbers)(
        "fails for non-integer value %j",
        (fixture) => expect(isInteger(fixture)).toBe(false)
    );
});

Запуск Jest теперь должен предоставить нам отчет о том, какие тесты пройдены, а какие не пройдены, сгруппированные в описанные наборы тестов.

describe() также можно вкладывать друг в друга для создания более сложных иерархий тестов.

Как запускать Jest каждый раз, когда файлы меняются?

Чтобы запускать Jest каждый раз при изменении файлов, мы можем использовать флаги--watch и --watchAll.

Флаг --watch сообщит Jest следить за изменениями в файлах, отслеживаемых Git. Jest запустит только те тесты, на которые повлияли измененные файлы. Чтобы это работало, проект также должен быть репозиторием Git.

Флаг --watchAll сообщит Jest следить за изменениями во всех файлах. Всякий раз, когда файл изменяется, Jest запускает все тесты.

Оба режима --watch и --watchAll поддерживают дополнительную фильтрацию тестов во время их выполнения. Это позволяет запускать только тесты, соответствующие имени файла, или запускать только неудачные тесты.

# Runs tests on changed files only and re-runs for any new change
# Note: the project must also be a git repository
jest --watch

# Runs tests on all files and re-runs for any new change
jest --watchAll

Как получить отчет о покрытии тестами с помощью Jest?

Чтобы получить отчет о тестовом покрытии с помощью Jest, мы можем использовать --coverage флаг.

Покрытие тестами — это метрика тестирования программного обеспечения, которая описывает, сколько строк исходного кода (операторов) тестируемого модуля выполняется (покрывается) тестами. 100%-ное покрытие теста для модуля означает, что каждая строка кода в модуле была вызвана тестом.

Мы всегда должны стремиться к высокому покрытию тестами — в идеале 100 % — но также имейте в виду, что полное покрытие не означает, что мы протестировали все случаи, а только строки кода.

# Runs tests and prints a test coverage afterwards
jest --coverage
Мы можем комбинировать разные флаги, чтобы получить больше возможностей от Jest. Например, чтобы просмотреть все файлы и получить отчет о покрытии, мы можем запустить jest --watchAll --coverage.

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

Пример кода модульного теста Jest

Чтобы установить Jest:

# For NPM users
npm install --save-dev jest @types/jest

# Yarn users
yarn add --dev jest @types/jest

Устройство, которое будет тестироваться в isInteger.js:

// isInteger.js
module.exports = (value) => !isNaN(parseInt(value, 10));

Модульный тест в isInteger.spec.js:

// isInteger.spec.js
const isInteger = require("./isInteger");

describe("isInteger", () => {
    const integerNumbers = [-10, -1, 0, 1, 10];

    test.each(integerNumbers)(
        "passes for integer value %j",
        (fixture) => expect(isInteger(fixture)).toBe(true)
    );

    const floatNumbers = [-10.1, -1.1, 0.1, 1.1, 10.1];

    test.each(floatNumbers)(
        "fails for non-integer value %j",
        (fixture) => expect(isInteger(fixture)).toBe(false)
    );
});

Тестовый скрипт в package.json:

jest --watchAll --coverage