`node:test` のスナップショットテスト(--experimental-test-snapshots)を試す
この記事の中でスナップショットテスト(--experimental-test-snapshots
)については名前だけで具体例が無かったのですが、ちょうどJestのスナップショットテストを使っているOSSをメンテしていたのでJest -> node:test
に置き換えられるか試してみました。
node:test
+ TypeScriptのセットアップ
実際のコードを置き換えてみる前に、最小限のコードで node:test
とTypeScriptを使ったテストを実行する方法を試してみます。
まずは node --test --experimental-stryp-types
でTypeScriptのテストを実行できる環境からセットアップします。実行環境のNode.jsは執筆時点でLTS v22最新のv22.11.0を使用し、import/exportを利用するため "type": "module"
でESMにします。package.jsonはこのようになりました。
// package.json
{
"name": "node_test_snapshot",
"private": true,
"type": "module",
"engines": {
"node": "=22.11.0"
},
"scripts": {
"test": "node --test --experimental-strip-types 'tests/**/*.test.ts'"
}
}
最小構成のテストのためにまずは実装側のコードを簡単に用意します。
// src/add.ts
export function add(a: number, b: number): number {
return a + b;
}
テスト側ではこの add()
関数をimportしてテストします。自分はdescribe, itスタイルが好みなのですが node:test
はこのスタイルもサポートしているので以下のように書くことができます。
// tests/add.test.ts
import { add } from '../src/add.ts';
import { describe, it } from 'node:test'; // Cannot find module 'node:test' or its corresponding type declarations.ts(2307)
import assert from 'node:assert/strict'; // Cannot find module 'node:test' or its corresponding type declarations.ts(2307)
describe('add', () => {
it('1 + 1 = 2', async () => {
const actual = add(1, 1)
assert.strictEqual(actual, 2)
})
})
ここまでのpackage.jsonの構成ではVSCode上で型エラーが出てしまいますが、直し方は後述します。 --experimental-strip-types
はNode.jsの実行時に型情報を削除して実行するオプションなので、VSCode上でエラーが出ていても実行自体に問題はありません。
node --test --experimental-strip-types 'tests/**/*.test.ts'
を実行するとテストが実行されて成功するはずです。
$ node --test --experimental-strip-types 'tests/**/*.test.ts'
(node:26537) ExperimentalWarning: Type Stripping is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
(node:26544) ExperimentalWarning: Type Stripping is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
▶ add
✔ 1 + 1 = 2 (0.602881ms)
✔ add (0.937778ms)
ℹ tests 1
ℹ suites 1
ℹ pass 1
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 61.516127
--experimental-strip-types
によってtscやJestなどを使わずとも.tsのテストを直接実行できました。後はVSCode上の型エラーの解消ですが、これはNode.jsの標準ライブラリの型情報が無いためなので @types/node
をインストールすれば解消されます。注意点として@types/node
のバージョンはv22以上をインストールしましょう。今までv20を利用していた環境などで @types/node
が古いままだと VSCode上で node:test
の型エラーが出てしまってハマることになります(実体験)。
この記事を執筆している時点では @types/node
の最新バージョンはv22.10.1だったのでこちらをインストールしました。
// package.json
{
"name": "node_test_snapshot",
"private": true,
"type": "module",
"engines": {
"node": "=22.11.0"
},
"scripts": {
"test": "node --test --experimental-strip-types 'tests/**/*.test.ts'"
},
"devDependencies": {
"@types/node": "22.10.1"
}
}
これでVSCode上の型エラーは出なくなったはずです。
スナップショット(--experimental-test-snapshots)の使い方
やっと本題のスナップショットです。スナップショットの対象は何でもいいのですが、先ほどの add()
関数を使って適当なオブジェクトを作ってそれをスナップショットテストしてみます。
先ほどのテストに雑にスナップショットテストも追加しました。全てのコードを以下に示します。
// tests/add.test.ts
import { add } from '../src/add.ts';
import { describe, it } from 'node:test';
import assert from 'node:assert/strict';
describe('add', () => {
it('1 + 1 = 2', async () => {
const actual = add(1, 1)
assert.strictEqual(actual, 2)
})
})
describe('snapshot', () => {
it('1 + 1 = 2', async (t) => {
const actual = {
"expression": "add(1, 1)",
"result": add(1, 1)
}
t.assert.snapshot(actual)
})
})
スナップショットテストの実行には現状では --experimental-test-snapshots
オプションが必要です。これを付けないと TypeError [Error]: t.assert.snapshot is not a function
のようなエラーが出てしまいます。オプションを付けて実際に実行してみるとこのようにエラーとなります。
$ npm run test
> test
> node --test --experimental-strip-types --experimental-test-snapshots 'tests/**/*.test.ts'
(node:527768) ExperimentalWarning: Type Stripping is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
(node:527774) ExperimentalWarning: Type Stripping is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
(node:527774) ExperimentalWarning: Snapshot testing is an experimental feature and might change at any time
▶ add
✔ 1 + 1 = 2 (0.259715ms)
✔ add (0.616412ms)
▶ snapshot
✖ 1 + 1 = 2 (0.427018ms)
Error [ERR_INVALID_STATE]: Invalid state: Cannot read snapshot file '/home/kesin/github/kesin11-private/blog/20241130_node_test_snapshot/tests/add.test.ts.snapshot.' Missing snapshots can be generated by rerunning the command with the --test-update-snapshots flag.
Jestなどでスナップショットテストを使ったことがある人はピンと来たと思いますが、まだ一度も実行したことがないので正解となるスナップショットファイルが生成されていないために出ているエラーですね。書かれている通り、--test-update-snapshots
オプションを付けて再度実行しましょう。
$ node --test --experimental-strip-types --experimental-test-snapshots --test-update-snapshots 'tests/**/
*.test.ts'
(node:529033) ExperimentalWarning: Type Stripping is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
(node:529039) ExperimentalWarning: Type Stripping is an experimental feature and might change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
(node:529039) ExperimentalWarning: Snapshot testing is an experimental feature and might change at any time
▶ add
✔ 1 + 1 = 2 (0.515685ms)
✔ add (0.914312ms)
▶ snapshot
✔ 1 + 1 = 2 (0.295125ms)
✔ snapshot (0.350107ms)
ℹ tests 2
ℹ suites 2
ℹ pass 2
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 64.615503
これでスナップショットファイルが生成されました。テストファイルと同じディレクトリに配置されるようなので実際に中身を見てみましょう。
$ ls tests/
add.test.ts add.test.ts.snapshot
$ cat tests/add.test.ts.snapshot
exports[`snapshot > 1 + 1 = 2 1`] = `
{
"expression": "add(1, 1)",
"result": 2
}
`;
exportsというオブジェクトのキーにテストケース名、値にスナップショット対象のオブジェクトをJSONにした文字列が入っているみたいです。
スナップショットファイルが生成されたので、次回以降は --test-update-snapshots
を外して実行すれば前回の実行と比較して差分がある場合にテストがfailとなります。Jestなどと同様に作成されたスナップショットファイルは git commit
しておきましょう。
スナップショットの微妙なハマりどころ
先ほどのコードで気が付いたかもしれませんが assert.strictEqual
は node:assert
からimportしていますが、スナップショットテストは t.assert.snapshot
になります。
JestやVitestでは expect().toMatchSnapshot()
という使い方だったので、それともまた異なる形になっていて少し分かりにくいですね。自分はハマりました。
他にもスナップショットテストのドキュメント中のサンプルコードでは test('テストケース名', (t) => {})
の t.assert.snapshot
の例しか書かれていませんが、実は it
でも問題なく t.assert.snapshot
は使えたりします。
先ほどのコードは既に it
の方を使っています。再掲するとこの部分になります。
describe('snapshot', () => {
it('1 + 1 = 2', async (t) => {
const actual = {
"expression": "add(1, 1)",
"result": add(1, 1)
}
t.assert.snapshot(actual)
})
})
このような細かい部分に関しては現時点では node:test
のドキュメントを探すよりも VSCode上で定義元にジャンプして @types/node
の型定義を見た方が早いです。it
でも使えることは型定義を見て知りました。
node:test
に移行してみた
実際にスナップショットテストを含むJestのテストを 非常に小規模なOSSですが、拙作のKesin11/ts-junit2jsonのスナップショットテストをJestから node:test
に実際に移行したときのpull-requestがこちらです。
小規模なOSSなのでもともとdevDependenciesの依存も少なかったですが、Jest関連のパッケージを外すことができたのでさらにシンプルになりました。もうほとんど typescript
本体と型定義のパッケージぐらいにしか依存していないですね。
node:test
のちょっとよく分からない点
ここまでは基本的に良い話でしたが、ここからは実際に移行してみてよく分からなかった点を書いていきます。
相対パスでimportする場合に拡張子が.jsではなく.tsにする必要がある?
さきほどのテストの例で import { add } from '../src/add.ts';
というように拡張子に .ts
を付けています。しかしバンドラーなどに頼らない純粋なESMの場合はimportの拡張子は .js
にする必要があるはずです。実際 .js
に書き換えるとテスト実行がエラーになります。
$ node --test --experimental-strip-types --experimental-test-snapshots 'tests/**/*.test.ts'
Error [ERR_MODULE_NOT_FOUND]: Cannot find module '...省略.../src/add.js' imported from ...省略.../tests/add.test.ts
どうやら --experimental-strip-types
を使う場合はimportの拡張子を .ts
にしないとダメそうです。テストのadd.test.ts内のみimportの拡張子を .ts
で書くように気を付ければ問題はないかと思ったのですが、現実には add()
のようにテスト対象が簡単な実装であることの方が少ないはずで、普通はadd.tsの中でさらに他のモジュールをこのようにimportしているでしょう。
// add.ts
import { ONE } from "./const.ts"; //
export function add(a: number, b: number): number {
// 例としてはだいぶ無理やりですが a + b + 1 を返します
return a + b + ONE;
}
この場合、const.tsをimportするときにも拡張子を .ts
にしないと node --test --experimental-strip-types
でテスト実行する場合には先ほどと同様にimportできないエラーになります。しかし、テストではなく普通に tsc
で.jsにコンパイルして使う場合には import { ONE } from "./const.js"
にしないといけないはずです。何ともかみ合わせが悪い。
この問題はもはやスナップショットテストと関係ない話なので、もっと以前に --experimental-strip-type
が追加されたとき誰かが解説してそうだと思ったらやはり先人の方たちが既に書かれていました。
自分はjavascript/TypeScriptのimport問題に詳しくない(というか闇が深すぎるのでいい感じになるまで待ちたい)ので、これらの記事を読んだ感想としては test:node
意外とめんどくさそう・・・でした。
自分みたいによっぽど小規模なOSSでimport問題に特にハマることは少ないとか、気合で解決する自信がある人は node:test
で頑張ってみてもいいと思いますが、少なくとも現時点では結局JestやVitestに頼った方が面倒ごとは少ないと思います。
まとめ
node:test
の --experimental-test-snapshots
はJestなどと使い方が若干異なるものの、普通にスナップショットテストとして利用できました。一方でTypeScriptのプロジェクトでは node:test
を利用する場合は --experimental-strip-types
との組み合わせの問題が新たに発生するので、よほどdevDependenciesを減らしたい場合以外はJestやVitestを使った方が楽だと思います。
Discussion