Node.jsのTest runnerを使ってみた
Test runnerとは
Node.jsに標準で搭載されたテストツール。
The node:test module facilitates the creation of JavaScript tests.
v18で追加された。v20で安定版に。
Test Runnerの実行方法
node --test
コマンドで実行できる。 npm init
した直後のプロジェクトで実行すると以下のように出力される。
なにやら出力された。綺麗だ。
$ node --test
ℹ tests 0
ℹ suites 0
ℹ pass 0
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 3.725192
今回テストするサンプルコード
入力した関数を指定回数実行してからエラーをthrowするシンプルなリトライ関数をテストする。最近自前で書いてお世話になったので。
const retry = async (someFunction, maxRetryCount) => {
let currentCount = 0;
let err = null;
while (maxRetryCount >= currentCount) {
try {
return await someFunction();
} catch (e) {
currentCount++;
err = e;
continue;
}
}
throw err;
};
module.exports = {
retry,
};
まずは簡単にテストを書いてみる
テストコード
ドキュメントを読みながらまずはシンプルなテストを。
// node:testパッケージが今回メインテーマのTest Runner
// assert関連の関数も標準搭載されているのでこちらを使う
// assertは実装されてから結構長い標準パッケージのようだ
const test = require("node:test");
const assert = require("node:assert");
const lib = require("./lib");
// 簡単な数値比較
test("Success test", async (t) => {
assert.strictEqual(1, 1);
});
// async関数もテストできる
// バージョンアップのたびに状況が良くなっているとはいえNode.jsで気軽にasyncな関数を検証できるのは便利
test("Success async retry.", async (t) => {
const res = await lib.retry(() => {
return 123;
}, 2);
assert.strictEqual(res, 123);
});
実行
*.test.js
というファイル名でテストファイルを作成すると、ファイル名を指定せずともTest Runnerが自動実行してくれる。便利。
以下のような感じでテストケースの実行結果を出力してくれる。
$ node --test lib.test.js
✔ Success test (0.947609ms)
✔ Success async retry. (0.173258ms)
ℹ tests 2
ℹ suites 0
ℹ pass 2
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 49.756683
ネストしたテストケース
テストのなかにテストも定義できる。
const test = require("node:test");
const assert = require("node:assert");
const lib = require("./lib");
// リトライが動作しているかテストしたい
test("Retry function.", async (t) => {
let loopCount = 0;
try {
const res = await lib.retry(async () => {
loopCount++;
throw new Error("test");
}, 2);
} catch {}
assert.strictEqual(loopCount, 3);
});
// ネストしたテスト
test("Retry function. Nested test.", async (t) => {
let loopCount = 0;
try {
const res = await lib.retry(async () => {
loopCount++;
// testのなかにtestが
await t.test(`checkpoint ${loopCount}`, (t) => {
assert.ok(loopCount);
});
throw new Error("test");
}, 2);
} catch {}
assert.strictEqual(loopCount, 3);
});
実行
Retry function. Nested test.
の中にテスト結果が出力されている。
$ node --test lib2.test.js
✔ Retry function. (1.133513ms)
▶ Retry function. Nested test.
✔ checkpoint 1 (0.163339ms)
✔ checkpoint 2 (0.097078ms)
✔ checkpoint 3 (0.077512ms)
▶ Retry function. Nested test. (0.918553ms)
ℹ tests 5
ℹ suites 0
ℹ pass 5
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 55.539431
Mocking
オブジェクトの一部のメソッドの挙動を差し替えるなど、モック関連の機能も備えている。関連機能が便利だったので少し紹介。
テストコード
const assert = require("node:assert");
const { test } = require("node:test");
test("Function call count", (context) => {
// fnが実行されるたびにfn.mock.callCount()がインクリメントされる
// mock.fnに入力された引数なんかも追跡できる
// 対象のコードにmock.fnを差し込んで関数の挙動を追跡するテストなんかも場合によっては便利そう
const fn = context.mock.fn(() => 1);
assert.strictEqual(fn.mock.callCount(), 0);
fn();
assert.strictEqual(fn.mock.callCount(), 1);
fn();
assert.strictEqual(fn.mock.callCount(), 2);
});
test("setTimeout", (context) => {
// mock timerのセットアップ setTimeoutに対してtimersを有効にする
const fn = context.mock.fn();
context.mock.timers.enable(["setTimeout"]);
// 有効にしている間はsetTimeoutの時間が進まない
setTimeout(fn, 9999);
assert.strictEqual(fn.mock.callCount(), 0);
// 時間を1進める
context.mock.timers.tick(1);
assert.strictEqual(fn.mock.callCount(), 0);
// 時間を9997進める 1 + 9997 = 9998
context.mock.timers.tick(9997);
assert.strictEqual(fn.mock.callCount(), 0);
// 時間を1進める 9998 + 1 = 9999
context.mock.timers.tick(1);
assert.strictEqual(fn.mock.callCount(), 1);
});
実行
setTimeoutを使ったテストがちゃんと通過した。v20.8.1時点では実験的なAPIだよと警告が表示される。
(node:760115) ExperimentalWarning: The MockTimers API is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
✔ Function call count (1.052185ms)
✔ setTimeout (0.663746ms)
ℹ tests 2
ℹ suites 0
ℹ pass 2
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 54.301783
テストカバレッジ
v20.8.1時点では実験的機能だがカバレッジなんかも出力できる。
node --test --experimental-test-coverage
で出力可能。
$ node --test --experimental-test-coverage
✔ Success test (1.315008ms)
✔ Success async retry. (0.210672ms)
✔ Retry function. (1.96745ms)
▶ Retry function. Nested test.
✔ checkpoint 1 (0.265709ms)
✔ checkpoint 2 (0.089615ms)
✔ checkpoint 3 (0.099715ms)
▶ Retry function. Nested test. (1.153531ms)
▶ Retry function
✔ should success (0.92665ms)
▶ should fail
✔ checkpoint 1 (0.221682ms)
✔ checkpoint 2 (0.131076ms)
✔ checkpoint 3 (0.079023ms)
▶ should fail (1.222688ms)
▶ Retry function (5.024167ms)
(node:760630) ExperimentalWarning: The MockTimers API is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
✔ Function call count (1.982019ms)
✔ setTimeout (1.384543ms)
ℹ tests 14
ℹ suites 1
ℹ pass 14
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 93.680134
ℹ start of coverage report
ℹ ------------------------------------------------------------------
ℹ file | line % | branch % | funcs % | uncovered lines
ℹ ------------------------------------------------------------------
ℹ lib.js | 100.00 | 100.00 | 100.00 |
ℹ lib.test.js | 100.00 | 100.00 | 100.00 |
ℹ lib2.test.js | 100.00 | 75.00 | 100.00 |
ℹ lib3.test.js | 100.00 | 77.78 | 100.00 |
ℹ mocktimer.test.js | 100.00 | 100.00 | 100.00 |
ℹ ------------------------------------------------------------------
ℹ all files | 100.00 | 85.71 | 100.00 |
ℹ ------------------------------------------------------------------
ℹ end of coverage report
ひとまずここまで。ちょっと遅れてのキャッチアップだったけれども知れてよかった。
Rustのcargo testなんかを使っていると思うが、やっぱり言語が標準でテストツールを準備してくれるのは悩む時間を減らすことにつながるので良い。Jestセットアップ面倒だなと思うこともあったので個人的には刺さった機能だ。