はじめに
TDD(テスト駆動開発)を実践していると、「どこからテストを書き始めればよいのか」という疑問に直面することがあります。小さな関数から始めるべきか、それともシステム全体の振る舞いから始めるべきか。この問いに対する答えの一つがOutside-In TDDです。
Outside-In TDDは、ユーザーに最も近い「外側」から開発を始め、段階的に内部の実装へと進んでいくアプローチです。受け入れテスト(Acceptance Test)を起点として、必要なコンポーネントを「発見」しながら設計と実装を同時に進めていきます。
本記事では、Outside-In TDDの基本概念から実践的なワークフロー、そしてInside-Out TDDとの比較まで、体系的に解説します。この記事を読み終える頃には、要件駆動のテスト設計と、モックを活用した効果的な開発アプローチを習得できているでしょう。
Outside-In TDDとは
Outside-In TDDは、システムの「外側」つまりユーザーインターフェースやAPIエンドポイントからテストを開始し、内部の実装へと段階的に進んでいく開発アプローチです。London School TDDまたはMockist TDDとも呼ばれます。
基本的な考え方
Outside-In TDDでは、以下の順序で開発を進めます。
- ユーザーストーリーや要件から始める
- 受け入れテスト(外側のテスト)を書く
- そのテストを通すために必要なコンポーネントを「発見」する
- 発見したコンポーネントに対する単体テストを書く
- 実装を完成させ、すべてのテストをパスさせる
graph TD
subgraph "Outside(外側)"
A[受け入れテスト]
B[コントローラー/API]
end
subgraph "Middle(中間層)"
C[サービス層]
D[ビジネスロジック]
end
subgraph "Inside(内側)"
E[リポジトリ]
F[外部API連携]
end
A --> B
B --> C
C --> D
D --> E
D --> F
style A fill:#e3f2fd,stroke:#1565c0,color:#000000
style B fill:#e3f2fd,stroke:#1565c0,color:#000000
style C fill:#fff3e0,stroke:#e65100,color:#000000
style D fill:#fff3e0,stroke:#e65100,color:#000000
style E fill:#e8f5e9,stroke:#2e7d32,color:#000000
style F fill:#e8f5e9,stroke:#2e7d32,color:#000000このアプローチの核心は、実装の詳細を考える前に、「何が必要か」を明確にすることにあります。
London School と Detroit School
TDDのアプローチには大きく分けて2つの流派があります。
| 観点 | London School(Outside-In) | Detroit School(Inside-Out) |
|---|---|---|
| 起点 | ユーザーストーリー/受け入れテスト | ドメインモデル/単体テスト |
| 進行方向 | 外側から内側へ | 内側から外側へ |
| テストダブル | 積極的に使用 | 最小限に抑える |
| 設計へのアプローチ | 発見的(Emergent) | ボトムアップ |
| 検証方法 | 振る舞い検証(Behavior) | 状態検証(State) |
London Schoolの名前は、ロンドンのXP(エクストリーム・プログラミング)コミュニティで発展したことに由来します。一方、Detroit Schoolは、XPが最初に実践されたデトロイト(クライスラーのC3プロジェクト)にちなんで名付けられました。
Double Loop TDD
Outside-In TDDの実践において中心的な概念がDouble Loop TDDです。これは、外側のループ(受け入れテスト)と内側のループ(単体テスト)の2つのフィードバックループを使い分ける開発手法です。
Double Loopの構造
graph LR
subgraph "外側のループ(Acceptance Test)"
A1[Red: 受け入れテスト失敗] --> A2[機能実装を開始]
A2 --> A3[Green: 受け入れテスト成功]
A3 --> A4[Refactor: 全体の改善]
A4 --> A1
end
subgraph "内側のループ(Unit Test)"
U1[Red: 単体テスト失敗] --> U2[最小限の実装]
U2 --> U3[Green: 単体テスト成功]
U3 --> U4[Refactor: コード改善]
U4 --> U1
end
A2 -.-> U1
U3 -.-> A3
style A1 fill:#ffcdd2,stroke:#c62828,color:#000000
style A3 fill:#c8e6c9,stroke:#2e7d32,color:#000000
style A4 fill:#e1bee7,stroke:#7b1fa2,color:#000000
style U1 fill:#ffcdd2,stroke:#c62828,color:#000000
style U3 fill:#c8e6c9,stroke:#2e7d32,color:#000000
style U4 fill:#e1bee7,stroke:#7b1fa2,color:#000000外側のループ(Outer Loop)
外側のループでは、ユーザーの視点からシステム全体の振る舞いを検証します。
- 目的: ユーザーストーリーの完了を確認
- スコープ: エンドツーエンドまたは統合レベル
- 頻度: 機能単位で1回
- 実行時間: 比較的長い
内側のループ(Inner Loop)
内側のループでは、個々のコンポーネントの振る舞いを検証します。
- 目的: 各コンポーネントの正確性を確認
- スコープ: 単体テストレベル
- 頻度: 実装の各ステップで複数回
- 実行時間: 非常に短い
Double Loopの流れ
実際の開発では、以下のような流れになります。
- 受け入れテストを書く(外側のループ: Red)
- 受け入れテストは失敗する
- 最初のコンポーネントの単体テストを書く(内側のループ: Red)
- 単体テストをパスさせる最小限の実装(内側のループ: Green)
- 必要に応じてリファクタリング(内側のループ: Refactor)
- 次のコンポーネントへ進む(内側のループを繰り返す)
- すべてのコンポーネントが完成し、受け入れテストがパスする(外側のループ: Green)
- 全体をリファクタリング(外側のループ: Refactor)
Outside-In TDDの実践
ここからは、具体的なコード例を使ってOutside-In TDDのワークフローを体験していきましょう。題材として、ユーザー登録機能を実装します。
ユーザーストーリー
まず、実装する機能のユーザーストーリーを明確にします。
|
|
ステップ1: 受け入れテストを書く(外側のループ)
まず、機能全体の振る舞いを検証する受け入れテストを書きます。
|
|
このテストは当然失敗します。まだ何も実装していないからです。これが外側のループの「Red」状態です。
ステップ2: コントローラーの設計と単体テスト
受け入れテストを通すために、まずコントローラー(エントリーポイント)から設計を始めます。Outside-In TDDでは、**まだ存在しない依存オブジェクトを「発見」**しながら進めます。
|
|
ここで重要なのは、UserServiceというまだ存在しないクラスを「発見」していることです。コントローラーは「何をしたいか」だけを知っており、「どうやるか」はUserServiceに委譲します。
ステップ3: コントローラーの実装
単体テストをパスさせる最小限の実装を書きます。
|
|
ステップ4: サービス層の設計と単体テスト
次に、コントローラーから「発見」されたUserServiceの設計に進みます。
|
|
ここでまた新たな依存オブジェクトが「発見」されました。
UserRepository: ユーザーの永続化を担当EmailService: メール送信を担当PasswordHasher: パスワードのハッシュ化を担当
ステップ5: サービス層の実装
|
|
ステップ6: 内側のコンポーネントへ
同様のプロセスで、UserRepository、EmailService、PasswordHasherの各コンポーネントを実装していきます。これらは「内側」のコンポーネントであり、最終的には実際のデータベースや外部サービスと連携します。
graph TD
AT[受け入れテスト] --> UC[UserController]
UC --> US[UserService]
US --> UR[UserRepository]
US --> ES[EmailService]
US --> PH[PasswordHasher]
UR --> DB[(データベース)]
ES --> SMTP[SMTPサーバー]
style AT fill:#e3f2fd,stroke:#1565c0,color:#000000
style UC fill:#e3f2fd,stroke:#1565c0,color:#000000
style US fill:#fff3e0,stroke:#e65100,color:#000000
style UR fill:#e8f5e9,stroke:#2e7d32,color:#000000
style ES fill:#e8f5e9,stroke:#2e7d32,color:#000000
style PH fill:#e8f5e9,stroke:#2e7d32,color:#000000Outside-In TDDのメリット
Outside-In TDDには、従来のTDDアプローチと比較して以下のようなメリットがあります。
ユーザー視点を常に意識できる
受け入れテストから開始することで、実装の詳細に埋没することなく、常にユーザーにとっての価値を意識した開発が可能になります。
必要なコンポーネントを「発見」できる
実装を始める前にすべての設計を決める必要がありません。テストを書く過程で、自然と必要なインターフェースや責務の分離が明確になります。
APIファーストの設計
依存オブジェクトのインターフェースを、実装前に定義できます。これにより、使いやすいAPIを持つコンポーネントが自然と生まれます。
過剰設計の防止
「必要になったら作る」というアプローチにより、YAGNI(You Aren’t Gonna Need It)の原則に沿った開発ができます。
テストの独立性
モックを活用することで、各コンポーネントのテストを完全に独立させることができます。これにより、テストの実行速度が向上し、障害の特定も容易になります。
Outside-In TDDのデメリットと注意点
一方で、Outside-In TDDには注意すべき点もあります。
モックの過剰使用リスク
モックを多用することで、テストが実装の詳細に密結合してしまう可能性があります。リファクタリング時にテストが壊れやすくなることがあります。
|
|
統合テストの重要性
単体テストだけでは、コンポーネント間の連携が正しく動作するかを検証できません。Outside-In TDDでは、受け入れテストや統合テストが特に重要になります。
学習コスト
モックやスタブの効果的な使い方を習得するには、ある程度の学習と経験が必要です。
Inside-Out TDDとの比較
Outside-In TDDを深く理解するために、Inside-Out TDD(Classic TDD / Detroit School)との比較を見てみましょう。
開発の進め方の違い
graph LR
subgraph "Outside-In TDD"
O1[受け入れテスト] --> O2[コントローラー]
O2 --> O3[サービス]
O3 --> O4[リポジトリ]
end
subgraph "Inside-Out TDD"
I4[ドメインモデル] --> I3[リポジトリ]
I3 --> I2[サービス]
I2 --> I1[コントローラー]
end
style O1 fill:#e3f2fd,stroke:#1565c0,color:#000000
style I4 fill:#e8f5e9,stroke:#2e7d32,color:#000000使い分けの指針
| シナリオ | 推奨アプローチ |
|---|---|
| 要件が明確で、ユーザーストーリーが具体的 | Outside-In |
| ドメインロジックが複雑 | Inside-Out |
| チームにTDD経験者が少ない | Inside-Out |
| マイクロサービスやAPIの開発 | Outside-In |
| 既存システムへの機能追加 | 状況に応じて選択 |
併用するアプローチ
実際の開発では、両方のアプローチを組み合わせることが効果的な場合もあります。
- Outside-Inで全体の構造を設計
- Inside-Outでドメインロジックを実装
- Outside-Inで統合とAPIの調整
実践のためのヒント
Outside-In TDDを効果的に実践するためのヒントをいくつか紹介します。
テストリストを作成する
開発を始める前に、実装が必要なテストケースをリストアップしておきましょう。
|
|
適切な粒度のモックを使う
モックは「協調オブジェクト」に対して使い、純粋な値オブジェクトには使わないようにしましょう。
|
|
コントラクトテストを追加する
モックとの整合性を保つため、統合テストやコントラクトテストを追加しておきましょう。
まとめ
Outside-In TDDは、ユーザー視点から開発を進めることで、価値のある機能を確実に実装するためのアプローチです。主なポイントを振り返りましょう。
- Outside-In TDDは外側(ユーザーインターフェース)から内側(ドメイン)へ開発を進める
- Double Loop TDDにより、受け入れテストと単体テストの2つのフィードバックループを活用する
- モックを活用して依存オブジェクトを「発見」し、設計を進める
- Inside-Out TDDと適切に使い分けることで、より効果的な開発が可能
Outside-In TDDは万能ではありませんが、特にAPIやマイクロサービスの開発、明確なユーザーストーリーがある場合に非常に効果的です。まずは小さな機能から試して、チームに合ったスタイルを見つけていきましょう。
参考リンク
- Growing Object-Oriented Software, Guided by Tests - Steve Freeman, Nat Pryce著(Outside-In TDDの原典)
- Martin Fowler: Mocks Aren’t Stubs - ClassicistとMockistの違いを解説
- Test Double Blog: The failures of introducing test driven development - TDD教育の課題と解決策
- 依存性注入(DI)とテスタビリティ - DIの基本とテスト可能な設計
- モック・スタブの使い方完全ガイド - テストダブルの種類と実践パターン