Создание тестового фреймворка JavaScript


Фреймворк модульного тестирования

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

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

Например, чтобы протестировать следующую функцию:

function helloWorld() { return 'Hello world!'; }

Нужно написать тест jasmine spec:

describe('Hello world', () => { ① it('says hello', () => { ② expect(helloWorld())③.toEqual('Hello world!'); ④ }); });

① Функция describe(string, function) определяет тестовый набор — набор индивидуальных спецификаций теста.

② Функция it(string, function) определяет отдельную спецификацию теста, которая содержит одно или несколько ожиданий теста.

③ Выражение expect(actual) — это фактическое значение в тесте. В сочетании с Matcher он описывает ожидаемый фрагмент поведения в приложении.

④ Выражение matcher(expected) — это Matcher. Он выполняет логическое сравнение ожидаемого значения с фактическим, переданным функции expect. Если они имеют ложное значение, спецификация не выполняется.

Setup и teardown

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

Эти действия называются setup и teardown (для очистки). В Jasmine есть несколько функций для упрощения этого процесса:

beforeAll вызывается один раз перед запуском всех спецификаций в тестовом наборе.

afterAll вызывается один раз после завершения всех спецификаций в тестовом наборе.

beforeEach вызывается перед каждой спецификацией теста, если функция запущена.

afterEach вызывается после выполнения каждой спецификации теста.

Использование в Node

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

node_prj src/ one.js two.js test/ one.spec.js two.spec.js package.json

Тест содержит файлы спецификации, которые являются модульными тестами для файлов в папке src. В package.json test находится в разделе script.

