Red 🔴 - 失敗するテストの書き方
テスト駆動開発(TDD)の最初のステップは Red🔴
、つまり「失敗するテストを書くこと」から始まります。 これは、実装がまだ存在しないことを前提に、テストを先に記述することで、「設計をテストから導く」アプローチです。
良いテストの原則
良いテストを書くことはTDDの成功に不可欠であり、後続のステップの効果を最大化します。
以下、TDDのRedステップで活用できる効果的なテストの書き方と、その手法について記します。
FIRST原則
TDDにおける良いテストは、以下の「FIRST」原則に従うべきです。
原則 | 説明 |
---|---|
Fast(高速) | テストは可能な限り高速に実行できること |
Independent(独立) | テスト同士が互いに依存せず、どのような順序でも実行できること |
Repeatable(反復可能) | 環境に依存せず、何度実行しても同じ結果が得られること |
Self-validating(自己検証) | テストは成功か失敗かを自動的に判定できること |
Timely(適時) | 実装前にテストを書くこと |
明確な意図
テストは何をテストしているのかを明確に示すべきです。
テスト名は以下を示すと良いでしょう。
- 「何を」
- 「どのような条件で」
- 「どうなるべきか」
例
// 曖昧なテスト名
test('add関数', () => {...});
// 明確なテスト名
test('add関数に2と3を渡すと5を返す', () => {...});
テスト設計パターン
Given-When-Then パターン
振る舞い駆動開発(BDD)から来たこのパターンは、テストの構造を明確にします。
Given(前提条件)
: テストの初期状態、準備When(操作)
: テスト対象の機能を実行Then(期待結果)
: 期待される結果を検証
// Given-When-Thenパターンの例
test('ユーザーが有効な認証情報を提供するとログインに成功する', () => {
// Given:テストの準備
const user = new User('username', 'password');
const authService = new AuthService();
// When:テスト対象の機能を実行
const result = authService.login(user.username, user.password);
// Then:期待される結果を検証
expect(result.success).toBe(true);
expect(result.message).toBe('ログイン成功');
});
Arrange-Act-Assert パターン
Given-When-Thenと同様のパターンですが、より技術的な表現です。
Arrange(準備)
: テストの初期状態を設定Act(実行)
: テスト対象の機能を実行Assert(検証)
: 結果を検証
テストファーストのアプローチ
ボトムアップテスト
単純な機能や小さなコンポーネントから始めて、徐々に複雑な機能のテストへと進みます。
- 最小の機能単位(関数やメソッド)のテストから始める
- 複数の機能を組み合わせたテストへ進む
- 最終的に統合テストへ発展させる
明確な要件からのテスト導出
仕様や要件を元にテストケースを導き出す方法:
- 要件から具体的な例を特定する
- 例をテストケースに変換する
- エッジケースや例外ケースも考慮する
失敗するテストの書き方
意図的な失敗
TDDでは、まだ存在しない機能に対してテストを書くため、最初のテストは必ず失敗します。
良い失敗のパターンを知ることが重要です。
コンパイルエラー
: 関数やクラスがまだ存在しない実行時エラー
: 関数は存在するが、必要な処理が実装されていないアサーション失敗
: 関数は動作するが、期待した結果を返さない
一度に一つのテスト
TDDの重要な原則は「一度に一つのテスト」です。
- 複数の機能を一つのテストで検証しない
- 一つのテストは一つの概念だけをテスト
- 小さなステップで進めることで、問題の特定が容易になる
効果的なテストケース設計
境界値分析
機能の限界や境界条件をテストすることで、バグを見つけやすくなります。
- 最小値、最大値
- ゼロやnullの処理
- 空のコレクションや文字列
- エッジケース
同値分割
入力値を同じ振る舞いをする範囲(同値クラス)に分割し、各クラスから代表的な値をテストします。
- 有効な入力値のクラス
- 無効な入力値のクラス
- 特殊な処理が必要な値のクラス
モックとスタブの活用
依存関係の分離
テスト対象のコードが他のコンポーネントに依存している場合、モックやスタブを使って分離します。
スタブ
: 特定の入力に対して決まった出力を返す単純なオブジェクトモック
: 呼び出された回数や引数などの検証機能を持つより高度なオブジェクト
例
// モックを使ったテストの例
test('ユーザーサービスがデータベースから正しくユーザーを取得する', () => {
// モックデータベースを作成
const mockDatabase = {
findUserById: jest.fn().mockReturnValue({ id: 1, name: 'テストユーザー' })
};
// モックを使用するユーザーサービス
const userService = new UserService(mockDatabase);
// テスト実行
const user = userService.getUserById(1);
// 検証
expect(mockDatabase.findUserById).toHaveBeenCalledWith(1);
expect(user.name).toBe('テストユーザー');
});
外部依存の処理
ネットワーク、データベース、ファイルシステムなどの外部依存は、テストを遅くしたり不安定にしたりします。これらはモックに置き換えることで、テストを高速で信頼性の高いものにできます。
TDDにおけるテストとリファクタリングの関係
良いテストはリファクタリングを支援します。
- テストがあることで、安全にコードを改善できる
- 既存の機能を壊さないことを保証
- 設計の改善に自信を持てる
Redステップにおけるテスト設計の実践テクニック
TDDにおけるRedステップ(失敗するテストを書く)では、以下のような観点・テクニックが役立ちます。
手法 | 内容 | メリット | 使う場面 |
---|---|---|---|
仕様から逆算 | 要件・仕様から期待される振る舞いをテストに落とし込む | 実装前に仕様理解が深まる | 要件定義が明確な場合 |
仮名関数で先行実装 | 未定義の関数・クラスを恐れずに使ってテストを書く | テストが自然に設計を導く | 実装の構造が未定義なとき |
境界値・異常系先行 | 極端な入力や例外ケースのテストから始める | バグの予防、堅牢性向上 | 安全性や信頼性が重要な場面 |
仕様から逆算してテストを書く
- 要求仕様(ユーザー視点、プロダクトオーナー視点)をコード化する
- 「まずテストありき」で問題を定義
例
// 例:ログイン成功の仕様を先にテスト化
expect(auth.login("user", "pass")).toBe(true);
仮名関数で未定義のまま書く
- 実装がない状態でもまずテストを書けるように、未定義の関数やクラスを堂々と書く
- IDEがエラーを出すことを恐れず、テストが導く設計を意識
例
// 仮名関数を使用してテストを書く(未定義でもOK)
expect(calculateShipping("沖縄")).toBe(1200);
境界値や異常系から入る
- 正常系だけでなく、異常系・エラー・極端なケースから入るのも有効
- バグを予防するテスト設計としても有名
例
// 例:ゼロ除算の例外を期待するテスト
expect(() => divide(10, 0)).toThrow("除算エラー");
テスト(Red ステップ)での心得
- 失敗することを恐れずに書く
- 「どのように動いてほしいか」をテストで表現する
- 実装に引きずられず、仕様ベースの発想を意識する
- テストが意図的に失敗することで、Greenステップへの道筋が明確になる
チェックリスト:テストの自己評価
チェック項目 | 内容 |
---|---|
実装前にテストが書かれているか? | ✅ |
テストが失敗する理由が明確か? | ✅ |
仕様や要件と対応したテスト内容か? | ✅ |
曖昧な期待値や断定がないか? | ✅ |
境界値・異常系の観点があるか? | ✅ |
まとめ
TDDの最初のステップであるRedフェーズでは、適切なテストを書くことが重要です。
良いテストとは
- 明確で理解しやすい
- 一つの概念だけをテスト
- 高速で独立している
- 自己検証可能
- 適切なタイミングで書かれている
これらの原則と手法を適用することで、TDDプロセス全体の効果を最大化し、より堅牢で保守性の高いコードを作成することができます。