Рассмотрим как организовать юнит-тестирование для проектов на TS. Предполагается, что проект управляется npm. Потребуется установить три пакета для организации Unit-тестирования — это mocha (читается как мокка
), chai и nyc.
Для JS этих пакетов было бы достаточно, но, т.к. у нас TS, то нужно еще описание типов: @testdeck/mocha, @types/chai, @types/mocha.
Вы можете установить их вручную, или посмотреть следующий пример конфигурации package.json, который включает минимальный требуемый набор пакетов. Версии, конечно же, могут отличаться.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
{ "name": "Testing environmemt sample", "version": "1.0.0", "main": "index.js", "scripts": { "run": "tsc.cmd && node ./dest/index.js", "test": "nyc ./node_modules/.bin/_mocha 'tests/**/*.test.ts'" }, "author": "", "license": "ISC", "keywords": [], "description": "", "devDependencies": { "@testdeck/mocha": "^0.3.3", "@types/chai": "^4.3.4", "@types/mocha": "^10.0.1", "chai": "^4.3.7", "mocha": "^10.2.0", "nyc": "^15.1.0", "ts-node": "^10.9.1", "typescript": "^4.9.4" } } |
Mocha
Мокка — это, собственно, самый важный компонент этой инсталляции — фреймворк для тестирования. Чаще всего используют либо его, либо JEST.
Для него нужно создать дополнительный файл конфигурации в корне проекта — .mocharc.json
1 2 3 4 |
{ "require": "./register.js", "reporter": "list" } |
Как видите, он ссылается еще на один файл, где мы настроим TS для тестирования, это ./register.js
1 2 3 4 5 6 7 8 9 |
const tsNode = require('ts-node'); const testTSConfig = require('./tests/tsconfig.json'); tsNode.register({ files: true, transpileOnly: true, project: './tests/tsconfig.json' }); |
Это позволяет сделать независимую настройку typescript для фазы тестирования.
Chai
Это node.js библиотека проверки утверждений, внедряющая интерфейсы тестирования. Её можно подключить и как пакет и просто как js-скрипт.
Обычно, создавая тесты, вы пишите какие то утверждения (assertions), ожидая какой то результат. Chai предоставляет целых три интерфейса для написания тест-кейсов: should, expect и assert (или, если угодно, называйте это стилями). Мне лично нравится should, который превращает код в подобие человеку-понятного текста (посмотрим на него и восхитимся чуть позднее).
Chai помогает как с TDD, так и с BDD. Т.е. вы можете писать, используя его, как поведенческие, интеграционные тесты, так и модульные Unit-тесты, о которых идет речь в данной статье.
NYC
Еще один компонент, который мы используем в данной сборке, нужен для анализа покрытия кода тестами. Он вроде бы так и расшифровывается — now you're covered
Подробнее можете ознакомиться с возможностями на странице пакета.
Нам нужно сконфигурировать его, создав файл .nyrc.json
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
{ "extends": "@istanbuljs/nyc-config-typescript", "include": [ "src/**/*.ts" ], "exclude": [ "node_modules/" ], "extension": [ ".ts" ], "reporter": [ "text-summary", "html" ], "report-dir": "./coverage" } |
Обратите внимание, что именно через NYC мы запускаем тесты в package.json.
Общая структура проекта
В целом, структура проекта будет выглядеть вот так:
В /src у нас находится index.ts, а также есть модуль, который экспортирует пример функции.
Для полноты картины приведу конфигурационные файлы TS
/tsconfig.json
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
{ "exclude": [ "./node_modules/", "./dist/", "./tests/" ], "compilerOptions": { "target": "es2021", "experimentalDecorators": true, "module": "commonjs", "outDir": "./dest", "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "strict": true, "skipLibCheck": true } } |
/tests/tsconfig.json
Он немного отличается от основного, был создан как результат определенного опыта использования mocha + chai.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
{ "extends": "../tsconfig.json", "compilerOptions": { "baseUrl": "./", "module": "commonjs", "experimentalDecorators": true, "strictPropertyInitialization": false, "isolatedModules": false, "strict": false, "strictNullChecks": true, "noImplicitAny": false, "typeRoots" : [ "../node_modules/@types" ] }, "exclude": [ "../node_modules" ], "include": [ "./**/*.ts" ] } |
Тестируемый модуль
Перейдем от настройки к практике. У нас есть модуль — /src/modules/module.ts, в котором объявлена и экспортируется функция. В данном случае функция — это задачка с leetcode под номером 1701 — https://leetcode.com/problems/average-waiting-time/
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
export const moduleFunc = averageWaitingTime; function averageWaitingTime(customers: number[][]): number { let chief = 0; let totalTime = 0; customers.forEach(elm => { if (elm[0] >= chief) { totalTime += elm[1]; chief = elm[0] + elm[1]; } else { totalTime += chief + elm[1] - elm[0]; chief += elm[1]; } }) return totalTime / customers.length; }; |
На входе она ожидает массив из кортежей, а возвращает число.
Теперь мы хотим написать тесты для нашей функции.
Тестирование
Все тесты должны располагаться в папке /test, и иметь расширение *.test.ts. Это декларируется строкой запуска тестов в package.json:
1 |
"test": "nyc ./node_modules/.bin/_mocha 'tests/**/*.test.ts'" |
Я создал несколько тестов для нашей функции в файле /tests/module.test.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
// импортируем тестируемый модуль import { moduleFunc } from '../src/modules/module'; // мокка + чай import { suite, test } from '@testdeck/mocha'; import * as _chai from 'chai'; // мы используем интерфейс should _chai.should(); @suite export class sampleModuleTest { @test 'SampleTest #1' (): void { moduleFunc([[5,2],[5,4],[10,3],[20,1]]).should.be.equal(3.25); } @test 'SampleTest #2' (): void { moduleFunc([[1,2],[2,5],[4,3]]).should.be.equal(5); } @test 'SampleTest #3' (): void { moduleFunc([[2,3],[6,3],[7,5],[11,3],[15,2],[18,1]]).should.be.closeTo(4.166667, 0.0001); } } |
Выбранный should интерфейс Сhai позволяет создавать цепочечные утверждения, которые, по сравнению с унылыми assert сравнениями, будут понятны и тестировщиками и бизнес-аналитикам. Один раз попробовав should, я больше не пишу в assert стиле :).
Ну вот для сравнения:
1 |
assert.equal(foo, 'bar'); |
Запуская npm run test, вы получите результат вида: