node:test でテストを書く
Node.js 18 から #Node.js 自体にテストランナーが含まれるようになった 最近 #Jest の ESM 対応が渋めなので Vitest に移る人が増えつつある 一方 Vite を使ってないプロジェクトに Vitest を入れたいかというと…みたいな気持ちもある
適当な node.js プロジェクトでテストを書く時 node:test で済めばいいなーという気持ち
やってみて気になったポイント
node --test は当然 .ts を実行できない
ts-node にはまだ --test オプションがない
厳しい。はやく ts-node に入ってくれ
あと当然 node 環境なので ブラウザ API が絡むテストを書くときは注意
逐一 /// <reference lib="dom" /> を書かないといけない
出力の見た目が寂しい
色とか何とかならんのだろうか
https://scrapbox.io/files/63f0e0367cebfe001b9c8854.png
describe it test などがあるが、どれを使うか
Jest のように BDD スタイルにするのかどうか
expect() が存在しないので、describe を使ったところで BDD でかける感じがしない
ネイティブに存在するのは assert() だけ
expect のためだけにライブラリを入れるのは本末転倒に感じる
基本平らに test(...) を置いて assert() する方向で考える
test だと引数が渡ってくるが、it や describe にはこない
code:typescript
test("when user exists", async (t) => {
t.afterEach(() => { ... })
await t.test('works', async () => {
assert.equals(..., ...)
})
})
なにこれ
この t は TestContext と言うらしい
t.beforeEach や t.test などがメソッドとして生えている
テストがネストするときに、子のテストが終わったかどうかを await で検知したりする
この場合、await t.test が終わると t.afterEach が実行される(はず)
これ t.test の前の await 書き忘れそうなので、ESLint で防ぎたい
むしろ describe + it のときはこういう非同期のテストの終了をどう判定するんだろう(?)
TestContext ないのにできる方法あるんだろうか
……みたいなことを考えていた結果 describe + it 方式で書く気持ちを完全に失ってしまった
実はできるという人は教えてほしいです
モックをどうするか
Node.js のモックはつらいがち
が、言うて fetch() とかが気楽にモックできた方がうれしい
どこまでできるかやってみる
TestContext をユーティリティに引き回すのが良さそう
やってみたらこんな書き方ができるようになった
code:typescript
test("when user exists", async (t) => {
const stub = stubFetch(t);
await t.test("returns url", async () => {
// 中でふつうに fetch() してるメソッド
const user = await api.getUserById(1);
assert.equal(user.name, 'John');
});
});
実装はこんな感じ
t を引き回すことにより、beforeEach と afterEach を勝手に実行する君として振る舞えるというのがポイント
#Rspec の include_context みたいな使い方 これができる一点で自分はもう test(...) 全振りに傾くようになった。
「モック用のルーター」を内部に持つ設計にしてみた
API のレスポンスは一旦 JSON しか考慮してないが、Map<string, Response> なので他のレスポンスにも拡張しやすいはず
code:typescript
/// <reference lib="dom" />
import { test } from "node:test";
type TestFn = typeof test;
type TestContext = TestFn extends (fn: (t: infer T) => void) => void
? T
: never;
/**
* @license Public Domain
*/
export function stubFetch(t: TestContext) {
const router = new Map<string, Response>();
const original = globalThis.fetch;
const fetch: typeof original = async (input, requestInit) => {
const request = new Request(input, requestInit);
const response = router.get(request.url);
if (!response) {
console.log(
Could not find mock for ${request.url}. Using native fetch()...
);
return original(input, requestInit);
}
return response;
};
t.beforeEach(() => {
globalThis.fetch = fetch;
});
t.afterEach(() => {
globalThis.fetch = original;
});
function add<T>(url: URL | string, json: T, responseInit?: ResponseInit) {
router.set(
new URL(url).toString(),
new Response(JSON.stringify(json), responseInit)
);
}
return {
add,
fetch,
};
}