Как создать фреймворк модульного тестирования JavaScript с нуля

1656642261 kak sozdat frejmvork modulnogo testirovaniya javascript s nulya

Альсидес Кейрос

1*4DbVu2McfBneFwYxigrpnA
Вот как будет выглядеть результат нашего тестирования

Обещаю, это будет весело. =)

Вероятно, автоматизированные тесты являются частью вашей ежедневной рутины (если нет, перестаньте читать статью и начните сначала, обучаясь у самого отца TDD). Вы достаточно долго пользуетесь тестовыми фреймворками, такими как Node-tap (или Tape), Jasmine, Mocha или QUnit, просто признавая, что они делают некоторые магические вещи, и не задаете о них слишком много вопросов. Или если вы похожи на меня, возможно, вам всегда интересно, как все работает, включая, конечно, фреймворки тестирования.

Эта статья проведет вас через процесс создания фреймворка тестирования JavaScript с нуля, с достаточно приличным DSL и хорошо детализированным результатом. Это первая моя статья #LearnByDIY серия. Идея состоит в том, чтобы демистифицировать определенные виды программного обеспечения, к которому мы привыкли, путем создания их более простых версий.

Отказ от ответственности

Прежде чем начать, некоторые важные примечания:

  • Целью этой статьи является нет создать готовый к производству инструмент. Пожалуста, не используйте фреймворк, созданный для тестирования рабочего кода. Его цель сугубо просветительская. =)
  • Конечно, наш маленький фреймворк не будет полнофункциональным. Такие вещи, как асинхронные тесты, параллельное исполнение, более богатый набор соответствий, CLI (с такими параметрами, как --watch), подключаемые репортеры и DSL и т.д., не будут присутствовать в нашей окончательной версии. Однако я настоятельно рекомендую что вы продолжаете играть с этим проектом и, возможно, попробуете реализовать некоторые из этих отсутствующих частей. Возможно, вы сможете превратить его в серьезный проект с открытым кодом. Я хотел бы знать, что этот игрушечный проект стал «фактической» системой тестирования.

⚔️ Tyrion – крошечная платформа тестирования

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

1*cedi3xCR8cINoPAje7Nrjw
Тирион маленький, но храбрый.

Мы будем использовать Node.js в этом проекте с хорошими и старыми модулями CommonJS. Минимальная версия Node, которая вам понадобится – v8.6.0. Если у вас более старая версия, обновите ее.

О, я чуть не забыл… Я использую Yarn в этой статье для таких вещей, как yarn init, yarn link и так далее, но вы можете использовать «ванильный» NPM подобным образом (npm init, npm link…).

Создание структуры папок проекта

Сначала создадим такую ​​структуру папок:

tyrion/||______ proj/|      ||      |______ src/||______ playground/       |       |______ src/       |______ tests/

Иными словами»:

$ mkdir -p tyrion/proj/src tyrion/playground/src tyrion/playground/tests

Нам нужно две папки, каждая для отдельного проекта.

  • The proj папка будет содержать пакет фреймворка Tyrion.
  • The playground папка будет содержать одноразовый проект Node для игры с нашим фреймворком. Он будет служить лабораторией во время процесса разработки.

Инициализация проектов Node

Перейдите к playground папку и запустите yarn init -y. Эта команда создает базовый файл package.json. Откройте, извлеките "main": "index.js", и добавьте запись «сценарии», как в примере ниже:

{  "name": "playground",  "version": "1.0.0",  "scripts": {    "test": "node tests"  },  "license": "MIT"}

После создания этого файла давайте сделаем то же для другого проекта, самого пакета Tyrion. В proj папку, выполн yarn init. Вам будет предложено ввести определенную информацию, чтобы правильно создать файл package.json. Введите следующие значения (выделенные жирным шрифтом):

question name (proj): tyrion <enter>question version (1.0.0): <enter>question description: <enter>question entry point (index.js): src/index.js <enter>question repository url: <enter>question author: <enter>question license (MIT): <enter>question private: <enter>

