コンテンツにスキップ

Node.js API テスト

API テストでは、テストプロセス内で作ったアプリに HTTP リクエストを送ります。アプリの import 時に DB プールが作られることもあります。その前にプロセスへフォークを割り当て、ケース間では同じフォークをリセットします。リセット後も URL と既存の接続は使えます。

shadowPg でアプリ設定を変えない API テスト

Section titled “shadowPg でアプリ設定を変えない API テスト”

同じプロセスの Express + Supertest や Fastify inject では、テストコールバック内のリクエストから pg まで実行コンテキストが伝わります。グローバルセットアップで snapshot を準備したら、アプリのデータソースを変えずにテストできます。アプリの Pool はテスト前に作って構いません。

test/api-shadow.test.ts
import request from 'supertest';
import { afterAll, expect, test } from 'vitest';
import { shadowPg } from '@pgmem/core';
import { app } from '../src/app';
import { pool } from '../src/db';
afterAll(() => pool.end());
shadowPg(test, '注文を作成する', async () => {
const response = await request(app).post('/orders').send({ sku: 'book' });
expect(response.status).toBe(201);
});
shadowPg(test, '次のケースは準備済みデータから始まる', async () => {
const response = await request(app).get('/orders');
expect(response.body).toHaveLength(0);
});

Fastify では request(app) を app.inject(...) に置き換えます。ケースを逐次実行し、終了時にアプリの Pool を閉じてください。別プロセスの HTTP サーバーにはこのテストプロセスの実行コンテキストは届きません。

DB を準備してフォークを登録する

Section titled “DB を準備してフォークを登録する”

ユニットテストのグローバルセットアップでマイグレーションとシードを一度だけ実行します。Vitest の setupFiles では @pgmem/core/register を先頭に置き、アプリの import より先に DATABASE_URL を設定します。Express と Fastify で同じ設定を使えます。

vitest.config.ts
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globalSetup: ['./test/global-setup.ts'],
setupFiles: ['@pgmem/core/register'],
},
});

アプリを維持し、ケースごとにデータを戻す

Section titled “アプリを維持し、ケースごとにデータを戻す”

テストファイルごとにアプリを一度だけ作ります。beforeEach は失敗したケースの次も含め、毎回準備済みスナップショットへ戻します。afterAll でアプリと DB プールを閉じます。以下は src/app がアプリを、src/db がプールを export する想定です。実際の構成に合わせて import を変更してください。

test/api.test.ts
import request from 'supertest';
import { afterAll, beforeEach, expect, test } from 'vitest';
import { currentFork } from '@pgmem/core';
import { app } from '../src/app';
import { pool } from '../src/db';
beforeEach(() => currentFork().reset());
afterAll(() => pool.end());
test('HTTP 経由で注文を作成する', async () => {
const created = await request(app).post('/orders').send({ sku: 'book' });
expect(created.status).toBe(201);
const list = await request(app).get('/orders');
expect(list.body).toHaveLength(1);
});
test('次のケースは準備済みシードから始まる', async () => {
const list = await request(app).get('/orders');
expect(list.body).toHaveLength(0);
});

スイートごとに別のシードを使う

Section titled “スイートごとに別のシードを使う”

ファイルのフォークに一度シードを入れてスナップショットを作り、各ケースの前にそこへ戻します。スイートが終わるまでスナップショットを閉じないでください。

import { beforeAll, beforeEach, afterAll } from 'vitest';
import { currentFork, type PgmemSnapshot } from '@pgmem/core';
let seeded: PgmemSnapshot;
beforeAll(async () => {
await seedOrders(process.env.DATABASE_URL!);
seeded = await currentFork().snapshot();
});
beforeEach(() => currentFork().reset({ snapshot: seeded }));
afterAll(() => seeded.close());

ブラウザーと別プロセスのアプリを使う場合は E2E テスト を参照してください。

ファイル単位の並列化にはランナーの別ワーカーを使い、ワーカーごとにアプリと pgmem フォークを用意します。1 つの Express・Fastify アプリと Pool を共有するケースは、shadowPg を付けても逐次実行してください。非同期コンテキストは新規 pg 接続を振り分けますが、取り出し中の接続は元のフォークに属したままです。上の共有アプリと reset の例では test.concurrent は使えません。ケース単位で並列化するなら、ケースごとに別のアプリ・Pool・フォークを作るか、別ワーカーのファイルに分けます。reset や終了の前に、リクエスト処理、バックグラウンドジョブ、トランザクションを完了させてください。