- name
- test-driven-development
- description
- テストを先に書いてから実装するための手順。シグネチャ→テスト→失敗の確認→実装の順に進め、テストが実装を検証していることを確かめる。新規実装・更新実装・バグ修正の3つの進め方と、テストを弱めて通す迂回の禁止、テスト側の修正が要ると考えたときの判定手順を定める。テスト駆動開発でコードを書く・直すときに使用する。
- license
- MIT
- metadata
- {"version":"1.0.0"}
# test-driven-development: テスト駆動開発の進め方
> **skill version**: 1.0.0
セッション内で初めて本スキルを使用するときは「test-driven-development スキル v1.0.0 を使用します」と宣言する。
テストを先に書き、**そのテストが実装を検証していることを確かめながら**コードを書くための手順書。
言語・テストフレームワークを問わない。
## 前提と適用範囲
- 本スキルは**テスト駆動開発を採ると決めた開発**で使う。採るかどうかは利用者の方針であり、本スキルは判断しない。
- 「テストを書く」「テストを実行する」ができれば成立する手順であり、特定の言語や道具に依存しない。
- 作業を分担して進める場合も手順は同じ。**テストを書く担い手と本体を実装する担い手を分けられるなら分ける**と、
実装の都合がテストに混ざりにくくなる。
**適用する対象**: 振る舞いを持つコード。新しい関数・クラス・モジュール、既存の振る舞いの変更、バグの修正。
**適用しない対象**:
- 使い捨ての探索コード(結果を捨てる前提の試作)
- ドキュメント・設定値・定型データの変更
- 自動生成される成果物
- 外部システムを呼ぶだけで、自前の判断を持たない受け渡し層(実際の挙動の確認で代替する)。
**分岐や変換を行うなら適用する。**
**「テストが書きにくい」は適用しない理由にしない。** テストの書きにくさは、責務が絡んでいる、
依存が直に埋め込まれている、といった設計の問題を示す信号である。免除の口実にせず、設計を見直す。
## 3つの段階と、2つの確認
### 第1段階: シグネチャ
- 名前・引数・戻り値・型・送出しうるエラーを決める。
- **契約を書く。** 前提条件(呼び出す側が守るべきこと)、事後条件(満たされる結果)、不変条件を、
その言語の文書化手段で明示する。
- **本体は実装しない。** 未実装であることが分かる形にする(その言語で未実装を表す慣行に従う)。
既存の本体があるときは、後述の「更新実装」の項に従う。
- **ダミーの戻り値を返さない。** 固定値や空の値を返すと、次の段階のテストが偶然通ってしまい、
**第2段階の確認が意味を失う**。
- ここで設計上の判断が要るなら、テストを書く前に決める。シグネチャが定まらないままテストは書けない。
### 第2段階: テスト
**本体のコードではなく、シグネチャ・契約・実装の計画を根拠に**テストを書く。観点は後述の一覧による。
- **1つのテストで1つの事実を検証する。** 名前は、検証する振る舞いを述べる。
- **期待値はテストの中に直接書く。** 実装と同じ式で計算した値と比べない。
同じ式で求めれば、実装が誤っていても一致してしまう。
- **決定性を保つ。** 時刻・乱数・ネットワーク・ファイルの列挙順・並行実行の順序に結果を左右されないようにする。
左右される場合は、その値を外から与えられるようにして固定する。
- **失敗したときに何が起きたのかが読めるようにする。** 何と何を比べたのかが出力から分かること。
### 第2段階の確認: テストが失敗することを確かめる
**実装に入る前にテストを実行する。** 確認するのは2点である。
1. **新しく書いたテストがすべて失敗すること。** 失敗しないテストは、実装が無くても通るテストであり、
何も検証していない。書き直す。よくある原因は、アサーションが無い、条件が緩すぎる、
検証対象を呼んでいない、模擬物(本物の代わりに置く偽の部品)が検証対象そのものを
置き換えている、である。
2. **失敗の理由が本体の側にあること。** 新規実装なら「まだ実装していないから」、
更新実装なら「新しい仕様にまだ対応していないから」、バグ修正なら「バグがあるから」である。
構文の誤り、読み込みの誤り、準備処理の誤りによる失敗は**テスト側の欠陥**なので、先に直す
(これは後述の分類 (A) にあたり、合議は要らない)。
**この段階を飛ばさない。** 通過だけを確認する進め方では、
**そのテストが実装を検証しているかどうかを確かめる機会がどこにも無くなる**。
### 第3段階: 実装
- テストを満たす**一般解**を書く。
- **テストに出てくる具体的な入力値を、実装が特別扱いしてはならない。**
入力値で分岐する、期待値をそのまま返す、といった形で通すのは実装ではない。
- テストが通らないときは実装を直す。テストの方を直したくなったら、後述の判定手順に従う。
### 実装後の確認と整理
- **新しいテストだけでなく、既存のテストも含めて全件を実行する。** 既存の振る舞いを壊していないことをここで見る。
- **通過・失敗・スキップの件数を確認する。** スキップが増えていたら、その理由を報告に書く。
- 通過したら整理する(命名、重複の除去、責務の分割)。
**整理したあとで、もう一度全件を実行して通過を確認する。**
## モード別の進め方
3つの段階と2つの確認は共通で、モードごとに次の差がある。
### 新規実装
3つの段階をそのまま順に行う。
### 更新実装(すでに本体がある場合)
- **第1段階**: 必要ならシグネチャを更新する。**本体の実装は直さない。**
第2段階に備えて**更新後のシグネチャだけを読む**。読み取りの過程で本体の一部が目に入るのは避けられないが、
**その知識をテストの根拠に使わない**。実装がそうなっているからテストもそうする、という順序になると、
テストは実装の写しになり、誤りを検出しなくなる。
- **第2段階**: 更新後の仕様に沿ってテストを追加・修正する。
**既存テストの期待値を変えるときは、更新後の仕様を出典として示す**(後述の分類 (C))。
出典を示せないなら、それは仕様の変更ではなく (D) にあたり、合議が要る。
- **第2段階の確認**: **新しく書いた、あるいは修正したテストが失敗すること**を確認する。
既存のテストは通ったままでよい。
- **第3段階**: 実装を更新する。**既存の振る舞いを壊していないことを、既存テストの通過で確認する。**
### バグ修正
- **第1段階**: シグネチャは通常変えない。**バグの再現条件を特定する**
(どの入力で、何が起き、本来は何が起きるべきだったか)。
- **第2段階**: **バグを再現する失敗テストを先に書く。**
報告された入力に対して、**期待される正しい出力**を書く。
- **第2段階の確認**: **そのテストが、バグのせいで失敗することを確認する。**
失敗しないなら、再現条件をまだ特定できていない。修正に進まず、条件の特定に戻る。
- **第3段階**: 修正する。
- **再現テストは恒久的に残す。** 同じ退行を次に検出するのは、このテストである。
## テストケースを立てる観点
1. **正常系** — 代表的な値と、**境界値**(下限・上限とその前後、空・1件・多件)。
2. **異常系** — 範囲外の値、型の違う値、欠損(未設定・空)、壊れた形式。
3. **状態と順序** — 中断・取り消し、同じ要求の重複、並行実行、再入、順序を入れ替えた呼び出し。
4. **外部依存の異常** — 空の応答、タイムアウト、エラー応答、遅延、想定しない形式。
5. **セキュリティ** — 注入(問い合わせ言語・コマンド・スクリプト)、権限の境界、過大な入力。
6. **退行** — 過去に直したバグの再現テスト。バグ修正モードで作られ、ここに積み上がる。
**網羅率の数値を通過条件にしない。** 数値目標は、通すためだけのテストを増やす動機になる。
観点を満たしているかは、この一覧に照らして確認する。
## やってはならないこと
**「テストを通す」ことと「テストが通る状態を作る」ことは違う。**
後者は、そのテストが検出するはずだった誤りを隠す。以下を禁止する。
ただし**禁止を意識するだけでは足りない**ので、それぞれに検出の手がかりを添える。
| してはならないこと | 検出の手がかり |
|---|---|
| テストを削除する/スキップ・除外の印を付ける/アサーションを消す・緩める | 実行結果の**件数とスキップ数**を前後で比べる。減っていたら理由を述べる |
| **期待値を、実装が実際に出した値に書き換える** | テストの差分を読む。**実装の出力は仕様の出典にならない。** 根拠を示せない期待値の変更は後述の**分類 (D)** であり、合議なしには行えない |
| **実装がテストの入力値を特別扱いする**(入力値での分岐、期待値をそのまま返す) | 実装を読み、**テストに出てくる具体値が実装に現れていないか**を見る。現れていれば一般解になっていない |
| エラーを握りつぶして通す | エラーを**捕まえたまま何もしていない箇所**と、**返されたエラーを見ずに捨てている箇所**を探す |
| 模擬物で検証対象そのものを置き換える | そのテストが**検証対象の実装を一行も通らない**なら、検証していない |
| 比較や検証の仕組み自体を無効化する(常に真を返す比較を定義する、テストの実行を途中で打ち切って正常終了させる、テストの設定を書き換える) | **実行結果ではなく、実装とテスト設定の差分を読む。** この種の細工は結果だけを見ても分からない |
| 一部のテストだけを実行して「通った」と報告する | 実行したコマンドに**対象を絞る指定が入っていないか**を見る。報告された件数を、テスト全体の数と突き合わせる |
このうち**入力値の特別扱い**と**期待値の書き換え**は、テストに手を触れずに、
あるいは「テストを直している」という自覚なしに起こる。
**第2段階の確認と、実装の差分を読むこと以外では捕まらない。**
## テスト側の修正が必要と考えられる場合
テスト側に誤りがある可能性は実際にある。しかし**本体ではなくテストを直すことを安易に行うと、
テスト駆動開発は成立しなくなる**。そこでテスト側の変更を4つに分け、**判断が要るものだけ**を合議にかける。
| 分類 | 内容 | 扱い |
|---|---|---|
| **(A) 実行できない欠陥** | 構文の誤り、読み込みの誤り、準備処理の誤り、テスト自身の明らかな誤字 | **合議は要らない。直してよい。ただし期待値には触らない**(触るなら (C) か (D)) |
| **(B) テストの追加** | 観点の抜けを埋めるテストを足す | **自由。** 既存のテストを消さない限り制限しない |
| **(C) 仕様の変更に伴う期待の更新** | 仕様が変わったので、既存テストの期待値を新しい仕様に合わせる | **合議は要らない。ただし変更後の仕様の出典**(実装の計画、課題、仕様書など)**を示し、記録に残す。** 出典を示せないなら (D) である |
| **(D) 根拠を示せない期待の変更** | 仕様上の根拠を示せないまま期待値やアサーションの意味を変える、テストを削除する、適用範囲を狭める | **合議が必要**(下記) |
境界の引き方:
- 動かないテストを動くようにするのは (A)。**期待している振る舞いは変わらない。**
- **(C) と (D) を分けるのは「変更後の仕様を出典として示せるか」**である。示せるなら (C)、示せないなら (D)。
**「たぶん仕様が変わったはず」は出典ではない。** 実装が実際に出した値も出典ではない。
### (D) の手順
1. **作業を委ねられた側なら、自分で修正せずに差し戻す。** 「テスト側の修正が要ると考える理由」を、
仕様の出典・該当するテスト・失敗の出力を添えて、依頼元へ返す。
2. **差し戻しを受けた側、または自分でテスト側の誤りだと考えた側が、自分を含む3者で判定する。**
独立した**判定役サブエージェント**2者に依頼する。
**異なるモデルであることが望ましい**(選べない場合は同じモデルでもよい)。
3. **依頼は中立に書く。結論を示唆しない。** 渡す材料は**仕様の出典・テスト・実装・失敗の出力**とし、
問いは「**仕様に照らして、テストと実装のどちらが誤っているか**」とする。
「テストが間違っていると思うので確認してほしい」と訊いてはならない。
同意が返るだけで、合議が追認の手続きになる。
4. **3者のうち2者以上がテスト側の誤りと判定した場合に限り、テストを修正する。**
5. 棄却された場合は**実装側で対応する**。
6. **判定の結果と根拠を作業の記録に残す。** 誰が何を根拠にどう判定したかが、後から追えるようにする。
**判定役を用意できない場合**: 独立した判定役をどうしても用意できない環境では、
**自分だけで決めず、利用者に判断を仰ぐ**。「用意できないので自分で決める」は選択肢に入れない。
## 完了の基準
- 第2段階の確認で、新しい(あるいは修正した)テストが**本体の側の理由で失敗した**ことを確認している。
- 既存を含む**全件のテスト**が通過している。**スキップが増えていない。**
- 実装が、テストの具体的な入力値を特別扱いしていない。
- 観点の一覧に照らして、明らかな抜けがない。
- テスト側を修正した場合、**どの分類か**と、(C) なら**仕様の出典**、(D) なら**判定の記録**が残っている。
- 通過の報告に、実行したコマンドと件数が添えられている。
在 GitHub 查看