В экосистеме JavaScript насчитывается много средств выполнения тестов: Jest, Karma, Ava, Mocha. И это лишь малая их часть. Встроенный тест-раннер имеется в Deno. Экосистема со встроенным тестом и fmt — более зрелая и лучшая, на мой взгляд.
Итак, какова цель этой статьи? По сути, она заключается в том, чтобы показать: в сегодняшней экосистеме nodejs сделать эффективный тест-раннер несложно.
Изначально хотелось написать его на Rust, поэтому я задался вопросом: «Каковы основные технические требования?». Нужно:
- Исходный код теста вместе с его зависимостями, а значит, понадобится упаковщик вроде SWC.
SWC — это расширяемая платформа на Rust для следующего поколения быстрых инструментов разработчика. Она применяется в Next.js, Parcel и Deno и таких компаниях, как Vercel, ByteDance, Tencent, Shopify.
2. Поддержка TypeScript, поэтому понадобится предварительный этап транспиляции в JavaScript — здесь тоже пригодится SWC.
3. Запускать этот код в среде выполнения JavaScript. Попробуем крейт rusty_v8 из проекта deno (крейт — это библиотека, напоминает модуль node в диспетчере пакетов npm). API показался отличным — создаешь скрипт и запускаешь изолированно (если я правильно понимаю).
4. Сделать API самогó тестового набора и структуры тестов, причем в JavaScript, ведь применять их будут пользователи-разработчики этого языка.
К моменту формулирования этого требования стало понятно, что писать API на Rust не стоит и 80 строками он не ограничится.
CLI «Run-Chewy»
Я решил написать крошечный тест-раннер run-chewy на nodejs, просто чтобы лучше разобраться в основном пакете SWC и поэкспериментировать с рабочими потоками. В SWC предоставляется простой API на JavaScript.
Точка входа
Тест-раннер — это инструмент CLI. Ограничусь передачей в него одного аргумента isTS
для указания пользователем, применяет он TypeScript или нет. Альтернативой может быть конфигурационный файл chewy.json
.
Точка входа проекта — файл cli.js
, которым запускается метод run()
:
const yargs = require('yargs/yargs')
const { hideBin } = require('yargs/helpers')
const argv = yargs(hideBin(process.argv)).argv
const ChewyRunner = require('./lib/runner');
if (argv.isTS) {
console.log("running")
ChewyRunner.run({isTS: argv.isTS});
} else {
console.log("supply the isTS arg to decide if running js or ts tests")
}
Тест-раннер
Вот основные технические требования по тест-раннеру. Что нужно:
- Список тестовых файлов для запуска, он должен браться из текущей запускаемой папки.
- Перебрать список и спарсить каждый файл, чтобы получить полный исходный код (с зависимостями).
- В случае с TypeScript перед выполнением транспилировать исходный код в JavaScript.
- Выполнить этот код параллельно.
- Вывести результаты теста или (в случае любой ошибки времени выполнения) ошибку.
const workerThreads = require('node:worker_threads');
const swc = require('@swc/core');
const fs = require('fs');
const path = require('path');
function grabFilesSync(currentDirPath) {
const files = [];
const walker = (_path, cb) => {
fs.readdirSync(_path).forEach((name) => {
const filePath = path.join(_path, name);
const stat = fs.statSync(filePath);
if (stat.isFile()) {
if (filePath.match(/.*.spec.ts|.*.test.ts|.*.spec.js|.*.test.js/)) {
cb(filePath);
}
} else if (stat.isDirectory()) {
walker(filePath, cb);
}
});
};
walker(currentDirPath, (filePath) => {
files.push(filePath);
});
return files;
}
function initOptions(config) {
return {
isModule: true,
module: {
type: "commonjs"
},
jsc: {
target: "es2020",
parser: config.isTS ? {syntax: "typescript"} : {syntax: "ecmascript"},
transform: null,
}
}
}
class ChewyRunner {
static run(config) {
if (workerThreads.isMainThread) {
const tests = grabFilesSync(process.cwd());
for (const t of tests) {
console.log("Running ", t); //выполнить добавление цветов и форматов
fs.promises.readFile(t)
.then((buff) => swc.transform(buff.toString(), initOptions(config)))
.then((output) => {
const worker = new workerThreads.Worker(output.code, { eval: true });
worker.on('exit', (c) => console.log('done')); // выполнить улучшенную обработку ошибок
});
}
}
}
}
module.exports = ChewyRunner;
Внимание! В менее чем 60 строках кода здесь включены основные, определенные нами требования. Но кое-что отсутствует:
- Пул потоков, которым бы ограничивалось их создание. Вместо этого для каждого теста инициируется новый рабочий поток.
- Метод
run
асинхронный, но отлова события ошибки в рабочем потоке (и отклонения) нет. - В этой реализации нет поддержки es-модулей.
- Для написания раннера не применялся TypeScript.
Интересная, на мой взгляд, часть реализации — применение API на JavaScript из SWC. Здесь можно освоить его настройку.
API
В API должен быть группирующий блок для запуска тестов в «наборе» и описание этого набора, то же для тестового блока, а также поддержка асинхронных функций. Блок должен быть готов выполняться немедленно, аналогично структуре describe
обычных фреймворков тестирования. В API должны быть блоки сравнения (необязательно при наличии внешних библиотек):
Chewy.suite('suite', async () => {
console.log('suite cb');
Chewy.test('test', () => {
Chewy.assertEq([[[1, 2, 3]], 4, 5], [[[1, 2, '3']], 4, 5]);
})
})
const assert = require("node:assert");
const isAsync = (fn) => fn.constructor.name === 'AsyncFunction'; // https://github.com/tc39/proposal-async-await/issues/78#issuecomment-164408675
const noop = () => {};
const group = (description, handler) => {
console.log(`Running: [ ${description} ]`);
if (isAsync(handler)) {
return handler().then(noop)
.catch((err) => {
console.error(err)
});
}
handler();
}
class Chewy {
static suite = group;
static test = group;
static x = Chewy; //выполнить пропуск реализации
static assertEq = (actual, expected) => assert.strict.deepEqual(actual, expected);
}
module.exports = Chewy;
Внимание! В ~20 строках кода здесь включены основные, определенные нами требования. Но кое-какой функционал отсутствует, например улучшенная обработка ошибок, хуки before
/teardown
, реализация пропуска теста/набора и, возможно, заглушки.
Заключение
Можно ли написать простой раннер на nodejs почти без зависимостей? Запросто! Я не публиковал в npm, так что не используйте его дома.
Читайте также:
- Как профессионально использовать сопоставимые типы TypeScript
- Создание хука Git pre-commit для автопроверки и исправления кода JavaScript и TypeScript
- Rest и Spread в JavaScript. Возможности, о которых вы не знали
Читайте нас в Telegram, VK и Дзен
Перевод статьи Liron Hazan: Write your own Javascript/Typescript tests runner in 80 lines of code </>