{ ..., "script": { "test": "jest" // or "jasmine" } }

Если в командной строке запущен npm run test, тестовый фреймворк jest запустит все файлы спецификации в папке test и отобразит результат в командной строке.

Переходим к созданию собственного тестового фреймворка, который будет работать на Node.

В нашем тестовом фреймворке будет CLI-часть, с помощью которой он будет запускаться из командной строки. Вторая часть — исходный код тестового фреймворка, который будет находиться в папке lib.

Начнем с создания проекта Node:

mkdir kwuo cd kwuo npm init -y

Устанавливаем зависимость chalk, с помощью которой мы будем раскрашивать результаты тестов: npm i chalk.

Создаем папку lib, в которой будут находиться файлы:

mkdir lib

Создаем папку bin, поскольку фреймворк будет использоваться в качестве инструмента CLI Node:

mkdir bin

Начнем с создания файла CLI.

Создаем файл kwuo в папке bin и добавляем следующее:

#!/usr/bin/env nodeprocess.title = 'kwuo'require('../lib/cli/cli')

Устанавливаем шебанг с указанием на /usr/bin/env, чтобы запускать этот файл без команды node. Заголовок процесса устанавливаем на «kwuo» и запрашиваем файл «lib/cli/cli». Эти действия вызывают файл cli.js, который запускает процесс тестирования.

Переходим к установке и заполнению lib/cli/cli.js.

mkdir lib/cli touch lib/cli/cli.js

Этот файл находит папку test, получает все файлы из нее и запускает их.

Прежде чем реализовывать «lib/cli/cli.js», нужно установить глобальные переменные. Функции describe, it, expect, afterEach, beforeEach, afterAll и beforeAll используются в тестовых файлах:

describe('Hello world', () => { it('says hello', () => { expect(helloWorld()).toEqual('Hello world!'); }); });

Однако ни одна из них не определена в этих файлах. Каким образом файлы и функции работают без ReferenceError? Причина в том, что тестовый фреймворк реализовывает функции и устанавливает их как global перед запуском тестовых файлов.

Создаем файл index.js в папке lib:

touch lib/index.js

Здесь мы устанавливаем глобальные переменные и реализуем функции describeitexpectafterEachbeforeEachafterAll и beforeAll:

// lib/index.js const chalk = require('chalk') const log = console.log var beforeEachs = [] var afterEachs = [] var afterAlls = [] var beforeAlls = [] var Totaltests = 0 var passedTests = 0 var failedTests = 0 var stats = [] var currDesc = { it: [] } var currIt = {} function beforeEach(fn) { beforeEachs.push(fn) } function afterEach(fn) { afterEachs.push(fn) } function beforeAll(fn) { beforeAlls.push(fn) } function afterAll(fn) { afterAlls.push(fn) } function expect(value) { return { // Сопоставляем и подтверждаем, что ожидаемые и фактические объекты одинаковы. toBe: function(expected) { if (value === expected) { currIt.expects.push({ name: `expect ${value} toBe ${expected}`, status: true }) passedTests++ } else { currIt.expects.push({ name: `expect ${value} toBe ${expected}`, status: false }) failedTests++ } }, // Сопоставляем ожидаемый и фактический результат теста. toEqual: function(expected) { if (value == expected) { currIt.expects.push({ name: `expect ${value} toEqual ${expected}`, status: true }) passedTests++ } else { currIt.expects.push({ name: `expect ${value} toEqual ${expected}`, status: false }) failedTests++ } } } } function it(desc, fn) { Totaltests++ if (beforeEachs) { for (var index = 0; index < beforeEachs.length; index++) { beforeEachs[index].apply(this) } } //var f = stats[stats.length - 1] currIt = { name: desc, expects: [] } //f.push(desc) fn.apply(this) for (var index = 0; index < afterEachs.length; index++) { afterEachs[index].apply(this) } currDesc.it.push(currIt) } function describe(desc, fn) { currDesc = { it: [] } for (var index = 0; index < beforeAlls.length; index++) { beforeAlls[index].apply(this) } currDesc.name = desc fn.apply(this) for (var index = 0; index < afterAlls.length; index++) { afterAlls[index].apply(this) } stats.push(currDesc) } exports.showTestsResults = function showTestsResults() { console.log(`Total Test: ${Totaltests} Test Suites: passed, total Tests: ${passedTests} passed, ${Totaltests} total `) const logTitle = failedTests > 0 ? chalk.bgRed : chalk.bgGreen log(logTitle('Test Suites')) for (var index = 0; index < stats.length; index++) { var e = stats[index]; const descName = e.name const its = e.it log(descName) for (var i = 0; i < its.length; i++) { var _e = its[i]; log(` ${_e.name}`) for (var ii = 0; ii < _e.expects.length; ii++) { const expect = _e.expects[ii] log(` ${expect.status === true ? chalk.green('√') : chalk.red('X') } ${expect.name}`) } } log() } } global.describe = describe global.it = it global.expect = expect global.afterEach = afterEach global.beforeEach = beforeEach global.beforeAll = beforeAll global.afterAll = afterAll

В начале работы мы добавили библиотеку chalk, чтобы обозначить неудачные тесты красным цветом, а пройденные — зеленым. Сокращаем console.log до log.

Затем устанавливаем массивы beforeEachs, afterEachs, afterAlls и beforeAlls. beforeEachs содержит функции, которые вызываются в начале функции it, к которой он прикреплен. afterEachs вызывается в конце it. beforeEachs и afterEachs вызываются в начале и конце describe.

Устанавливаем Totaltests, которая будет содержать количество запущенных тестов. passTests содержит количество пройденных тестов, а failTests — количество неудачных тестов.

stats собирает статистику каждой функции describe, а curDesc обозначает текущую функцию describe, запущенную для сбора данных тестирования. currIt содержит запущенную в данный момент функцию it, выполняющую сбор тестовых данных.

Устанавливаем функции beforeEach, afterEach, beforeAll и afterAll, которые передают аргумент функции в соответствующие массивы: afterAll — в массив afterAlls, beforeEach — в beforeEachs и т. д.

Также у нас есть функция expect. Она выполняет тестирование:

expect(56).toBe(56) // Ожидается, что 56 будет равно 56. expect(func()).toEqual("nnamdi") // Ожидается, что func возвратит строку, которая будет равна "nnamdi".

Функция expect принимает аргумент для тестирования и возвращает объект, который содержит функции matcher. В данном случае она возвращает объект с функциями toBe и toEqual с аргументом, который они используют для сопоставления с аргументом значения, предоставляемым функцией expect. toBe использует === для сопоставления аргумента значения с ожидаемым аргументом. toEqual использует == для проверки фактического значения с ожидаемым. Функции увеличивают переменные passedTests и failedTests в зависимости от результатов теста, а также записывают статистику в переменную currIt. Мы используем только две функции matcher, однако их гораздо больше:

  • toThrow
  • toBeNull
  • toBeFalsy
  • и т. д.

Вы также можете реализовать их.

Переходим к функции it. Аргумент desc содержит имя описания теста, а fn — функцию. Сначала он обрабатывает beforeEachs, устанавливает статистику и вызывает функцию fn и afterEachs.

Функция describe выполняет те же действия, что и it, но вызывает beforeAlls и afterAlls в начале и в конце.

Функция showTestsResults анализирует массив stats и печатает пройденные и неудачные тесты на терминале.

Таким образом, мы реализовали и установили все функции в объект global, чтобы тестовые файлы могли вызывать их без ошибок.

Вернемся к «lib/cli/cli.js»:

// lib/cli/cli.js const path = require('path') const fs = require('fs') const { showTestsResults } = require('./../')

Сначала он импортирует функцию showTestsResult из «lib/index.js», которая отобразит результат запуска тестовых файлов в терминале. Кроме того, импорт этого файла установит глобальные переменные.

Продолжим:

// lib/cli/cli.js const path = require('path') const fs = require('fs') const { showTestsResults } = require('./../') /** * search for test folder */ function searchTestFolder() { if (!fs.existsSync('test/')) { return false } return true } /** * get all test files in the test/ folder */ function getTestFiles() { let f = null if (f = fs.readdirSync('test/')) { return f.length == 0 ? null : f } } /** * run the test files * @param {*} f */ function runTestFiles(f = []) { f.forEach((g) => { require(fs.realpathSync(`test/${g}`)) }) showTestsResults() } function run() { if (searchTestFolder()) { let files; if (files = getTestFiles()) { runTestFiles(files) } else { console.error('No test files found.') } } else { console.error(`'test/' folder doesn't exist`) } } run()

Функция run является главной и запускает весь процесс. Она выполняет поиск папки test с помощью searchTestFolder, получает тестовые файлы в массиве — getTestFiles, а затем просматривает массив тестовых файлов и запускает их с помощью runTestFiles.

searchTestFolder использует метод fs#existSync, чтобы проверить, существует ли папка «test/» в проекте.

Функция getTestFiles читает содержимое папки «test» с помощью метода fs#readdirSync и возвращает его.

runTestFiles принимает файлы в массиве, просматривает их с помощью метода forEach и использует метод require для запуска каждого файла.

Структура папки kwuo выглядит следующим образом:

Тестирование фреймворка

Попробуем протестировать наш фреймворк с реальным проектом Node.

Создаем проект:

mkdir examples mkdir examples/math cd examples/math npm init -y

Создаем папку src и добавляем add.js и sub.js:

mkdir src touch src/add.js src/sub.js

Содержимое add.js и sub.js:

// src/add.js function add(a, b) { return a+b } module.exports = add // src/sub.js function sub(a, b) { return a-b } module.exports = sub

Создаем папку test и тестовые файлы:

mkdir test touch test/add.spec.js test/sub.spec.js

Файлы спецификации будут проверять функции add и sub в add.js и sub.js:

// test/sub.spec.js const sub = require('../src/sub') describe("Subtract numbers", () => { it("should subtract 1 from 2", () => { expect(sub(2, 1)).toEqual(1) }) it("should subtract 2 from 3", () => { expect(sub(3, 2)).toEqual(1) }) }) // test/add.spec.js const add = require('../src/add') describe("Add numbers", () => { it("should add 1 to 2", () => { expect(add(1, 2)).toEqual(3) }) it("should add 2 to 3", () => { expect(add(2, 3)).toEqual(5) }) }) describe('Concat Strings', () => { let expected; beforeEach(() => { expected = "Hello"; }); afterEach(() => { expected = ""; }); it('add Hello + World', () => { expect(add("Hello", "World")) .toEqual(expected); }); });

Теперь воспользуемся «test» в разделе «scripts» в package.json для запуска тестового фреймворка:

{ "name": "math", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "kwuo" }, "keywords": [], "author": "Chidume Nnamdi <[email protected]>", "license": "ISC" }

Запускаем npm run test в командной строке:

$ npm run test

Результат теста будет следующим:

В статистике указано количество пройденных тестов в общей сложности и список тестовых наборов с маркировкой пройден/неудачный. «add Hello + World» возвращает «HelloWorld», однако ожидалось «Hello». После исправления и повторного запуска, все тесты будут пройдены.

// test/add.spec.js ... describe('Concat Strings', () => { let expected; beforeEach(() => { expected = "Hello"; }); afterEach(() => { expected = ""; }); it('add Hello + World', () => { expect(add("Hello", "")) .toEqual(expected); }); });

Как видите, созданный тестовый фреймворк работает, как Jest и Jasmine.

Код на Github

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

Фреймворк можно использовать из NPM:

cd IN_YOUR_NODE_PROJECT npm install kwuo -D

Измените «test» в package.json на следующее:

{ ... "scripts": { "test": "kwuo" ... } }

Перевод статьи Chidume Nnamdi 🔥💻🎵🎮: Build Your Own JavaScript Testing Framework


Поделиться статьей:


Вернуться к статьям

Комментарии

    Ничего не найдено.