API テスト運用ガイド API Testing — Operations Guide

parky/api のテストは 単体 (unit) / E2E / 契約 (contract) の 3 層で構成します。 すべて Vitest ベース。単体は外部依存ゼロで数百ミリ秒、E2E は実 Supabase dev を叩き、 契約は OpenAPI スナップショットと生成型の drift を CI で検出します。

Tests for parky/api are layered across unit / E2E / contract. Everything runs on Vitest. Unit tests have no external deps and finish in hundreds of milliseconds; E2E hits the real dev Supabase; contract checks detect drift of the OpenAPI snapshot and generated types in CI.

3 層の役割分担Layering

TL;DRTL;DR

# 1. 初回だけ(E2E で必要。単体だけなら不要)
cp parky/api/tests/.env.test.example parky/api/tests/.env.test
#    → SUPABASE_URL / _SERVICE_ROLE_KEY / _ANON_KEY を埋める
#      (値は parky/api/scripts/secrets.env と同じで OK)

# 2. 別ターミナルで BFF を起動(E2E で必要。単体だけなら不要)
cd parky/api && npm run dev     # wrangler dev が :8787

# 3. テストを走らせる
cd parky/api
npm run test:unit       # 単体のみ (外部依存なし、ms オーダー)
npm run test:unit:watch # 単体を watch モード
npm run test:e2e        # E2E のみ (BFF + Supabase 必要)
npm run test            # 単体 + E2E まとめて (= npm run test:all)
npm run test:changed    # git 差分から影響するテストだけ自動選別して実行
npm run test:cleanup    # テスト痕跡を全消去(残骸があった時の手動リセット)

設計方針Design principles

なぜ実 Supabase を叩くのかWhy hit the real Supabase

Supabase の JWT は JWKS(非対称鍵)検証なのでローカルの HS256 署名では通りません。 そのためテストユーザーを実際に auth.users に作り、password grant で JWT を取得します。 Hyperdrive + postgres.js 経由の挙動、PL/pgSQL RPC、RLS バイパスなど、BFF の 本番と同じコード経路を踏めるので、モックより信頼度が高くなります。

Supabase JWTs are verified via JWKS (asymmetric keys), so local HS256 signatures do not pass. Tests create real users in auth.users and obtain JWTs via the password grant. This exercises the BFF's real code path — Hyperdrive + postgres.js, PL/pgSQL RPCs, RLS bypass — which is far more trustworthy than mocks.

なぜ wrangler dev ローカルかWhy local wrangler dev

データ汚染防止(冪等性ルール)Data-pollution prevention (idempotency rules)

テストが作るレコードはすべて本番と識別可能なプレフィックスを持たせます。

Every record the tests create carries a prefix that is distinguishable from production.

対象Target ルールRule
auth.users.email __test__e2e_<scope>+<uid>@parky-e2e.test .test TLD と __test__ プレフィックスの二重ガード) (double-guarded by the .test TLD and a __test__ prefix)
app_users.email / display_name __TEST__ プレフィックス + scope 名prefix + scope name
parking_lots.name __TEST__lot_<uuid8>
tags.name __TEST__tag_<uuid6>
user_search_presets.name __TEST__preset_<...>

掃除は三段構え。すべて冪等で、何度実行しても結果は同じになります。

Cleanup runs at three layers. Every layer is idempotent — repeating a run has no extra side-effects.

  1. afterAll()cleanupAll() を呼び、そのテストが作成した ID を実削除。
  2. Each afterAll() calls cleanupAll() to delete the IDs the test created.
  3. Vitest の globalSetup 終了時に cleanupByPrefix() でプレフィックス一致を一掃(落ちたテストの救済)。
  4. Vitest's globalSetup tear-down runs cleanupByPrefix() to sweep anything matching the prefix (safety net for tests that crashed).
  5. npm run test:cleanup で手動リセット(CI / 緊急時)。
  6. npm run test:cleanup runs the cleanup on-demand (CI or manual reset).

