Node.js API テスト
API テストでは、テストプロセス内で作ったアプリに HTTP リクエストを送ります。アプリの import 時に DB プールが作られることもあります。その前にプロセスへフォークを割り当て、ケース間では同じフォークをリセットします。リセット後も URL と既存の接続は使えます。
shadowPg でアプリ設定を変えない API テスト
Section titled “shadowPg でアプリ設定を変えない API テスト”同じプロセスの Express + Supertest や Fastify inject では、テストコールバック内のリクエストから pg まで実行コンテキストが伝わります。グローバルセットアップで snapshot を準備したら、アプリのデータソースを変えずにテストできます。アプリの Pool はテスト前に作って構いません。
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 で同じ設定を使えます。
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 を変更してください。
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);});import { afterAll, beforeEach, expect, test } from 'vitest';import { currentFork } from '@pgmem/core';import { buildApp } from '../src/app';
const app = buildApp();beforeEach(() => currentFork().reset());afterAll(() => app.close());
test('HTTP 経由で注文を作成する', async () => { const created = await app.inject({ method: 'POST', url: '/orders', payload: { sku: 'book' } }); expect(created.statusCode).toBe(201); const list = await app.inject({ method: 'GET', url: '/orders' }); expect(list.json()).toHaveLength(1);});スイートごとに別のシードを使う
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 テスト を参照してください。
安全な並列実行
Section titled “安全な並列実行”ファイル単位の並列化にはランナーの別ワーカーを使い、ワーカーごとにアプリと pgmem フォークを用意します。1 つの Express・Fastify アプリと Pool を共有するケースは、shadowPg を付けても逐次実行してください。非同期コンテキストは新規 pg 接続を振り分けますが、取り出し中の接続は元のフォークに属したままです。上の共有アプリと reset の例では test.concurrent は使えません。ケース単位で並列化するなら、ケースごとに別のアプリ・Pool・フォークを作るか、別ワーカーのファイルに分けます。reset や終了の前に、リクエスト処理、バックグラウンドジョブ、トランザクションを完了させてください。