Теперь нам нужно установить Tyrion в зависимости от разработки в нашем проекте игровой площадки. Если бы это был опубликованный пакет, нам нужно было бы просто установить его непосредственно, через npm i --dev или yarn add --dev. Поскольку Тирион только локально, это невозможно. К счастью, и Yarn, и NPM имеют функцию, помогающую разработчикам во время этой фазы «начального» пакета, позволяя нам имитировать связь между двумя пакетами (один как зависимость от другого).

Чтобы создать эту ссылку на зависимость, перейдите к proj папку и запустите:

$ yarn link

Тогда, в playground folderзапустить:

$ yarn link tyrion 

Это все. Теперь Tyrion зависим от проекта игровой площадки.

Создание некоторых модулей, которые будут нашими «подопытными кроликами»

В playground/src папку, давайте создадим два модуля для тестирования Tyrion:

Написание некоторых контрольных работ

Пора использовать наше воображение. Как выглядеть DSL от Tyrion? Вы болеете expect, assert , и так дальше? Давайте сделаем это по-другому, просто ради развлечения. я предлагаю guarantee как наша функция утверждения. Вам это нравится?

Давайте напишем несколько тестов, чтобы было понятнее. Конечно, ничего не получится, поскольку мы ничего не реализовали в нашем фреймворке.

И а tests/index.js файл, чтобы импортировать наши тесты только в одном месте.

Тирион позаимствует один из принципов Node-tap:

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

Это означает, что ему не может понадобиться специальный раннер, размещающий магические функции в глобальном пространстве. node test.js это вполне нормальный способ запустить тест, и он должен функционировать точно так же, как его запускает модный бегун с отчетами и т.д. Тесты JavaScript должны быть приложениями JavaScript; не англоязычные стихи со странной пунктуацией.

https://www.node-tap.org/#tutti-i-gusti-sono-gusti.

Как вы, возможно, помните, в файле package.json нашей игровой площадки у нас есть test сценарий, который просто запускается node tests. Итак, чтобы выполнить его, просто введите npm test и нажмите ввод. Да, сделайте это. Давайте посмотрим, как он выходит из строя:

1*9h8lW-Kon3LuqlqQUI-0hA

Это заблуждение понятно. В нашей структуре нет ничего. Ни один модуль вообще не экспортируется. Чтобы это исправить, в proj папку, создайте a src/index.js файл, экспортирующий пустой объект, как вы можете видеть ниже:

module.exports = {};

Сейчас мы побежим npm test снова:

1*ge5-uzTfUm-2bgrB8Gm3SQ

Узел жалуется, потому что наш guarantee функция не существует. Это также легко исправить:

const guarantee = () => {};
module.exports = { guarantee };

Запустите тестовый сценарий снова:

1*ZNsFSHaRf8JWCRake1fTNw

Вуаль! Ошибок нет, но ничего не происходит. =(

Гарантийная функция

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

Давайте реализуем:

И чтобы проверить, работает ли это, давайте добавим еще одно утверждение в конце нашего number-utils.test.js файл:

guarantee(123 === 321); // This should fail 

Теперь запустите его еще раз:

1*O8V_b4cKO7zxutYoLVuRdw

А-ха! Это работает! Это некрасиво, но это функционально.

Функция проверки

Нам нужен способ обратить утверждение в тестовые единицы. В общем, все фреймворки тестирования имеют эту функцию, например it функции в Жасмин или test функция в Node-tap.

В Tyrion будет вызвана наша тестовая функция check. Его подпись должна быть check(testDescription, callback). Мы также хотим, чтобы он давал нам более удобные результаты, описывая успешные и непроходимые тесты.

Вот как это будет выглядеть:

Теперь мы можем переписать наши тесты, чтобы использовать новое check функция:

И повторно запустите наш набор тестов:

1*CMuw0w6MzEfrKsyvR2QykQ

Круто. Но… как насчет некоторых цветов?? Разве не было бы гораздо проще различить тесты, которые сдали, а что не сдали?

Добавьте цветовой модуль как зависимость:

yarn add colors

Итак, импортируйте его в верхней части proj/src/index.js файл:

const colors = require('colors');

И давайте добавим несколько цветов к нашему результату:

const check = (title, cb) => {  try{    cb();    console.log(`${' OK '.bgGreen.black} ${title.green}`);  } catch(e) {    console.log(`${' FAIL '.bgRed.black} ${title.red}`);    console.log(e.stack.red);  }};
1*D_Qp69Sx7kC5AfX2N9nfzg

Так лучше. =)