POST 系は本番衝突しないデータで行うのが鉄則。 例として email は .test TLD、name は __TEST__ プレフィックス必須です。 新しい fixture を追加する時もこのルールを守ってください。

POST tests must use data that cannot collide with production. Emails use the .test TLD; names carry the __TEST__ prefix. Keep this invariant when adding new fixtures.

新しいテストを追加する手順How to add a new test

  1. tests/routes/<name>.test.ts を作る。ファイル名規約で src/routes/<name>.ts が自動的にカバー対象になります。
  2. Create tests/routes/<name>.test.ts. The naming convention automatically maps it to src/routes/<name>.ts.
  3. 例外的に別ファイルも覆いたい場合は冒頭に // @covers src/routes/xxx.ts, src/lib/yyy.ts を書く。
  4. To cover extra files, add // @covers src/routes/xxx.ts, src/lib/yyy.ts at the top.
  5. admin は tests/routes/admin-<name>.test.tssrc/routes/admin/<name>.ts、 owner は tests/routes/owner-<name>.test.tssrc/routes/owner/<name>.ts
  6. Admin routes use tests/routes/admin-<name>.test.tssrc/routes/admin/<name>.ts, and owner routes use tests/routes/owner-<name>.test.tssrc/routes/owner/<name>.ts.
  7. fixture は createTestUser() / createTestParkingLot() / promoteToAdmin() から組み立てる。
  8. Build fixtures via createTestUser(), createTestParkingLot(), promoteToAdmin().
  9. 最後に必ず afterAll(() => cleanupAll()) を置く。
  10. Always end with afterAll(() => cleanupAll()).
  11. npm run test:changed で確認する(test:map は自動で再生成されます)。
  12. Verify with npm run test:changed (the route map regenerates automatically).

Impact 解析(test:changed の仕組み)Impact analysis (how test:changed works)

