name: test-writing-rules description: "テストを書く前に発動。仕様から観点とテストケースを導出するための 3 軸チェックリストと技法カタログ。実装の翻訳ではなく仕様の検証になるテストを設計する。"
テスト作成ルール
テストを書く前に発動し、対象機能に対して仕様ベースで最適なケース集合を設計するための指針。
大原則
- テストは 「退行検知」と「仕様の言語化」 のために書く
- カバレッジ閾値は退行検知の最低ラインであり、達成目標ではない。
vitest.config.tsの閾値ギリギリを稼ぐためのテストを書かない - テストの本質は 仕様の検証。実装の振る舞いを写し取るのが目的ではない
- 実装を読んでアサーションに翻訳した「tautological test」は禁止。実装にバグがあっても通るため
- 順序は 仕様(型 / 関数名 / OpenAPI スキーマ / ドキュメント / 呼び出し側)から先に観点を導出 → 後から実装を読んで漏れを補強
- 各テストは F.I.R.S.T.(Fast / Independent / Repeatable / Self-validating / Timely)を満たす。本 Skill の指針はこの原則の具体化
ワークフロー
テストを書く前に毎回辿る:
- 仕様の読解 — Hono ルートなら
createRouteの入出力 Zod スキーマ、Mastra ツールならinputSchema/description、Repository なら呼び出し元のユースケース、フックならProps型と画面側の使われ方を把握 - 観点の列挙 — 下記の 3 軸を当ててケース候補を出す(詳細:
references/perspectives.md) - 技法の選択 — 関連する技法を選んでケースに展開(詳細:
references/techniques.md) - 実装で漏れ補強 — 列挙したケースが全分岐を踏めているか確認、足りなければ仕様視点で追加
- 記述 — 1 ケース 1 テストに落とす。テスト名は日本語、技法は describe や
itの文章で表現する(// 技法: 〇〇のラベルコメントは書かない)
観点を導出する 3 軸
書き始める前に各軸で漏れを潰す。詳細は references/perspectives.md。
軸 1: 入出力の分類
- 正常系 (Happy Path): 仕様通りの入力で仕様通りの出力
- 準正常系 (Edge Case): 仕様の許容範囲内だが境界・特殊・冗長な入力(空白、重複、順不同、Unicode、長文、
null/undefined/NaN) - 異常系 (Sad Path / Negative): 明確なエラーが返るべき入力。
HTTPExceptionの status・コード・message までアサート(error-handlerで{ error: { code, message } }に整形される) - 例外系 (Error Path): throw が起きる経路。
expect(...).toThrow(SpecificError)で型まで確認
軸 2: 機能横断の観点
- 冪等性 / 並行性 / 順序依存 / タイムアウト・リトライ / 部分失敗 / リソースリーク / 入力サニタイズ / i18n・エンコーディング
軸 3: ドメイン / 基盤の観点
機能が触る基盤・ドメインに応じて当てる。実装の癖(特定 middleware の優先順位、特定 helper の init 忘れ等)はテスト観点ではなく実装詳細なので含めない。詳細は references/perspectives.md。
- LLM / Agent: tool call の異常、structured output のスキーマ違反、非決定性の固定、PII 混入
- RDB / KV / オブジェクトストレージ / ベクトル検索: トランザクション境界、UNIQUE 制約違反、eventual consistency、次元数整合
- 外部 API / Webhook: 署名検証、4xx/5xx の retry 可否分岐、レート制限、部分失敗
- 状態遷移ドメイン: 不正遷移、戻り遷移、遷移時の副作用
- 認証 / 認可: 未認証 / ロール不足 / 別オーナー / 期限切れ / 不正署名
- テストランナー / モック / クエリキャッシュ: 未モックリクエスト検知、handler リセット境界、fake timers の時刻基準、独立 QueryClient
- ブラウザ / UI: ストリーミングの中間状態、画面離脱後の async、a11y
ミューテーション思考
書いたケースが本当に効くか、頭の中で実装に小さな変更を入れて落ちるか確認:
>を>=に変えても落ちないテストは弱い&&を||に変えても落ちないなら不足return xをreturn undefinedにしても落ちないなら戻り値検証が甘い
構造
- AAA(Arrange / Act / Assert)の流れは大半の単体テストで自然に出る。明示コメント・空行は 任意。冗長なら入れない
- GWT(Given / When / Then)は受け入れテストや複雑な業務シナリオに限って使う
- テスト名は「対象 / 条件 / 期待振る舞い」の 3 要素
例:
POST /admin/broadcast: scheduled_at が過去日時のとき 400 を返す describeのネストは 2 階層まで。深くなるならファイルを分ける- 1 テスト 1 振る舞い。1 つの仕様(status・error.code・DB 副作用など)を成立させる複数 assert は許容するが、無関係な仕様を 1 テストに詰めない
テストダブル
- テストダブルは I/O 境界 に限定(HTTP / DB / オブジェクトストレージ / 時刻 / 乱数 / LLM)。ドメインロジック自体や自分のコードはモックしない
- 時刻依存は
vi.useFakeTimers()または DI された clock。new Date()直接呼び出しはラップ提案を一緒に出す - 戻り値は本番スキーマ(Zod / Drizzle の型)に準拠させる
- 「モック」と一括りにせず種類を意識する(Martin Fowler の分類 より):
- Stub: 決まった値を返すだけ。状態確認しない
- Spy: 呼ばれた事実・引数・回数を記録する(
vi.fn()の典型用途) - Mock: 期待される呼び出しを事前定義し、満たさないと失敗する(厳格な振る舞い検証)
- Fake: 軽量だが動作する代替実装(
createTestDb()の in-memory SQLite /mswのリクエストハンドラ /vi.useFakeTimers()の仮想時計が該当)
- Fake で代替できるなら Stub/Spy より Fake を優先する。本番に近い挙動で検証できるため
テスト道具の使い方(指針)
「どこに何があるか」ではなく「どう使うか」の指針。詳細パスは CLAUDE.md / server/CLAUDE.md / web/CLAUDE.md を参照。
- モック境界: 外部 I/O(HTTP / DB / オブジェクトストレージ / 時刻 / 乱数 / LLM)に限ってモック。ドメインロジック自体はモックしない
- Hono ルートを叩く時: 本番と同じ middleware スタック(principal 解決 + errorHandler)込みのアプリで叩く。生 Hono を直接組み立てて middleware を省いたテストは仕様検証にならない
- DB を伴うテスト: in-memory SQLite ヘルパを使い、
beforeEachで毎回作り直して状態を隔離。テスト間で row を引き継がない - Mastra tool の execute: テスト用 context builder で
RuntimeContextの中身を 必ず明示(特にenv)。default を信用しない - msw: 各テスト先頭で
server.use(...)を必ず書く。setup.tsがonUnhandledRequest: "error"のため未モック呼び出しは即落ちる。afterEachのresetHandlersでテスト間の漏れを防ぐ - QueryClient: テスト毎に独立クライアントを作るヘルパで包む。retry: false / gcTime: 0 / staleTime: 0 が前提で、retry 挙動を確認したいケースは別途モック
- 時刻 / 日付: web は
TZ=Asia/Tokyo固定で実行。時刻依存ロジックはvi.useFakeTimers()か DI された clock を使う。new Date()直接呼び出しは避ける - co-located 配置: テストは対象ファイルの隣に置く(
foo.tsの隣にfoo.test.ts) - 書かない領域:
vitest.config.tsで除外されているファイル(Sentry init / Astro shell / 外部 SDK 連携の深い tsx 等)にカバレッジ目当てのテストを足さない
ケース設計の出力フォーマット
テストを書く前に、必ず下記の形でケース集合を一度言語化する。これでテスト過多と漏れを同時に防ぐ。
対象仕様: <関数 / ルート / フックのシグネチャと責務>
採用技法: <同値分割 + 境界値 / 状態遷移 / デシジョンテーブル など>
書くケース:
- <観点>: <入力 → 期待挙動>
- ...
書かないケース(理由付き):
- <観点>: <なぜ書かないか。例: Zod が境界で弾く / Hono router が同じ branch を踏む / 純粋関数で例外系が無い>
禁止事項
- tautological test(実装をそのまま assert に翻訳しただけ)
- 実装の private 状態を直接覗いて assert
- 巨大スナップショットの濫用(差分が読めないもの)
expect(true).toBe(true)系の placeholderskip/onlyのコミット- 真の LLM / 外部 API への到達(
onUnhandledRequest: "error"で msw が落とすが、念のため)
参照
- 観点 3 軸の詳細:
references/perspectives.md - 技法カタログ(同値分割 / 境界値 / デシジョンテーブル / 状態遷移 / ペアワイズ / エラー推測)と適用例:
references/techniques.md - このリポジトリでのテスト最小コード例(Hono ルート / Mastra tool / DB / web フック・コンポーネント / fakeTimers):
references/examples.md