Функция xcheck

Было бы хорошо иметь простой способ отключить определенный тест, например xit функции в Жасмин. Это можно легко реализовать, создав функцию no-op, которая просто выводит, что тест выключен (ну это не полностью no-op, но почти):

const xcheck = (title, cb) => {  console.log(`${' DISABLED '.bgWhite.black} ${title.gray}`);};
module.exports = { guarantee, check, xcheck };

Итак, импортируйте xcheck функция в number-utils.test.js файл и выключите один из наших тестов:

const { guarantee, check, xcheck } = require('tyrion');const numberUtils = require('../src/number-utils');
// method: isPrimexcheck('returns true for prime numbers', () => {  guarantee(numberUtils.isPrime(2));  guarantee(numberUtils.isPrime(3));  guarantee(numberUtils.isPrime(5));  guarantee(numberUtils.isPrime(7));  guarantee(numberUtils.isPrime(23));});

И вот как он ведет себя:

1*kSCPsNlejl9OdZC8VUruyw

Результат теста и код выхода

Если бы мы хотели использовать Tyrion на сервере CI, ему нужно было бы завершить свой процесс с разными кодами выхода для условий ошибки и успеха.

Еще одна желаемая функция – конспект теста. Было бы хорошо знать, сколько тестов пройдено, не пройдено или пропущено (отключенных). Для этого мы могли бы увеличить некоторые счетчики у обоих check и xcheck функции.

Мы создадим end функция, которая печатает результат теста и завершает соответствующим кодом выхода:

И не забудьте позвонить в playground/tests/index.js файл:

const { end } = require('tyrion');
require('./string-utils.test');require('./number-utils.test');
end();

Или возможно:

const tyrion = require('tyrion');
require('./string-utils.test');require('./number-utils.test');
tyrion.end();

А теперь запустим снова npm test:

1*MK7W8WxuDSD6grErVrSZKA

Прекрасно, работает.

Групповая функция

Многие тестовые фреймворки имеют определенный способ группировки связанных тестов. В Жасмине, например, есть describe функция. Мы будем реализовывать а group функция для этой цели:

И обновите наши тесты, чтобы использовать эту новую функцию:

Вот новый результат:

1*8y2171rDoHbOhGMIt3myGw

Благо, это работает. Плохая новость состоит в том, что это становится трудно понять. Нам нужен способ сделать отступление этого вывода, чтобы сделать его более читабельным:

Запустите снова:

1*X1QQ7GuihVzZkFS6yQ91Jg

Да лучше!

Итак, как это работает?

  • The repeat функция повторяет строку n раз.
  • The indent функция повторяет отступление (из четырех пробелов) n раз с помощью repeat функция.
  • The indentLines функция делает отступление в строке с несколькими строками путем добавления n отступы к началу каждой строчки. Мы используем его для отступления стеков ошибок.
  • The indentLevel переменная увеличивается в начале каждого выполнения группы и уменьшается в конце. Таким образом, вложенные группы могут иметь правильное отступление.

Больше ответчиков

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

Сначала создайте matchers папка:

$ mkdir proj/src/matchers

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

The same matcher использует оператор строгого равенства (===), чтобы проверить, есть ли два аргумента совершенно одинаковым объектом (для эталонных типов) или равными (для примитивных типов). Он ведет себя подобно toBe соответствие в Жасмин и t.equal в узле-кран.

Примечание: Node-tap также называется соответствующим t.sameно он работает иначе (он не будет проверять, абсолютно ли два объекта одинаковы, а будет проверять, оба ли они глубоко эквивалентны).

The identical matcher проверяет эквивалентность двух доводов. Он использует == оператор сравнения значений.

