Node.js のテストランナー機能が思ったより便利なのかも
◆ package.json を作らずパッケージ化してないちょっとしたスクリプトが多い
◆ npm ライブラリのテストツールを入れるなら package.json を作ってパッケージ化が必要
◆ それらをテストのためだけにパッケージ化したくない
◆ ちょっとしたスクリプトに対してかなり重たいテストツールをインストールしたくない
◆ ビルトイン機能なのでパッケージ化不要で簡単に使える
◆ npm ライブラリのテストツールを入れるなら package.json を作ってパッケージ化が必要
◆ それらをテストのためだけにパッケージ化したくない
◆ ちょっとしたスクリプトに対してかなり重たいテストツールをインストールしたくない
◆ ビルトイン機能なのでパッケージ化不要で簡単に使える
少し前に Node.js ではテストを実行する機能が追加されて LTS では 16 と 18 で使えます
Node.js のテスト機能は npm パッケージに色々あり Node.js の組み込みに追加されても別にどうでもいいかなとスルーでした
しかし考えてみると結構良いものなのかもしれません
1 つか 2 つの .js ファイルだけのところを いちいちパッケージ化はしたくないです
ちょっとしたスクリプトならわざわざテストコードを書くまでも無いことも多いのですが ロジックが少し複雑なものだとテストツールを使ったテストをしておきたいケースもあります
使い方の説明や 入力に対してどんな出力が起きるのかを文章で書くよりテストコードを書いておいてそれを見る方が楽ってこともありますし
そういうときに Node.js に組み込みの機能なら パッケージ化したりせず簡単に使えます
他でよく見るのに似たフォーマットなので簡単でした
ありがちなものですが add.js が足し算する関数をエクスポートしてる場合はこういう感じです
出力の方はあんまり見やすくないものでした
実際には ちょっとしたスクリプトだと こういうライブラリ的な部分に分けず そのままトップレベルでなにかの処理をすることが多いです
例えば 引数をファイルに追記するスクリプトがあったとします
[append-message.js]
この場合だとこういう感じでしょうか
あまりわかりやすくないですね
それに require を使うと 1 回しか実行できません
別パターンで実行したいこともありえますし グローバル変数を色々書き換えるスクリプトの場合もありえます
別プロセスで実行するほうがいいかもしれません
ただ パッケージ化してる部分になると 組み込み機能は機能が少なめなので jest などを入れたほうが良いと思います
個人的には モジュールのモック機能は必須レベルに重要なのですが 現状ではありません
Node.js 19 ではモック機能はあるのですが 関数やメソッド用です
require を置き換えて 本来のモジュールを読み込まずにテスト用モジュールに置き換えたいのですが そういうことはできないです
Node.js には yarn pnp などで使われる loader のカスタマイズ機能はあるらしいので その辺をうまく使って実現してくれるといいのですけど
https://nodejs.org/docs/latest-v18.x/api/test.html#describename-options-fn
ここによれば describe を使うとコールバックの最初に SuiteContext を受け取るようです
test だと TestContext が受け取れるところが SuiteContext になっているようです
実際に試してみます
結果は
となりました
SuiteContext ではなく空の配列でした
ちなみに現状だと 「node --test」 でテストモードとして実行すると console.log の中身はコンソールに表示されません
console.error でエラー出力に送ったり process.stdout.write で直接書き込んでも同じです
サブプロセスで実行して出力は捨ててそうです
なので 「--test」 をつけずに 「node test.js」 のように普通に実行するか util.inpsect でフォーマットしてファイルに書き込みするかしないと結果が見れません
https://github.com/nodejs/node/blob/v18.12.1/lib/internal/test_runner/test.js#L705
で describe に渡した関数 this.fn が実行されています
runInAsyncScope は 3 つ目以降の引数を ...args で受け取り ReflectApply で展開します
なので runInAsyncScope に渡すところで ... で展開しておく必要があるはずです
それがないので配列として受け取っているようです
test の方だとちゃんと TestContext が受け取れていますが test の方では
となっていて runInAsyncScope を直接呼び出さず ReflectApply 経由で呼び出しているからのようです
最新版だと修正済みかなと 19 の方を見ましたが 今のところ変わりないようです
それより気になるのは配列が空だったんですよね
渡し方がおかしくても SuiteContext が配列の 1 つ目の要素には入っていてもよさそうなのですけど
getRunArgs を見ると args は [] で固定なので ドキュメントのほうが間違ってるのかもしれません
this として受け取れる ctx は
なので第一引数ではなくて this で受け取れるが正しいのかもです
ただ すべてアロー関数で済ませたいので this が必須になるのはできればやめてほしいのですけどね
Test の方の run メソッドだと this.fn と ctx を args に unshift で追加してるので同じものを期待して args を空にしてるだけとも考えられます
なんにせよ バグなのかよくわからないものもある状況なので 本格的に使うなら experimental ステータスを抜けるのを待ったほうがいいのかもしれませんね
Node.js のテスト機能は npm パッケージに色々あり Node.js の組み込みに追加されても別にどうでもいいかなとスルーでした
しかし考えてみると結構良いものなのかもしれません
パッケージ化したくないとき
package.json を作ってパッケージ化している場所ならテスト用のパッケージを追加するだけですが パッケージ化していないちょっとしたスクリプトというものも結構あります1 つか 2 つの .js ファイルだけのところを いちいちパッケージ化はしたくないです
ちょっとしたスクリプトならわざわざテストコードを書くまでも無いことも多いのですが ロジックが少し複雑なものだとテストツールを使ったテストをしておきたいケースもあります
使い方の説明や 入力に対してどんな出力が起きるのかを文章で書くよりテストコードを書いておいてそれを見る方が楽ってこともありますし
そういうときに Node.js に組み込みの機能なら パッケージ化したりせず簡単に使えます
例
簡単に使ってみました他でよく見るのに似たフォーマットなので簡単でした
ありがちなものですが add.js が足し算する関数をエクスポートしてる場合はこういう感じです
const assert = require("assert")
const { test, it, describe } = require("node:test")
const add = require("./add.js")
test("1 + 1", (t) => {
assert.strictEqual(add(1, 1), 2)
})
出力の方はあんまり見やすくないものでした
root@694e65721638:/tmp/nodetest1# node --test add.test.js
TAP version 13
# Subtest: /tmp/nodetest1/add.test.js
ok 1 - /tmp/nodetest1/add.test.js
---
duration_ms: 64.5171
...
1..1
# tests 1
# pass 1
# fail 0
# cancelled 0
# skipped 0
# todo 0
# duration_ms 69.1822
実際には ちょっとしたスクリプトだと こういうライブラリ的な部分に分けず そのままトップレベルでなにかの処理をすることが多いです
例えば 引数をファイルに追記するスクリプトがあったとします
[append-message.js]
const fs = require("fs")
const [message] = process.argv.slice(2)
if (message) {
fs.appendFileSync("./output", message)
}
この場合だとこういう感じでしょうか
const assert = require("assert")
const { test, it, describe } = require("node:test")
const fs = require("fs")
test("append message", (t) => {
fs.writeFileSync("./output", "foo")
const argv = process.argv
process.argv = ["", "", "message1"]
require("./append-message.js")
process.argv = argv
assert.strictEqual(fs.readFileSync("./output").toString(), "foomessage1")
})
あまりわかりやすくないですね
それに require を使うと 1 回しか実行できません
別パターンで実行したいこともありえますし グローバル変数を色々書き換えるスクリプトの場合もありえます
別プロセスで実行するほうがいいかもしれません
機能は少なめ
そんな感じで基本的なテスト機能があるので パッケージ化したくないときや軽く使いたい場合には良さそうですただ パッケージ化してる部分になると 組み込み機能は機能が少なめなので jest などを入れたほうが良いと思います
個人的には モジュールのモック機能は必須レベルに重要なのですが 現状ではありません
Node.js 19 ではモック機能はあるのですが 関数やメソッド用です
require を置き換えて 本来のモジュールを読み込まずにテスト用モジュールに置き換えたいのですが そういうことはできないです
Node.js には yarn pnp などで使われる loader のカスタマイズ機能はあるらしいので その辺をうまく使って実現してくれるといいのですけど
ドキュメントと一致しない
まだ experimental のステータスなので仕方ないのでしょうがドキュメントと一致しない挙動がありましたhttps://nodejs.org/docs/latest-v18.x/api/test.html#describename-options-fn
ここによれば describe を使うとコールバックの最初に SuiteContext を受け取るようです
test だと TestContext が受け取れるところが SuiteContext になっているようです
実際に試してみます
const { test, it, describe } = require("node:test")
test("test", test_ctx => {
console.log(test_ctx)
})
describe("describe", suite_ctx => {
console.log(suite_ctx)
})
結果は
TestContext {}
[]
となりました
SuiteContext ではなく空の配列でした
ちなみに現状だと 「node --test」 でテストモードとして実行すると console.log の中身はコンソールに表示されません
console.error でエラー出力に送ったり process.stdout.write で直接書き込んでも同じです
サブプロセスで実行して出力は捨ててそうです
なので 「--test」 をつけずに 「node test.js」 のように普通に実行するか util.inpsect でフォーマットしてファイルに書き込みするかしないと結果が見れません
原因
SuiteContext ではなく 空のオブジェクトでもなく なぜか配列になっているのが気になったのでソースをみてみましたhttps://github.com/nodejs/node/blob/v18.12.1/lib/internal/test_runner/test.js#L705
PromiseResolve(this.runInAsyncScope(this.fn, ctx, args)),
で describe に渡した関数 this.fn が実行されています
runInAsyncScope は 3 つ目以降の引数を ...args で受け取り ReflectApply で展開します
なので runInAsyncScope に渡すところで ... で展開しておく必要があるはずです
それがないので配列として受け取っているようです
test の方だとちゃんと TestContext が受け取れていますが test の方では
const ret = ReflectApply(this.runInAsyncScope, this, runArgs);
となっていて runInAsyncScope を直接呼び出さず ReflectApply 経由で呼び出しているからのようです
最新版だと修正済みかなと 19 の方を見ましたが 今のところ変わりないようです
それより気になるのは配列が空だったんですよね
渡し方がおかしくても SuiteContext が配列の 1 つ目の要素には入っていてもよさそうなのですけど
getRunArgs を見ると args は [] で固定なので ドキュメントのほうが間違ってるのかもしれません
this として受け取れる ctx は
return { ctx: { signal: this.signal, name: this.name }, args: [] };
なので第一引数ではなくて this で受け取れるが正しいのかもです
ただ すべてアロー関数で済ませたいので this が必須になるのはできればやめてほしいのですけどね
Test の方の run メソッドだと this.fn と ctx を args に unshift で追加してるので同じものを期待して args を空にしてるだけとも考えられます
なんにせよ バグなのかよくわからないものもある状況なので 本格的に使うなら experimental ステータスを抜けるのを待ったほうがいいのかもしれませんね