何が起きるかWhat happens

  1. tests/impact/build-route-map.ts を実行して route-map.json を最新化。
  2. Run tests/impact/build-route-map.ts to refresh route-map.json.
  3. git diff --name-only で変更ファイルを取得(優先順: origin/main...HEADmain...HEADHEAD)。
  4. Collect changed files with git diff --name-only (priority: origin/main...HEADmain...HEADHEAD).
  5. 変更ファイル → route-map から「走らせるテスト集合」を展開。
  6. Expand changed files → the set of tests to run via the route map.
  7. 以下の場合は 全テスト走行(force-all)
    • tests/setup/** / tests/impact/** / tests/vitest.config.ts
    • package.json / package-lock.json / tsconfig.json / wrangler.toml
    • 変更が api/ 配下にあるが route-map に載っていない場合(新規ファイル等)
  8. Force-all triggers (run every test):
    • tests/setup/** / tests/impact/** / tests/vitest.config.ts
    • package.json / package-lock.json / tsconfig.json / wrangler.toml
    • Changes inside api/ that the route map doesn't yet know about (e.g. brand-new files).

route-map の中身What the route map contains

tests/impact/route-map.json は「src/* ファイル → 走らせる test ファイル群」の逆引き辞書です。

tests/impact/route-map.json is a reverse index from each src/* file to the tests that should run.

{
  "src/routes/me-search-presets.ts": ["tests/routes/me-search-presets.test.ts"],
  "src/lib/db.ts": ["tests/routes/admin-tags.test.ts", "tests/routes/me-search-presets.test.ts", ...],
  "__always__": ["tests/routes/..."]
}

粒度は 2 種類:

The map uses two granularities:

route-map.json はビルド成果物なので .gitignore 済み。 test:map コマンドまたは test:changed / test:all 実行時に毎回再生成されます。手で編集しないでください。

route-map.json is a build artifact (gitignored). It is regenerated on every test:map, test:changed, or test:all run. Never edit it by hand.

@covers ディレクティブThe @covers directive

テストファイル冒頭のコメントで上書き・追加できます。カンマ区切りまたは複数行で列挙可。 ディレクティブがあればファイル名規約は上書きされます。

Declare extra or overriding coverage in a top-of-file comment. Comma-separated or multi-line. When present, the directive overrides the filename convention.

// tests/routes/me-vehicles.test.ts
// @covers src/routes/vehicles.ts

import { describe, expect, it } from "vitest";
// …

よくあるトラブルTroubleshooting

BFF が /healthz に応答しませんでした

別ターミナルで npm run dev を起動し忘れています。wrangler dev が 起動完了メッセージを出してから npm run test を打ちます。

npm run dev isn't running in another terminal. Wait until wrangler dev prints its ready banner, then start the test run.

auth.admin.createUser failed

SUPABASE_SERVICE_ROLE_KEY が間違っているか、本番キーを入れてしまっています。 tests/.env.test を確認してください。

Wrong SUPABASE_SERVICE_ROLE_KEY — often a production key instead of dev. Recheck tests/.env.test.

前回のテスト痕跡が残ったStale data from a previous run

npm run test:cleanup を 1 回だけ叩けばプレフィックス一致のデータをすべて消去します。

Run npm run test:cleanup once — it removes everything matching the test prefix.

Hyperdrive の接続エラー

Hyperdrive connection error

wrangler dev は Hyperdrive binding を remote に繋ぎに行きます。 オフライン時はテストを回せません(現時点ではオフライン対応スコープ外)。

wrangler dev dials into the remote Hyperdrive binding. Tests can't run while offline (offline mode is out of scope).

ディレクトリ構成Layout

parky/api/tests/ ├── README.md ← このドキュメントへのリンクだけを置く ├── .env.test.example ← 環境変数テンプレート(Git 管理) ├── .env.test ← 実値(.gitignore) ├── tsconfig.json ← workers-types + DOM lib ├── vitest.config.ts ← E2E 用(globalSetup / timeout) ├── setup/ │ ├── env.ts ← .env.test 読み込み + プレフィックス定数 │ ├── supabase.ts ← admin / anon クライアント │ ├── fixtures.ts ← createTestUser / createTestParkingLot / cleanup* │ ├── http.ts ← fetch ラッパー + waitForHealthz │ ├── global-setup.ts ← Vitest globalSetup │ └── cleanup-cli.ts ← npm run test:cleanup エントリ ├── unit/ ← 純関数の単体テスト(BFF 不要) │ ├── vitest.config.ts ← globalSetup 無し・並列実行 │ ├── logger.test.ts ← 構造化ログ / LOG_LEVEL / err.cause / 循環参照 │ ├── errors.test.ts ← ApiError / ERROR_CATALOG / statusToCode │ └── db-errors.test.ts ← translatePgError 全 SQLSTATE マップ ├── routes/ ← E2E。1 ファイル = 1 ルートモジュール原則 │ ├── me-search-presets.test.ts │ ├── me-vehicles.test.ts │ ├── parking-lots.test.ts │ ├── reviews.test.ts │ └── admin-tags.test.ts └── impact/ ├── build-route-map.ts ← @covers + ファイル名規約 + 推移 import でマップ生成 ├── run.ts ← git diff → unit / e2e を振り分けて vitest 起動 └── route-map.json ← 生成物(.gitignore)

単体テストの追加: tests/unit/<name>.test.ts を作り、 ファイル冒頭に // @covers src/lib/xxx.ts を書く(ファイル名規約のフォールバックは無い)。 カバー対象が複数なら @covers をカンマ区切りで並べる。

Adding unit tests: create tests/unit/<name>.test.ts and declare // @covers src/lib/xxx.ts at the top (no filename-convention fallback). Multiple targets can be listed comma-separated.

関連リンクRelated