The deeplyIdentical matcher выполняет глубочайшее сравнение двух объектов. Такое сравнение может быть достаточно сложным или, по крайней мере, слишком сложным для целей настоящей статьи. Итак, давайте установим существующий модуль для обработки глубокого равенства и используем его в нашем матче:

$ yarn add deep-equal

Затем:

Вот как будет выглядеть ошибка:

1*P3SI0UMJlXOusnB0A9yrNw

The falsy совпадение не удастся выполнить, если приданное значение правдиво.

The truthy matcher работает подобно нашему guarantee функция. Он проходит, когда приданное значение истинно, и ломается, если оно ошибочно.

The throws matcher пройдет, если функция выдает ошибку. Можно указать требуемое сообщение об ошибке, но это не обязательно.

An index.js файл для повторного экспорта всех ответчиков:

И наконец, давайте склеим их вместе:

Вы можете использовать наши новые соответствия следующим образом:

const { guarantee, check } = require('tyrion');
check('playing with our new matchers', () => {  // The original guarantee function still works  guarantee(123 === 123);
  guarantee.truthy('abc');  guarantee.falsy(null);
  const a = { whatever: 777 };  const b = a;  guarantee.same(a, b);  guarantee.identical(undefined, null);
  const c = { whatever: { foo: { bar: 'baz' } } };  const d = Object.assign({}, c);  guarantee.deeplyIdentical(c, d);
  function boom() { throw new Error('Some error...'); }  guarantee.throws(boom);  guarantee.throws(boom, 'Some error...');});

Функция beforeEach

Для реализации a beforeEach нам нужно использовать стек, чтобы накопить все beforEach обратные вызовы. Это делается для каждого нового ограниченного уровня, созданного каждый раз, когда объявляется группа:

Как это работает?

  • Каждый раз, когда объявляется группа, мы присылаем новый массив к beforeEachStack переменная. Этот массив будет накапливать все beforeEach обратные вызовы, объявленные в данной области.
  • После завершения группового исполнения мы удаляем массив в верхней части стека обратных вызовов.
  • The beforeEach функция получает обратный вызов и добавляет его в массив вверху нашего стека обратных вызовов.
  • В начале каждого check функцию, мы вызываем каждую beforeEach обратный вызов на всех уровнях нашего стека.

Функция beforeAll

Нашим последним дополнением будет beforeAll функция. Для простотымы предполагаем, что вызовы к beforeAll функция всегда будет перед всеми группами и тестами (илив пределах группы, в ее самом верху).

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

Наша версия beforeAll просто получит обратный вызов и немедленно выполнит его.

const beforeAll = cb => cb();
module.exports = {   group, check, xcheck, guarantee, beforeAll, end };

Пример использования:

const { guarantee, check, group, beforeAll } = require('tyrion');
let a;beforeAll(() => {  a = { something: 'example' };});
group('playing with the beforeAll function', () => {  let b;  beforeAll(() => {    b = { something: 'example' };  });
  check('some test', () => {    guarantee.deeplyIdentical(a, b);  });
  check('another test', () => {    guarantee.identical(11, 11);  });});

Окончательная версия Тириона

Это было долгое путешествие, но Тирион наконец-то завершился. =)

Я добавил параметр SILENT, отключающий журнал. Он используется, чтобы облегчить тестирование Tyrion (да, тестовые фреймворки тоже нужно тестировать).

Полный проект доступен здесь.

Возможные улучшения

Тириона не хватает многих функций, таких как:

  • Поддержка асинхронных тестов
  • Параллельное исполнение тестов
  • afterEach и afterAll функции
  • А xgroup функция, отключающая всю группу
  • Функция, подобная Jasmine fit
  • Шпионы
  • Отделение DSL от логики отчетности.
  • Подключенные репортеры
  • Терминал CLI (с a --watch вариант)
  • Еще больше матчей
  • Более дружные стеки ошибок

Я призываю вас продолжать играть с этим проектом. Не стесняйтесь использовать и расширять его. Пожалуйста, сообщите мне свои мнения, предложения и эксперименты, оставив комментарий ниже. =)

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *