design-testing-strategy について
成果物の重要度や振る舞いに応じて、テスト種別とケースを選ぶためのリファレンススキルです。順序付きの判断ゲート、スキップ基準、記入スキーマ、具体例を備えています。
主な機能
- 01順序に沿ったテスト判断ゲート
- 02重要度に基づくスキップ基準
- 03テストマトリクスの記入規則
- 04ISTQBのケース設計技法
- 05一連の手順を示す例
活用シーン
- 01コード変更に適したテスト種別の選定
- 02効果の低いテストの除外
- 03境界値や状態からのケース導出
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ neolabhq/ context-engineering-kit --skill design-testing-strategyインストール後、Claude Code で/design-testing-strategyを入力して読み込まれたか確認します。
詳細
テスト戦略の設計
目的と重要度に適したテスト戦略を設計するためのリファレンスマニュアルです。
このスキルは哲学的なものではなく、意思決定を重視しています。すべてのゲートは決定的であり(X の場合は ON、Y の場合は OFF)、すべてのスキーマは適用され(フィールドの順序が重要)、すべての例は最初から最後まで具体的に示されています。
このスキルの使い方
- Decision Gates を順番に読みます(Gate 0 -> Gate 6)。各ゲートは独立しているため、任意のテスト種別を ON にした状態で完了できます。
- Strategic Skip Heuristics を適用し、この成果物に対する ROI が低い ON のゲートを除外します。
- ON にした各ゲートについて、Test Matrix Schema(
selected_typesエントリ)を記入します。フィールドの順序は重要です。 - 却下した種別は
rejected_typesに、意図的にスキップした種別はdeliberately_skippedに記載します。 - Case Design Techniques の ISTQB 手法を使って、Test Cases to Cover の Markdown 箇条書きを作成します。
- 対応する Worked Example(A pure function / B HTTP+DB endpoint / C UI component)と照合します。
判定ゲート
ゲートは数値順に適用します。各ゲートは独立したブール値(applies: true|false)を返します。ゲート同士が互いの判定を覆すことはありません。単一の成果物で、ユニット+インテグレーション+コントラクト+プロパティベースのすべてが ON になる場合があります。
| # | 種類 | ON になる条件 | OFF になる条件 | 出典 |
|---|---|---|---|---|
| 0 | すべてスキップ | 重要度が NONE(ドキュメントのみ、コメント、書式設定、生成コード、ロジックを含まない設定、一時的なプロトタイプ) | 分岐、計算された出力、副作用、またはユーザーに見える動作を含むもの | The Pragmatic Programmer — 「徹底的かつ効果的にテストする」という考え方は、費用対効果がゼロの場合に効果的にスキップすることも意味します |
| 1 | ユニット | コードに何らかのロジックが含まれる場合:分岐、ループ、条件式、計算、変換、解析、検証、書式設定 | 動作を伴わない純粋な宣言的配線(DI 登録、ルートテーブル) | テストピラミッド(Vocke)の基盤レイヤー+Beck の TDDのレッド・グリーン・リファクターによるユニットテスト |
| 2 | インテグレーション | 境界を越える場合:HTTP 呼び出し、DB クエリ、外部 SDK、メッセージキュー、ファイルシステム I/O、またはユニットテスト用の代替オブジェクトでは動作が歪む、2 つ以上の異なるコラボレーターとの連携 | I/O がなく、安定したコラボレーターが 0〜1 個の純粋関数 | テスティングトロフィー(Dodds) — インテグレーションテストは最も費用対効果の高いレイヤーです。Google「ユーザーに沿ってテストする」 |
| 3 | コンポーネントまたは E2E | UI があり、重要度が MEDIUM-HIGH 以上で、ユーザー向けのクリティカルパス(サインアップ、チェックアウト、認証、支払い、主要 CTA)に関わる場合 | 内部管理者専用画面、開発ツール、または重要でない UI | テストピラミッドの上部+ISO/IEC/IEEE 29119のリスク順位付け+Google の E2E 原則 |
| 4 | コントラクト | 1 つ以上の異なるクライアント(モバイル+ウェブ、複数の内部サービス、外部パートナー)が利用する公開 API で、かつデプロイサイクルが独立している場合 | コンシューマーとプロバイダーが同時にデプロイされる API | Pact/CDC+Pactflow の CDC 解説 |
| 5 | スモーク | デプロイ可能な対象(ウェブアプリ、API、サービス)があり、デプロイ後の検証に意味があるデプロイ/CI パイプラインが存在する場合 | ライブラリ、内部ヘルパー、またはデプロイパイプラインがない場合 | Google「優れたエンドツーエンドテストとは」 — スモークテストはデプロイゲート用の最小限の E2E テストです |
| 6 | プロパティベース | 入力領域が広い、または無制限(数値範囲、文字列、リスト、パーサー、シリアライザー、エンコーダー、数学的処理)で、不変条件が安定しており(往復変換、冪等性、単調性、可換性)、かつ重要度が MEDIUM-HIGH 以上の場合 | 入力領域が小さく有限、不変条件が不安定、または重要度が LOW の場合 | Hypothesis/QuickCheck |
ゲート適用アルゴリズム
for gate in [Gate 0, Gate 1, ..., Gate 6]:
if gate.ON_condition_met(artifact):
result[gate.type] = applies: true
else:
result[gate.type] = applies: false
if Gate 0 is true:
short-circuit: emit empty selected_types, document criticality=NONE, stop
重要度スケール(ゲート 3 と 6 で使用):
| レベル | 定義 |
|---|---|
NONE | ドキュメント、書式設定、生成コード、一時的なコード、ロジックを含まない設定 |
LOW | 内部開発ツール、管理者専用画面、ログ書式設定ツール |
MEDIUM | 標準的な CRUD、単一チームが利用する内部 API、重要でない UI、ヘルパーとユーティリティ |
MEDIUM-HIGH | クリティカルパス上のユーザー向け UI、複数のコンシューマーが利用する公開 API、ビジネスワークフロー |
HIGH | 資金の移動、認証/認可の判断、セキュリティ上重要な検証、データ整合性、規制対象の分野 |
テスト種別リファレンス
| 種別 | 使用する場面 | 使用しない場面 | フレームワーク | 代表的な依存関係 | Google のサイズ |
|---|---|---|---|---|---|
| unit | 純粋なロジック、単一の関数/メソッド/クラス、決定的な入力 | ロジックがなく、コードが I/O のオーケストレーションだけを行う場合 | vitest、jest、pytest、go test、JUnit、xUnit、RSpec | なし(またはインメモリのフェイク) | Small |
| integration | 境界をまたぐ処理(DB、HTTP、キュー、FS)、モックによって動作が歪む複数の協調オブジェクト | 境界のない純粋関数 | vitest、jest、pytest、go test、JUnit + Testcontainers、supertest、TestRestTemplate | Testcontainers 経由の実際の Postgres/Redis/Kafka、インプロセス HTTP サーバー、tmpdir 内の実際の FS | Medium(単一マシン、localhost は可) |
| component | 単一のコンポーネント内での UI のレンダリングと操作。アプリ全体のコンテキストは不要 | バックエンドのみのロジック、複数ページにわたるユーザーフロー | React Testing Library、Vue Test Utils、Angular TestBed、Storybook のインタラクションテスト | jsdom または happy-dom、fetch/axios レベルでのネットワークのモック | Small ~ Medium |
| e2e | 実行中のアプリを通る完全なユーザーパス:実際のブラウザー、実際のバックエンド、実際の DB | 内部ヘルパー、単一コンポーネント、重要度の低い UI | Playwright、Cypress、Selenium | 実際に実行中のアプリ + Testcontainers が用意する DB、またはシード済みのステージング環境 | Large(複数プロセス、場合によっては複数マシン) |
| smoke | デプロイ後の可否判定:/ にアクセスする、主要エンドポイントが応答する、ログインが機能する | 詳細な正確性の確認。スモークテストは設計上、浅いテストです | Playwright(重要なパスを 1~3 個)、HTTP プローブスクリプト、k6 の最小シナリオ | 実際にデプロイされた環境 | Large |
| contract | 独立したデプロイサイクルを持つ、2 つ以上の異なるクライアントが利用する公開 API | 単一の利用者のみの内部 API、プロバイダーと利用者が一緒にデプロイされる場合 | Pact、Spring Cloud Contract、OpenAPI スキーマバリデーター | Pact ブローカー、またはリポジトリ内のコントラクトファイル | Medium |
| property-based | 安定した不変条件を持つ、大規模または無制限の入力領域(パーサー、シリアライザー、エンコーダー、数学) | 小規模な有限入力領域、不安定な不変条件 | Hypothesis(Python)、fast-check(TS)、QuickCheck(Haskell)、jqwik(Java)、proptest(Rust) | unit と同じ | Small |
Google テストサイズの対応付け
Google Test Sizes (Bland) と SWE at Google Ch.11 では、スコープ(対象となるパス)とは独立して、リソース(サイズ)によってテストを分類しています。
| サイズ | プロセスモデル | ネットワーク | ファイルシステム | 時間の上限 | 備考 |
|---|---|---|---|---|---|
small | 単一プロセス、単一スレッド | なし | なし(インメモリのみ) | < 100ms | 高速、自己完結型、並列実行可能 |
medium | 単一マシン、複数プロセス可 | localhost のみ | tmpdir 可 | < 1s | Testcontainers はこのサイズに適合 |
large | 複数マシン | 外部ネットワーク可 | 永続 FS 可 | < 15min | 完全な e2e |
enormous | 分散型 | 広域ネットワーク | 場所を問わない | それ以上 | クラスター/カオス |
テストの種別(unit/integration/e2e)とサイズ(small/medium/large)は直交します。小規模な統合テスト(同じプロセス内で JDBC 経由で Testcontainers の Postgres を使用するテスト)も有効です。
Playwright と Cypress の比較(UI E2E)
| 観点 | Playwright | Cypress |
|---|---|---|
| ブラウザー | Chromium、Firefox、WebKit | Chromium、Firefox、WebKit(一部制限あり) |
| 複数タブ/複数オリジン | 対応 | 一部制限あり |
| 並列実行 | シャード機能を標準搭載 | 有料ダッシュボードまたは外部ツール |
| ネットワークインターセプト | ルートレベルで柔軟に対応 | cy.intercept |
| 選択の目安 | チームで Cypress をすでに標準化していない限り、新規プロジェクトでは Playwright を選びます | チームに Cypress の大規模な投資がある場合は Cypress を選びます |
テストケース設計の手法
ISTQB Foundation Level のブラックボックステスト技法を使って、選択した各テストタイプで何をテストするかを導き出します。参考資料:ISTQB BVA ホワイトペーパー、ASTQB ブラックボックステスト技法。
1. 同値分割法(EP)
システムが同じように動作すると予想される入力範囲を分割します。各分割についてテストは1件で十分です。
例 — discount(orderTotal: number) -> number:
| 分割 | 範囲 | 代表的なテスト入力 | 期待値 |
|---|---|---|---|
| しきい値未満 | 0 <= total < 100 | 50 | 0% discount |
| 中間層 | 100 <= total < 500 | 250 | 5% discount |
| 最上位層 | total >= 500 | 1000 | 10% discount |
| 無効(負の値) | total < 0 | -1 | throw / error |
4件のテストですべての分割を網羅できます。EP だけでは境界値を見落とすため、BVA と組み合わせます。
2. 境界値分析(BVA)
バグは境界値に集中します。境界値 B ごとに、B-1、B、B+1(浮動小数点数の場合は、表現可能な最小の刻み幅)をテストします。
例 — 同じ discount 関数で、境界値が 100 の場合:
| テスト入力 | 理由 | 期待値 |
|---|---|---|
99(= B-1) | 「しきい値未満」の分割における最後の値 | 0% discount |
100(= B) | 「中間層」の分割における最初の値 | 5% discount |
101(= B+1) | 2つずれた値になっていないことを確認 | 5% discount |
境界値 500 についても同様に、499、500、501 をテストします。合計は境界値テスト6件 + EP テスト4件 = 10ケースです。
B-1 / B / B+1 の3つ組は、境界値が変わっても同じ構成(入力値と期待値を変え、アサーションは同一)です。そのため、テーブル駆動テスト(下記のサブセクション5を参照)に自然に適しています。
3. デシジョンテーブル
出力が条件の組み合わせに依存する場合に使います。各列が1つのルールです。
例 — canCheckout(cartHasItems, paymentValid, addressOnFile):
| 条件/ルール | R1 | R2 | R3 | R4 |
|---|---|---|---|---|
| cartHasItems | T | T | T | F |
| paymentValid | T | T | F | * |
| addressOnFile | T | F | * | * |
| 結果 | 許可 | ブロック:住所 | ブロック:支払い | ブロック:カート |
各ルールにつき1件、合計4件のテストです(* = 問わない。統合によって省略)。
4. 状態遷移
動作が履歴に依存する場合に使います。状態、イベント、禁止されている遷移を特定します。
例 — 状態 {draft, submitted, paid, shipped, cancelled} を持つ注文の状態マシン:
| 遷移元 | イベント | 遷移先 | テスト |
|---|---|---|---|
| draft | submit | submitted | 正常系 |
| submitted | pay | paid | 正常系 |
| paid | ship | shipped | 正常系 |
| draft | cancel | cancelled | 早期キャンセル |
| paid | cancel | 拒否 | 禁止 — 返金フローが必要で、直接キャンセルは不可 |
| shipped | submit | 拒否 | 禁止 |
許可された遷移ごとに1件、禁止された遷移ごとに1件のテスト(ネガティブパス)を実施します。
5. テーブル駆動テスト
EP、BVA、またはデシジョンテーブル分析によって、同じ形式のケースが3つ以上導き出される場合(同じセットアップ、同じアサーションで、入力と期待される出力だけが異なるケース。例:有効/無効な日付形式の解析、税率区分ごとの税額計算、ルーティングルール)、それらを1つのテーブル駆動テストにまとめます。各ケースをデータテーブルの行として記述し、テスト本体で行を反復して、各行につき1つのアサーションを実行します。参考資料:Dave Cheney「テーブル駆動テストを優先する」;Go wiki:TableDrivenTests。
ケースごとにセットアップ、フレームワークの呼び出し、またはアサーションの形式が大きく異なる場合は、テーブルにまとめることを無理に強制しないでください。無理に統一すると、実際の違いが1つの名前の背後に隠れ、失敗メッセージも分かりにくくなります。そのようなケースは個別に名前を付けたテストとして分けてください。
実例 — discount(orderTotal)の6つのEP+BVAケース(境界値は100)を、1つのテーブル駆動ユニットテストにまとめます(TS/vitestの構文。同じパターンはGoのt.Run、JUnitの@ParameterizedTest、pytestのparametrizeにも適用できます)。
describe("discount", () => {
const cases: Array<{ name: string; input: number; expected: number }> = [
{ name: "EP: below threshold (typical)", input: 50, expected: 0 },
{ name: "BVA: B-1 at boundary 100", input: 99, expected: 0 },
{ name: "BVA: B at boundary 100", input: 100, expected: 0.05 },
{ name: "BVA: B+1 at boundary 100", input: 101, expected: 0.05 },
{ name: "EP: mid tier (typical)", input: 250, expected: 0.05 },
{ name: "EP: top tier (typical)", input: 1000, expected: 0.10 },
];
for (const c of cases) {
it(c.name, () => {
expect(discount(c.input)).toBe(c.expected);
});
}
});
name列は必須です。各行を個別に識別できるテストにすることで、「6行中の3行目」ではなく、該当するケースを特定できる失敗メッセージが表示されます。異なるアサーションが必要な行(例:負の入力のケースで例外が発生する場合)は、テーブルの外で別のテストにしてください。
依存関係の選択
Gate 2(インテグレーション)とGate 3(コンポーネント/E2E)では、依存関係を慎重に選択してください。目標は、CIで決定論的に実行できる範囲で、最大限の現実性を確保することです。
| 依存関係のスタイル | 使用するケース | 避けるケース | 備考 |
|---|---|---|---|
| Testcontainersを使った実インフラ | DB/Redis/Kafka/ブラウザーを使う場合、開発時に実際のドライバーの動作が必要な場合、CIの隔離環境が必要な場合 | コールドスタートの予算が1秒未満の場合、Dockerを利用できない場合 | Postgres/Redis/Kafka/Localstackのインテグレーションテストでは、これをデフォルトとします |
| インメモリのフェイク | 所有するインターフェースのセマンティクスが単純(キーと値、リストなど)で、テストの速度が重要な場合 | フェイクが実環境と異なり、インテグレーション境界で気付かないバグが発生する場合 | ヘキサゴナルアーキテクチャのリポジトリポートでは使用できます。ただし、そのポート自体について、実インフラに対するコントラクトテストがあることが条件です |
| モック(テストダブル) | 純粋なインターフェースを持つ単一の協調相手があり、テストの焦点がプロトコル(XがYを指定して呼び出されたか)にある場合 | 2つを超える協調相手をモックする場合、またはデータ構造をモックする場合(不完全なモックはアンチパターンです) | モックは分離するためのツールであり、テスト対象ではありません |
| スタブ化されたHTTP | Testcontainers/Localstackの選択肢が存在しない外部SaaSを呼び出す場合 | Pact/CDCが必要な場合(代わりにコントラクトテストを使用してください) | nock(Node)、responses(Python)、WireMock(JVM) |
| 実際の外部サービス | ステージング環境でのスモークテストに限る | ユニット/インテグレーション/CI(常に非決定論的です) | ステージング環境に対するスモークテストに限定してください |
トレードオフのまとめ:Testcontainers > インメモリのフェイク > モックの順で信頼性が高くなりますが、コストも同じ方向に増加します。境界の動作について誤解を招かない範囲で、最も低コストのレベルを選んでください。
戦略的スキップのヒューリスティック
明示的な「対応不要」のルールです。これらをスキップするのは怠慢ではなく、ISO/IEC/IEEE 29119 のリスクベーステストとリスクベーステストに基づく、リスク調整後の投資収益率です。
| スキップ項目 | ルール |
|---|---|
| 内部ヘルパーの e2e テストは不要 | 成果物に UI サーフェスもユーザー向けの経路もない場合、e2e テストはスキップします。ユニットテストと統合テストで十分です。 |
| デプロイ時に結合されたコンシューマー API のコントラクトテストは不要 | API を利用するクライアントが 1 つだけで、同時にデプロイされる場合、コントラクトテストは分離のメリットがないまま保守負担を増やします。 |
| 小規模な有限領域のプロパティベーステストは不要 | 入力領域が enum {A, B, C} の場合、EP と BVA で既に網羅されています。プロパティベーステストを追加しても、より多くのバグを発見することなく、インフラの負担が増えるだけです。 |
| 純粋関数の統合テストは不要 | formatCurrency ヘルパーをテストするために Postgres コンテナを追加するのは無駄です。ユニットテストのみで十分です。 |
| 静的マークアップのコンポーネントテストは不要 | コンポーネントに状態もイベントも条件付きレンダリングもない場合は、スナップショットで十分です。あるいは、テストを完全に省略します。 |
| 宣言的な配線のユニットテストは不要 | DI バインディング、ルート登録、スキーマ宣言については、代わりに統合テストのレベルでアサーションを行います(ルートが適切なハンドラーを提供するかどうかなど)。 |
| 統合テストで確実にカバーできる対象の e2e テストは不要 | Google の e2e 原則にあるように、振る舞いをカバーできるテストは小さいほど優れています。e2e は例外であり、デフォルトではありません。 |
| スパイク/使い捨てコードのテストは不要 | Beck の TDDにあるように、成果物が数時間以内に削除される場合は、人間のパートナーと例外を文書化します。その後、残すバージョンに対してテストを書きます。 |
| 「かつ」を含むテストは不要 | テスト名に「かつ」が含まれている場合は、個別のテストに分割します(振る舞いごとにアサーションは 1 つ)。 |
テストマトリックスのスキーマ
すべてのテスト戦略は、以下の YAML ブロックで表現しなければなりません。各リスト項目内のフィールド順序は重要です。判定者と後続ツールは、最初のキーを重要な項目(rationale/reason/why)、2 番目のキーを分類項目(type/what)として解析します。
スキーマ
test_strategy:
artifact: "<path or short identifier>"
rationale: "Why this test strategy is being applied to this artifact (specific, evidence-based)"
criticality: "NONE | LOW | MEDIUM | MEDIUM-HIGH | HIGH"
selected_types:
- rationale: "Why this type is being applied to this artifact (specific, evidence-based)"
type: "unit | integration | component | e2e | smoke | contract | property-based"
size: "small | medium | large | enormous"
framework: "vitest | jest | pytest | go test | JUnit | playwright | cypress | pact | hypothesis | ..."
dependencies:
- "List of dependencies: real Postgres via Testcontainers, in-memory fake, mocked HTTP via nock, etc."
gate: "Gate N (the gate that triggered this selection)"
rejected_types:
- reason: "Why this type does NOT apply to this artifact (cite Strategic Skip Heuristic or gate that did not trigger)"
type: "unit | integration | component | e2e | smoke | contract | property-based"
deliberately_skipped:
- why: "Cost / risk justification for skipping despite a partial signal"
what: "A specific category of test cases being skipped (e.g., 'browser compatibility on IE11', 'load testing beyond 100 RPS')"
YAML の記述例
test_strategy:
artifact: "POST /users (user registration endpoint)"
rationale: "User registration is a critical user-facing path; can be used by web and mobile apps independently of each other."
criticality: "MEDIUM-HIGH"
selected_types:
- rationale: "Endpoint contains validation logic (email format, password rules, uniqueness) — Gate 1 ON for branch coverage"
type: "unit"
size: "small"
framework: "vitest"
dependencies: ["in-memory user repository fake"]
gate: "Gate 1"
- rationale: "Endpoint writes to Postgres and emits user.created event to Kafka — Gate 2 ON, real boundary behavior matters"
type: "integration"
size: "medium"
framework: "vitest + supertest + Testcontainers"
dependencies: ["Postgres 15 via Testcontainers", "Kafka via Testcontainers"]
gate: "Gate 2"
- rationale: "Consumed by mobile app and web app on independent deploy cadences — Gate 4 ON, prevents drift"
type: "contract"
size: "medium"
framework: "Pact"
dependencies: ["Pact broker"]
gate: "Gate 4"
rejected_types:
- reason: "No UI surface in this artifact — Gate 3 OFF"
type: "component"
- reason: "No UI surface — Gate 3 OFF; e2e covered by web/mobile apps separately"
type: "e2e"
- reason: "Input domain (email, password) is large but invariants are well-covered by EP+BVA at unit level — property-based ROI is low at MEDIUM-HIGH criticality, only triggers Gate 6 partially"
type: "property-based"
deliberately_skipped:
- why: "Project does not have post-deploy probe pipeline yet; smoke would be no-op"
what: "Smoke test for /users after deploy"
- why: "Non-functional load testing is out of scope for this task; tracked separately in performance backlog"
what: "Load test verifying p99 < 200ms at 1000 RPS"
フィールド順序のチェックリスト(判定者はこの順序を厳密に確認します):
test_strategy:artifact、rationale、criticalityの順。selected_types[*]:rationale、type、size、framework、dependencies、gateの順。rejected_types[*]:reason、typeの順。deliberately_skipped[*]:why、whatの順。
ケース一覧のスキーマ
マトリックスの後に、実装するテストケースをフラットな Markdown の箇条書きで記述します。これは YAML マトリックスとは別です。理由は次のとおりです。
- a. テスト方法ではなく、テスト対象を記載するため
- b. 受け入れ条件に対応付けるため
形式
## Test Cases to Cover
### AC-N: [criterion title]
- [type] description
- [type] description
### AC-N: [criterion title]
- [type] description
- [type] description
各項目:
typeは、マトリックスのselected_types[*].typeのいずれかと一致します。descriptionは AAA/Given-When-Then(Dan North の BDD)の形式に従います。Bill Wake の AAA(2001)を参照してください。AC-Nは、そのケースで検証する受け入れ条件を参照します(インフラのスモークテストなど、受け入れ条件に紐付かない場合は省略します)。
記述例
## Test Cases to Cover
### AC-1: Discount returns the correct percentage based on the total
- [unit] discount returns 0% when total = 0 [EP partition: below threshold]
- [unit] discount returns 0% when total = 99 [BVA: B-1 at boundary 100]
- [unit] discount returns 5% when total = 100 [BVA: B at boundary 100]
- [unit] discount returns 5% when total = 101 [BVA: B+1 at boundary 100]
### AC-2: Discount fails when total is invalid
- [unit] discount throws when total = -1 [EP partition: invalid]
### AC-3: /orders saves the order to the database
- [integration] POST /orders persists order to Postgres and returns 201 with order id
### AC-4: /orders rejects duplicate idempotency key
- [integration] POST /orders rejects duplicate idempotency key with 409
### AC-5: /orders/:id returns order by id
- [contract] GET /orders/:id returns schema matching mobile-app pact
参考資料と関連資料
以下の14の資料が、上記のすべてのゲートとルールの根拠です。判断に迷った場合は、該当するゲートでリンクされている資料を参照してください。
- テストピラミッド — Mike Cohn(2009年、Succeeding with Agile)+ Ham Vocke、The Practical Test Pyramid、martinfowler.com。
- テスティングトロフィー — Kent C. Dodds(2018年)、The Testing Trophy and Testing ClassificationsおよびWrite Tests。
- Googleのテストサイズ — Mike Bland(2011年)、Small / Medium / Large、Software Engineering at Google Ch.11、Test Sizes (Google Testing Blog)。
- Google Testing on the Toilet — What Makes a Good End-to-End Test、Testing UI Logic - Follow the User、Origins (Mike Bland)。
- ISTQB Foundation Level — ブラックボックステスト技法:Boundary Value Analysis white paper、ASTQB Black-Box Techniques。
- ISO/IEC/IEEE 29119 — リスクベースのテストプロセス規格。Wikipediaの概要。
- Kent Beck — Test Driven Development: By Example(Addison-Wesley、2002年)。出版社のページ。ISBN 978-0321146533。
- The Pragmatic Programmer(20周年記念版) — Hunt & Thomas(2019年)。pragprog.com。
- AAAパターン — Bill Wake(2001年)、3A — Arrange, Act, Assert。Given-When-Then — Dan North、Introducing BDD。
- プロパティベーステスト — Hypothesis: What is property-based testing?、QuickCheck(Haskell)、fast-check(TS)。
- コントラクトテスト/コンシューマー主導コントラクト — Pact docs、Pactflow CDC explainer。
- Testcontainers — testcontainers.com。
- テーブル駆動テスト — Dave Cheney、Prefer table-driven tests、Go wiki: TableDrivenTests。
- リスクベーステスト — Risk Management During Test Planning (softwaretestinghelp.com)。
実例
各例では、以下を示します。
- a. 成果物と受け入れ基準
- b. ゲートごとのウォークスルー
- c. スキーマに沿った
test_strategyYAML - d.
Test Cases to Coverリスト - e. 却下したテスト種別についての解説
例 A — 純粋なヘルパー関数:formatCurrency(amount: number, code: string): string
成果物
function formatCurrency(amount: number, code: string): string;
// e.g. formatCurrency(1234.5, "USD") -> "$1,234.50"
// formatCurrency(1234.5, "EUR") -> "€1.234,50"
受け入れ基準:
- AC-1:USD の出力では、
$を先頭に付け、千の位をカンマ、小数点をピリオドで区切り、小数点以下を 2 桁にします。 - AC-2:EUR の出力では、
€を先頭に付け、千の位をピリオド、小数点をカンマで区切り、小数点以下を 2 桁にします。 - AC-3:サポートされていないコードに対して
Error("Unknown currency code")をスローします。 - AC-4:
amount = 0は"$0.00"/"€0,00"としてフォーマットされます。
重要度:LOW(表示のみで使用するヘルパーであり、ここでは金銭の移動はありません)。
ゲートの確認
| ゲート | 判定 | 理由 |
|---|---|---|
| 0 スキップ | OFF | ロジックがあります |
| 1 ユニット | ON | 通貨コードごとに分岐する純粋なロジックです — テストピラミッドの基盤 |
| 2 統合 | OFF | I/O も境界もありません — スキップのヒューリスティック:純粋関数には統合テストは不要 |
| 3 コンポーネント/E2E | OFF | UI の接点がありません |
| 4 コントラクト | OFF | 公開 API ではありません |
| 5 スモーク | OFF | デプロイできません |
| 6 プロパティベース | ON(一部) | 数値入力に上限はありませんが、不変条件(解析を介した往復変換、金額に対する単調性)があります — Hypothesis。重要度が MEDIUM-HIGH の場合は適用範囲を広げます。ここでは重要度が LOW であるため、控えめに適用します(プロパティ 1~2 件)。 |
test_strategy YAML
test_strategy:
artifact: "src/util/formatCurrency.ts"
rationale: "Pure helper function used in display only; no money movement here."
criticality: "LOW"
selected_types:
- rationale: "Pure logic with currency-specific branches and number formatting; EP+BVA on amount, decision table on currency code"
type: "unit"
size: "small"
framework: "vitest"
dependencies: []
gate: "Gate 1"
- rationale: "Amount domain is unbounded floats; invariant 'parseCurrency(formatCurrency(x, c)) ~= x' is stable; sparingly applied (1-2 properties) at LOW criticality"
type: "property-based"
size: "small"
framework: "fast-check"
dependencies: []
gate: "Gate 6"
rejected_types:
- reason: "No I/O, no boundary, no collaborators - Gate 2 OFF"
type: "integration"
- reason: "No UI surface - Gate 3 OFF"
type: "component"
- reason: "No UI surface - Gate 3 OFF"
type: "e2e"
- reason: "Internal helper, not consumed across deploys - Gate 4 OFF"
type: "contract"
- reason: "Library helper, no deploy pipeline target - Gate 5 OFF"
type: "smoke"
deliberately_skipped:
- why: "Locale list is finite (USD, EUR); exhaustive enumeration via decision table is sufficient and more maintainable than i18n property tests"
what: "Property-based fuzzing of currency code beyond known list"
対象とするテストケース
### AC-1: USD output uses `$` prefix, comma thousands, period decimal, two decimal places.
- [unit] formatCurrency(1234.5, "USD") returns "$1,234.50" [EP: typical USD]
- [unit] formatCurrency(0.01, "USD") returns "$0.01" [BVA: B+1 smallest non-zero]
- [unit] formatCurrency(-0.01, "USD") returns "-$0.01" [BVA: B-1 negative side]
### AC-2: EUR output uses `€` prefix, period thousands, comma decimal, two decimal places.
- [unit] formatCurrency(1234.5, "EUR") returns "€1.234,50" [EP: typical EUR]
- [property-based] for any non-NaN finite x in [-1e9, 1e9] and code in {USD, EUR}: parseCurrency(formatCurrency(x, code)) is within 0.005 of x [round-trip invariant]
### AC-3: Throws `Error("Unknown currency code")` for unsupported codes.
- [unit] formatCurrency(1, "XYZ") throws Error("Unknown currency code") [Decision table: unknown code]
### AC-4: `amount = 0` formats as `"$0.00"` / `"€0,00"`.
- [unit] formatCurrency(0, "USD") returns "$0.00" [BVA: B at amount=0]
- [unit] formatCurrency(0, "EUR") returns "€0,00" [BVA: B at amount=0 for EUR]
テスト種別を採用しなかった理由:このヘルパーには境界がないため(統合テストなし)、UI がないため(コンポーネント/E2E テストなし)、内部向けのライブラリ形式であるため(コントラクト/スモークテストなし)、さらに重要度が LOW で追加のテスト種別にかかるコストがその効果を大きく上回るためです。
例 B — DB と複数のコンシューマーを使用する HTTP POST エンドポイント:POST /users
成果物
次の処理を行うユーザー登録エンドポイントです。
- リクエストボディを検証します(メールアドレス形式、パスワードの複雑さ、年齢 >= 13)。
- Postgres に対してメールアドレスの一意性を確認します。
- ユーザーレコードを挿入します(トランザクション処理)。
user.createdイベントを Kafka に送信します。{id, email, createdAt}を含む201を返します。- 入力が無効な場合は
400、メールアドレスが重複している場合は409を返します。
利用者:独立したデプロイ周期を持つモバイルアプリ(iOS/Android)とウェブアプリ。
受け入れ基準:
- AC-1:有効なリクエストは
201を返し、ユーザーを永続化します。 - AC-2:メールアドレスの形式が無効な場合、フィールド単位のエラーとともに
400を返します。 - AC-3:パスワードがポリシーを満たさない場合、
400を返します。 - AC-4:メールアドレスが重複している場合、
409を返します。 - AC-5:登録に成功すると、
user.createdイベントを正確に 1 件送信します。 - AC-6:レスポンススキーマは、モバイルとウェブのコンシューマーに対して安定しています。
重要度:MEDIUM-HIGH(認証領域、ID ドメイン、複数のコンシューマーが利用するパブリック API)。
ゲートの判断手順
| ゲート | 判断 | 理由 |
|---|---|---|
| 0 Skip | OFF | 相応のロジックを含みます |
| 1 Unit | ON | バリデーター(メールアドレス、パスワード、年齢)は純粋なロジックです — テストピラミッドの土台 |
| 2 Integration | ON | 境界を越える処理:HTTP、Postgres、Kafka — ROI が最も高い領域であるテスティングトロフィー |
| 3 Component/E2E | OFF(ここでは) | この成果物に UI はありません。UI はモバイルおよびウェブのリポジトリにあり、それぞれでテストします |
| 4 Contract | ON | 独立したデプロイ周期を持つ、異なる 2 つのコンシューマー(モバイルとウェブ)があります — Pact CDC |
| 5 Smoke | ON | デプロイ可能な HTTP サービスです。デプロイ後に /users の登録をプローブすることに意味があります — Google の E2E |
| 6 Property-Based | OFF | 入力ドメイン(メールアドレス、パスワード、年齢)は制約されており、単体テストでの EP+BVA によって十分にカバーできます。重要度は MEDIUM-HIGH ですが、入力範囲が限定されているためゲート 6 は OFF です — スキップのヒューリスティック |
test_strategy YAML
test_strategy:
artifact: "POST /users (user registration endpoint)"
rationale: "User registration is a critical user-facing path; can be used by web and mobile apps independently of each other."
criticality: "MEDIUM-HIGH"
selected_types:
- rationale: "Validators (email, password, age) are pure logic; EP+BVA on each field; one test per partition"
type: "unit"
size: "small"
framework: "vitest"
dependencies: ["in-memory user repository fake (for service-level unit if needed)"]
gate: "Gate 1"
- rationale: "Endpoint writes to Postgres and emits to Kafka; mocking these distorts transactional and ordering behavior - Testcontainers gives real boundary fidelity"
type: "integration"
size: "medium"
framework: "vitest + supertest + Testcontainers"
dependencies: ["Postgres 15 via Testcontainers", "Kafka via Testcontainers"]
gate: "Gate 2"
- rationale: "Public API consumed by mobile + web on independent deploy cadences; contract testing prevents schema drift breaking either consumer"
type: "contract"
size: "medium"
framework: "Pact (provider verification)"
dependencies: ["Pact broker", "consumer-published pacts from mobile and web"]
gate: "Gate 4"
- rationale: "Deployable HTTP service with a post-deploy pipeline; one minimal smoke verifies /users responds 201 in the deployed environment"
type: "smoke"
size: "large"
framework: "Playwright (1 critical path)"
dependencies: ["deployed environment URL", "test account seeding"]
gate: "Gate 5"
rejected_types:
- reason: "No UI surface in this artifact - Gate 3 OFF; mobile and web repos own their own component tests"
type: "component"
- reason: "No UI surface - Gate 3 OFF; consumer e2e lives in mobile/web repos"
type: "e2e"
- reason: "Input domain is bounded and EP+BVA at unit level covers it; property-based on this glue endpoint adds infra without finding more bugs - Gate 6 OFF"
type: "property-based"
deliberately_skipped:
- why: "Performance/load testing is out of scope here; tracked in dedicated performance backlog"
what: "Load test verifying p99 < 200ms at 1000 RPS"
- why: "Cross-region failover is owned by infrastructure team, not this endpoint"
what: "Multi-region availability test"
対象となるテストケース
### AC-1: Valid request returns `201` and persists user.
- [unit] validateEmail accepts "[email protected]" [EP: well-formed]
- [integration] POST /users with valid body returns 201 and persists row in Postgres
- [smoke] POST /users in deployed environment returns 201 for a synthetic test account
### AC-2: Invalid email format returns `400` with field-level error.
- [unit] validateEmail rejects "alice@" [EP: missing domain]
- [unit] validateEmail rejects "" [BVA: empty boundary]
- [integration] POST /users with invalid email returns 400 and does NOT persist
### AC-3: Password not meeting policy returns `400`.
- [unit] validatePassword rejects 7-char password [BVA: B-1 at min length 8]
- [unit] validatePassword accepts 8-char password meeting policy [BVA: B at min length]
- [unit] validatePassword accepts 9-char password [BVA: B+1]
- [unit] validateAge rejects 12 [BVA: B-1 at boundary 13]
- [unit] validateAge accepts 13 [BVA: B at boundary 13]
### AC-4: Duplicate email returns `409`.
- [integration] POST /users with duplicate email returns 409 and does NOT emit event
### AC-5: Successful registration emits exactly one `user.created` event.
- [integration] POST /users emits exactly one user.created event to Kafka on success
- [integration] POST /users transaction rolls back when Kafka publish fails [State Transition: failure path]
### AC-6: Response schema is stable for mobile + web consumers.
- [contract] Provider satisfies mobile pact: POST /users response shape matches mobile contract
- [contract] Provider satisfies web pact: POST /users response shape matches web contract
型が却下された理由:UI が存在しない(コンポーネントテスト/E2E テストはコンシューマーアプリ側の対象)、入力空間が限定されている(プロパティベーステストの ROI が低い)、対象外の懸念事項(負荷、マルチリージョン)は根拠を明示して意図的に省略しています。
例 C — UI フォームコンポーネント:<RegistrationForm />(Web)
成果物
React フォームコンポーネント:
- フィールド:email、password、confirmPassword、age。
- クライアント側のバリデーション:メール形式、大小文字の混在と数字を含む 8 文字以上のパスワード、パスワードの一致、age >= 13。
POST /usersに送信します。- フィールド内のエラーと送信時のエラー(ネットワーク、409 duplicate)を表示します。
- リクエスト処理中は送信ボタンを無効にし、応答後に再び有効にします。
- WCAG 2.1 AA:ラベルを入力欄に関連付け、
aria-liveでエラーを通知し、バリデーション失敗時は最初のエラーにフォーカスを移します。
受け入れ基準:
- AC-1:ユーザーは有効なフォームを送信し、
/welcomeに遷移できます。 - AC-2:無効なメールアドレスの場合、フィールド内に
"Enter a valid email"と表示されます。 - AC-3:パスワードが一致しない場合、フィールド内に
"Passwords must match"と表示されます。 - AC-4:リクエスト処理中は送信が無効になります。
- AC-5:サーバーからの 409 応答時、フォームレベルで
"This email is already registered"と表示されます。 - AC-6:フォームはキーボードで操作でき、バリデーション失敗時は最初のエラーにフォーカスが移ります。
- AC-7:すべての入力欄にプログラム上のラベルがあり、
aria-live="polite"でエラーが通知されます。
重要度:MEDIUM-HIGH(登録はユーザー向けの重要な経路であり、多くの法域でアクセシビリティが規制されています)。
ゲートの判定
| ゲート | 判定 | 理由 |
|---|---|---|
| 0 スキップ | OFF | 動作とアクセシビリティのロジック |
| 1 ユニット | ON | バリデーションヘルパー(validateEmail、passwordsMatch、parseAge)は純粋なロジックです |
| 2 統合 | OFF(ここでは) | コンポーネント自体は実際の境界を越えません。ネットワークは fetch レベルでモックされています。ネットワーク統合は POST /users(例 B)が担当します |
| 3 コンポーネント/E2E | ON(コンポーネント)+ ON(登録経路の E2E) | UI サーフェス、重要度 MEDIUM-HIGH、ユーザー向けの重要な経路 — テストピラミッドの最上層 + ユーザーの操作に沿う |
| 4 コントラクト | OFF | UI は API を利用します。プロバイダー側のコントラクトテストは例 B にあります |
| 5 スモーク | ON | Web アプリはデプロイされます。「登録ページが表示され、送信できる」ことのスモークテストには意味があります |
| 6 プロパティベース | OFF | フォーム入力の範囲は限定されています。EP+BVA で網羅できます |
test_strategy YAML
test_strategy:
artifact: "src/components/RegistrationForm.tsx"
rationale: "React form component used in web app; registration is a business-critical user-facing path."
criticality: "MEDIUM-HIGH"
selected_types:
- rationale: "Validation helpers (validateEmail, passwordsMatch, parseAge) are pure logic; EP+BVA per field"
type: "unit"
size: "small"
framework: "vitest"
dependencies: []
gate: "Gate 1"
- rationale: "UI rendering + interaction within a single component; network mocked at fetch level - tests focus on user-facing behavior per Follow the User"
type: "component"
size: "small"
framework: "vitest + React Testing Library"
dependencies: ["happy-dom", "msw (mock service worker) for fetch"]
gate: "Gate 3"
- rationale: "Registration is a critical user-facing path; one e2e covers the full happy path with real backend (Testcontainers-backed)"
type: "e2e"
size: "large"
framework: "Playwright"
dependencies: ["app server running locally", "Postgres via Testcontainers", "Kafka via Testcontainers"]
gate: "Gate 3"
- rationale: "Web app deploys to staging/prod; smoke verifies /register page loads and form submits in deployed env"
type: "smoke"
size: "large"
framework: "Playwright (1 critical path)"
dependencies: ["deployed environment URL", "test account seeding"]
gate: "Gate 5"
rejected_types:
- reason: "Component does not own a real boundary; network integration is owned by POST /users (provider) - Gate 2 OFF for this artifact"
type: "integration"
- reason: "UI consumes the API; provider contract tests live with the provider (POST /users) - Gate 4 OFF for the consumer"
type: "contract"
- reason: "Bounded input space; EP+BVA at unit level is sufficient - Gate 6 OFF"
type: "property-based"
deliberately_skipped:
- why: "Cross-browser e2e on legacy browsers (IE11) is out of support per project browser matrix"
what: "Browser compatibility e2e on IE11 / Edge Legacy"
- why: "Visual regression (pixel diff) is owned by a separate Storybook chromatic pipeline"
what: "Pixel-level visual regression assertions"
対象とするテストケース
### AC-1: User can submit a valid form and is navigated to `/welcome`.
- [unit] validateEmail accepts "[email protected]" [EP: well-formed]
- [unit] parseAge rejects 12 [BVA: B-1 at boundary 13]
- [unit] parseAge accepts 13 [BVA: B at boundary 13]
- [e2e] user fills valid form, submits, and lands on /welcome page
- [smoke] /register page loads and form submits in deployed environment
### AC-2: Invalid email shows inline `"Enter a valid email"`.
- [unit] validateEmail rejects "" [BVA: empty boundary]
- [unit] validateEmail rejects "alice@" [EP: missing domain]
- [component] entering invalid email and blurring shows "Enter a valid email" inline
### AC-3: Mismatched passwords show inline `"Passwords must match"`.
- [unit] passwordsMatch returns true when both equal "Abcd1234"
- [unit] passwordsMatch returns false when one is "" [BVA: empty]
- [component] entering mismatched passwords shows "Passwords must match" inline
### AC-4: Submit is disabled while request is in flight.
- [component] submit is disabled when password and confirmPassword differ
- [component] submit click disables button while request is pending [State Transition: idle -> pending]
### AC-5: 409 response from server shows `"This email is already registered"` at form level.
- [component] 409 response shows form-level "This email is already registered"
### AC-6: Form is keyboard navigable; focus moves to first error on validation failure.
- [component] validation failure moves focus to first error field [a11y]
### AC-7: All inputs have programmatic labels; errors are announced via `aria-live="polite"`.
- [component] form renders email, password, confirmPassword, age, submit [happy path render]
- [component] all inputs have programmatic labels and errors live in aria-live="polite" region [a11y]
各テストタイプを却下した理由:この成果物は UI コンシューマーです。実際の境界は API であり、例 B(プロバイダー側)で統合テストとして検証されます。範囲の限られた UI 入力処理には、プロパティベーステストは適切ではありません。レガシー環境でのクロスブラウザテストとビジュアルリグレッションテストは対象外であり、理由を明記したうえで明示的に省略します。
スキルのセルフチェック
戦略を完成として宣言する前に、読み込みを確認してください:
- 7 つすべてのゲートを明示的に評価している(ON/OFF と理由)。
-
selected_types[*]の順序がrationale -> type -> size -> framework -> dependencies -> gateになっている。 -
rejected_types[*]の順序がreason -> typeになっている。 -
deliberately_skipped[*]の順序がwhy -> whatになっている。 - 各 AC が少なくとも 1 つのテストケースで参照されている。
- BVA ケースで、各数値境界について
B-1、B、B+1を列挙している。 - Google のテストサイズに従って、テストサイズ(small/medium/large)を割り当てている。
- スキップのヒューリスティックに従い、テスト名に「and」が含まれていない。
- 少なくとも 1 つの戦略的スキップのヒューリスティックを適用したか、明示的に検討し、理由を示したうえで適用しない判断をしている。
いずれかの確認項目を満たしていない場合は、提出前に戦略を修正してください。
カバレッジ分析
ミューテーションテストなどのカバレッジ分析手法(テスト作成後にテストスイートの品質を評価するために使用)は、付属の test-coverage スキルに記載されています。