test-design について
test-designは、単体テストや統合テストを作成・レビューする開発者向けのエージェントスキルです。観測可能な振る舞いの契約を軸にケースを設計し、ミューテーションチェックで回帰を検出できるか確認します。
主な機能
- 01振る舞いの契約に基づくテスト設計
- 02同値クラスと境界によるケース選定
- 03結果・状態・副作用のアサート
- 04モックを制限しFakeを優先
- 05ミューテーションによる有効性確認
活用シーン
- 01機能の単体・統合テストを計画
- 02実装詳細だけを確認するテストのレビュー
- 03境界値や不正入力のケース選定
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ gusen1453/ creed --skill test-designインストール後、Claude Code で/test-designを入力して読み込まれたか確認します。
詳細
テスト設計
出力言語: 回答と成果物は、ユーザー自身の言語で記述してください(リポジトリの履歴ではなく、ユーザーのメッセージに合わせてください)。
概要
**「テストにどの入力を与え、何をアサートし、何をモックすべきか」**に答えます。レッド・グリーン・リファクターのサイクル(これは TDD です)は扱いません。
基本原則:テストは、ユーザーや呼び出し元が観察できる振る舞いの契約を守るものであり、実装の詳細やカバレッジの数値を守るものではありません。 優れたテストは契約を反証可能な形で示す仕様です。悪いテストは、コードの内容を別の構文で言い直したものです。
必須の併用スキル: テストを先に書くサイクルについては tdd を参照してください。モックが増えすぎた場合のモジュール境界については solid を参照してください。このスキルが扱うのは、テストケースとアサーションの設計品質のみです。
使用する場面
- コードの単体テスト/統合テストを追加または変更するとき
- 他の人のテストが効果的か、形式だけのものかをレビューするとき
- 「このコードはテストする価値があるか?」「単体テストと統合テストのどちらにすべきか?」を判断するとき
- カバレッジへのプレッシャー、締め切り直前、またはサンクコストを理由に、形だけのテストを書こうとしているとき
使用しない場面: テストを実行するだけ、または失敗内容を読むだけの場合 → test-runner。テストファーストのサイクルに従うだけの場合 → tdd。
鉄則(譲れないルール)
Break one line of production code and the test protecting it MUST turn red.
If it can't, the test is formalism — delete it and rewrite.
テストを書いたら、そのテストが守るロジックを頭の中で(または実際に)壊し、テストが失敗することを確認してください。 ルールの文面に違反することは、その精神に違反することです。「カバレッジは問題ない」「精神には従っている」では不十分です。
ワークフロー(呼び出された場合)
「test-design を使って……」と宣言し、各ステップにつき 1 つの TODO を作成します。
- テストする価値があるか判断する(§1):独自ロジックの複雑さ → 単体テスト。依存関係の接続 → 統合テスト。価値がない場合は書かない。
- 振る舞いのリストを書く(§2):契約のみを基に、「……の場合、……するべき」と記述します。専用の分岐がないもの(例:「正当なコンテンツを削除しない」)も含めます。各ケースには、読み手に伝わる言葉で目的/条件/パラメーターを記述します(§2 を参照)。
- 代表的なポイントを選ぶ(§3):同値クラス/境界値/決定表/状態遷移/不正な入力からそれぞれ 1 つずつ代表例を選び、同質な重複は避けます。
- アサーションを書く(§4):戻り値/観察可能な状態/副作用の境界のみを、範囲を絞って厳密にアサートします。
expectedは手書きします。 - モックを決める(§5):モックするのは外部の世界のみとし、数は 2~3 個以下にします。Fake/Recording を優先します。
- ミューテーションチェック(鉄則):本番コードを壊し、各テストが失敗することを確認します。
- 合理化の一覧表と危険信号を確認する: いずれかに該当したら、作業を止めて再検討します。
§1 テストする価値があるか(カバレッジではなく ROI)
判断基準:複雑さがコード独自のロジックにあるのか、それとも依存関係間のやり取りにあるのか?
- 独自ロジック(アルゴリズム、純粋計算、ドメインモデル/状態機械、分岐の多いポリシー/バリデーション/縮退処理) → 単体テスト。最も ROI が高いものです。
- 依存関係の接続(CRUD、
A.get() → convert() → B.save()のようなつなぎ込み) → 統合テスト。この種のコードのリスクは、まさにモックが置き換える箇所にあります(依存関係が null を返す、タイムアウトする、例外を投げる、トランザクションがコミットされない)。単体テストでは、「すべてが問題ない場合に実行できる」ことしか証明できず、価値がありません。
つなぎ込みを単体テストしてはいけません。アルゴリズムを統合テストしてはいけません。
§2 振る舞いの一覧からケースを見つける
契約だけを根拠に「〜の場合、〜するはず」と書きます。ソース内の if の数は絶対に数えないでください。 ユーザーが最も気にかける、専用の分岐がない項目も必ずリストに含めてください。
ソースを読みながら書いたケースは、削除して書き直してください。それは契約ではなく、実装を保護するものです。
各ケースは、作者ではない人にも理解できるものでなければなりません。 コードを書く前に、平易なユーザー/プロダクトの言葉で次の3点を述べてください。
- 目標 — このケースで何を証明するのかを、作者ではない人にもわかる一文で示します(仕組みではなく、ユーザーに見える結果)。
- 条件 — ケースで設定する状態を示します(誰が/どのデータを/直前に何が起きたか)。
- パラメーター — 具体的な入力と、期待される観測可能な結果を示します(値と、その値が重要な理由:境界値、極端な値、実際のケースなど)。
目標を理解するためにソースを読み返す必要があるケースは、この基準を満たしていません。例:
Case: re-import with the same file twice
- Goal: a re-run must not double-count rows (the bug users reported as "my totals doubled")
- Conditions: the file was already imported once; no other change
- Parameters: same file; expect the row count unchanged, and an idempotency marker set
§3 代表値を選ぶ5つの手法
- 同値分割:振る舞いのグループごとに代表値を1つ選びます(同じグループ内の2つ目は重複です)。
- 境界値:すべてのしきい値で
boundary-1 / boundary / boundary+1をテストします(>=4では「ちょうど3」と「ちょうど4」を必ずテストします。>と>=の違いを検出する唯一の方法です)。 - デシジョンテーブル:出力が複数の条件に依存する場合は、組み合わせを列挙します。「複数が同時に該当する」ケースを必ず含めます。 条件が多い場合はペアワイズ法を使います。
- 状態遷移:正当な経路+不正な遷移(クローズ後の使用)+冪等性(クローズ×2)+繰り返し。
- 不正/悪意ある入力:空、null、サイズ超過、巨大なJSON、構造の破損(閉じタグの欠落、ネスト)、型の不一致。
§4 アサーション(推奨パターン)
次の3つの観測可能な対象だけをアサートします:①戻り値/スローされるエラー(型+メッセージ+コード) ②観測可能な状態変化 ③副作用の境界(リクエストが送信されたか、送信先、引数の形式)。
- 狭く、厳密に:大きな
toEqual(whole object)を1つ使うより、小さく正確なアサーションを複数使います。 expectedは手書きする:expected = input.replace(sameRegex)は「正規表現が自身と一致する」ことをアサートするため、常に成功します。- 実装の詳細に触れない:「回数/順序」自体が契約である場合(3回ちょうど再試行する、二重請求しないなど)を除き、プライベートな呼び出し回数や内部の分岐は対象にしません。
§5 モックのルール
遅い/不安定/副作用がある/コストが高い外部の依存先だけを置き換えます。テストしにくい設計を回避するためには使いません。
| モックを使う | 絶対にモックしない |
|---|---|
| ネットワーク/HTTP/サードパーティ製SDK/LLM、DB/FS/MQ、時計、乱数生成器 | テスト対象のユニット、純粋関数、値オブジェクト、スキーマ、シンプルなモジュール内の依存先 |
忠実度の順序:フェイク(インメモリ)>記録/契約リプレイ>スパイ>モックフレームワーク。契約の形式だけをアサートします:
// Good: assert the outward contract
expect(fetchMock).toHaveBeenCalledWith(
expect.objectContaining({ method: "POST", url: expect.stringContaining("/nodes") }),
)
// Bad: lock implementation detail, red on any refactor
expect(buildPayload).toHaveBeenCalledTimes(1)
§6 ユーザーの視点
実際の利用者の視点からアサートします。エンドユーザーには結果が見えます。上流のAPI/エージェントにはフィールドとエラーコードが見えます。運用担当者には、ログ/状態から問題を特定できるかどうかが見えます。
テスト名は役割+シナリオ+期待結果とします(case 1/should workは禁止)。公開エントリーポイントから入り、プライベート関数に踏み込まないでください。
§7 形状の選択
ロジックが複雑な場合 → ピラミッド(基盤となるユニットテストを多くする)。ビジネス処理/CRUDの場合 → ダイヤモンド(中間層の統合テストを多くし、ユニットテストはユーティリティとコアアルゴリズムだけにする)。 統合テストでしか検出できない問題があります。トランザクションのコミット、JSONフィールドの欠落、パラメーター検証、実際の依存先の障害などです。
合理化の一覧(形式化を書く前に自分に言い聞かせる言い訳)
| 言い訳 | 現実 |
|---|---|
| 「この分岐をカバーするテストを追加するだけ」 | カバレッジは保護になりません。「ユーザーに何が起きるか?」と問いましょう。リスクがなければ、テストしません。 |
「関数で expected を計算するほうが正確」 | それでは「実装が自分自身と等しい」ことをテストするだけで、常に成功します。expected は手書きのリテラルでなければなりません。 |
| 「分離をきれいにするために、もう少しモックする」 | モックが山積みなら、コードを分割するか統合テストにしましょう。最もバグが起きやすい箇所をフェイクに置き換えたことになります。 |
| 「ついでにこの接着部分/CRUDもテストしておこう」 | そのリスクはすべてモック化した依存先にあります。統合テストを使いましょう。 |
| 「境界を1つテストすれば十分」 | オフバイワンは境界でしか検出できません。 |
| 「いくつかのケースをテストすれば、より網羅的」 | 同じ同値クラスに属する複数ケースは、同質な重複であり、形式化にすぎません。 |
| 「もうすぐリリースだから、呼び出しの検証だけで十分」 | verify(called once) は何も守りません。時間がないなら、偽のテストを書くのではなく、テストを減らしましょう。 |
| 「オブジェクト全体のスナップショットを取るのが一番簡単」 | 無関係なフィールドが変わるだけで脆く失敗し、どの契約に違反したのかも分かりにくくなります。 |
| 「このテストは書きにくいから、モックで回避しよう」 | テストしにくいコードは使いにくいものです。これは設計上のシグナルなので、コードを修正しましょう。 |
| 「プロらしく見える/80%は必須」 | 実装をコピーしたテストが100個あっても、何も守りません。重要なのは数ではなく品質です。 |
危険信号 — すぐに中止してください
- 行/分岐をカバーするためだけにテストを追加している
expectedが手書きではなく、本番ロジックで計算されている- 3つ以上の依存先をモックする「ユニットテスト」
- アサーションがすべて
verify(...)/toHaveBeenCalledで、戻り値や状態を検証していない test1/should work/case 2のようなテスト名- ゲッター/セッター/DTO変換/単純な接着部分をテストしている
- 「コードを壊しても、おそらくこのテストは失敗しない」
1つでも当てはまったら → 中止して、「このテストは、ユーザーに見えるどんな悪い結果を防ぐのか?」に戻ってください。答えられないなら、書かないでください。
例(テクニックを組み合わせたもの)
テスト対象:strip(prompt) はエビデンスのダンプを削除し、短い指示を残します。[n] の引用が4つ以上ある段落は破棄されます。
振る舞いの一覧 → ケース(分岐を追うのではなく):
// role+scenario+expectation; narrow/hard assertions; hand-written expected
test("subagent receiving a prompt with task_result has the dump stripped but instructions kept", () => {
const out = new HeuristicDumpStripper().strip(
"Summarize in three sentences.\n<task_result>…huge dump…</task_result>\nPrefer the latest conclusion.",
)
expect(out).not.toContain("<task_result>") // dump gone
expect(out).toContain("Summarize in three sentences") // leading instruction kept
expect(out).toContain("Prefer the latest conclusion") // trailing instruction kept
})
// Boundary values: threshold >=4, test "exactly 3 kept / exactly 4 dropped"
test("paragraph with exactly 3 citations is kept", () => {
expect(strip("conclusion [1][2][3] still body")).toContain("still body")
})
test("paragraph with exactly 4 citations is dropped", () => {
expect(strip("noise [1][2][3][4] should be removed")).not.toContain("should be removed")
})
// Dirty input + fallback: non-empty when everything is stripped
test("falls back to truncated original (non-empty) when stripping empties it", () => {
expect(strip("<task_result>only dump</task_result>").length).toBeGreaterThan(0)
})
ミューテーションチェック: hits.length >= 4 を >= 5 に変更します。「ちょうど4つなら破棄」のテストは必ず失敗しなければなりません。すべて成功するなら、しきい値がテストされていません。書き直してください。
チェックリスト(テストごとに1回実施)
- テストする価値があるか?複雑なロジック → ユニットテスト、接着部分 → 統合テスト
- ケースはソースの分岐をコピーしたものではなく、振る舞いの一覧から導いている
- 同値クラスごとに代表ケースを1つ、しきい値ごとに境界±1のケースを用意し、「複数が同時に該当する」ケースもカバーしている
- 空/過大/不正/悪意のある入力に、それぞれ1ケース用意している
- 戻り値/観測可能な状態/副作用だけを、狭く明確にアサートしている。
expectedは手書き - 外部I/O/時刻/乱数生成だけをモックしている。ユニットのロジックは実物で、モック数は2~3以下
- テスト名は役割+シナリオ+期待結果を示し、公開APIから実行している
- ミューテーションチェックに合格する。本番コードを壊すと、このテストが失敗する