Kodokon kodokon.com

APIをテストする:node:test、fetch、テスト用データベース

node:testとfetchを使い、一時的なポート上でルートをエンドツーエンドでテストし、各テストが自前のインメモリSQLiteデータベースを持つようにしましょう。

9 分 · 3 問

このレッスンを Kodokon で開く

Node 20には必要なものすべてが同梱されています。ランナーとしてのnode:test、アサーションのためのnode:assert/strict、HTTPクライアントとしてのグローバルなfetchです。テストの依存はゼロ。そしてここで、最初のレッスンのアーキテクチャが報われます。createApp({ db })は注入されたデータベースを受け取るので、各テストは数ミリ秒で完全な、隔離されたAPIをマウントできます。

BASH
node --test
node --test --watch
ランナーはプロジェクトの*.test.jsファイルを自分で見つけ出します。

ルートをエンドツーエンドでテストするのに、Expressをモックする必要はありません。本物のサーバーをポート0でマウントし、システムが空いているポートを割り当てたら、それをfetchで叩きます。本物のクライアントが目にするものそのもの、ルーティング、ミドルウェア、シリアライズ、ステータスコードを試すのです。これは依存なしの、supertestの精神です。

JAVASCRIPT
import { once } from 'node:events';

export async function startTestServer(app) {
  const server = app.listen(0);
  await once(server, 'listening');
  const { port } = server.address();
  const url = `http://127.0.0.1:${port}`;
  const close = () =>
    new Promise((resolve) => {
      server.close(resolve);
    });
  return { url, close };
}
helpers.js:OSが割り当てたポート上の本物のサーバー。

テスト用データベースは、メモリ内のSQLite(:memory:)です。各テストはcreateDbを通じて、自前の真っさらで空のデータベースを構築し、それがスキーマを適用します。そして終わりぎわにそれを捨て去ります。テスト間で共有されるデータもなく、クリーンアップのスクリプトもありません。t.afterは、アサーションが失敗したときでもサーバーが閉じられることを保証します。

JAVASCRIPT
import test from 'node:test';
import assert from 'node:assert/strict';
import { createDb } from '../src/db/connection.js';
import { createApp } from '../src/app.js';
import { startTestServer } from './helpers.js';

test('POST /api/users creates a user', async (t) => {
  const db = createDb(':memory:');
  const app = createApp({ db });
  const server = await startTestServer(app);
  t.after(() => server.close());

  const res = await fetch(`${server.url}/api/users`, {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({
      email: 'ada@example.com',
      name: 'Ada',
    }),
  });

  assert.equal(res.status, 201);
  const body = await res.json();
  assert.equal(body.email, 'ada@example.com');
});
エンドツーエンドのテスト。本物のHTTPスタック、使い捨てのデータベース。

理解度チェック

このレッスンの要点をしっかり覚えているか確認しましょう。

  1. なぜテストサーバーをapp.listen(0)で起動するのですか?
    • ポート0はネットワークを無効にし、テストを速くする
    • システムが空いているポートを割り当てる。並列のテストが同じポートを取り合うことは決してない
    • 本番以外で許される唯一のポートだから
    • ポート0はExpressをテストモードに強制する
  2. 各テストで作られる:memory:データベースの決定的な利点は何ですか?
    • 比較のために実行間でデータを永続させる
    • 各テストが空の、隔離された状態から始まり、クリーンアップのスクリプトもテスト間の干渉もない
    • 挿入を簡単にするためにSQLの制約を無効にする
  3. なぜサーバーをテスト本体の末尾ではなく、t.afterで閉じるのですか?
    • t.afterはアサーションが失敗しても実行され、プロセスの終了を妨げる孤児のサーバーを避けられる
    • t.afterはテストの前に実行され、サーバーを準備する
    • 純粋に様式の問題で、振る舞いは同じ