概要
TDD(Test-Driven Development)を実践していく中で、
必ず直面するのが「テストしづらいコード」である。
その多くは「依存が暗黙的に組み込まれていること」が原因だ。
本稿では、依存性注入(Dependency Injection)を通じて、TDDに適した構造をどう設計するかを、
Kotlin + JUnit5 + Mockito をベースに徹底解説する。
1. テスタブルなコードには「依存の明示」が必要
❌ テストが難しいコードの典型例
class UserService {
fun registerUser(email: String) {
val mailer = Mailer()
mailer.send(email)
}
}
-
Mailer
が内部で直接 new されているため、テスト時に差し替え不能 - モックもDIも適用できないため、構造自体が閉じている
✅ TDDでは依存を外部から注入することで設計を開く
class UserService(private val mailer: Mailer) {
fun registerUser(email: String) {
mailer.send(email)
}
}
- 依存(Mailer)はコンストラクタで注入
- テスト時にモックを差し込むだけで振る舞いを検証可能
2. 実装とテストの分離:TDD視点での構造構築
インターフェースを使った依存の抽象化
interface Mailer {
fun send(to: String)
}
class SmtpMailer : Mailer {
override fun send(to: String) {
println("Sending mail to $to")
}
}
本体クラス(テスト対象)
class UserService(private val mailer: Mailer) {
fun registerUser(email: String): Boolean {
mailer.send(email)
return true
}
}
テストコード(Mockitoでモック化)
class UserServiceTest {
private val mailer = mock(Mailer::class.java)
private val service = UserService(mailer)
@Test
fun `should send mail on register`() {
val email = "test@example.com"
val result = service.registerUser(email)
assertTrue(result)
verify(mailer).send(email)
}
}
- ✅ 依存の抽象化により、テストが局所的に・高速に・確実に行える
- ✅ 実装を変えずに、テストの検証範囲を意図的に限定可能
3. TDDがDIを要求する理由
TDDの要件 | DIによる設計改善 |
---|---|
単体で検証可能な構造 | 依存を注入して制御可能な構造にする |
副作用の隔離 | 実装依存を**抽象(インターフェース)**に置換 |
曖昧な責務の可視化 | コンストラクタで依存を明示 |
モックの適用性 | DIにより差し替えが容易になる |
4. 設計パターンとしてのDI
コンストラクタインジェクション(最推奨)
class MyService(private val repository: UserRepository)
- ✅ 不変性の担保、明示性、テスト容易性すべてを備える
フィールドインジェクション(柔軟だが非推奨)
lateinit var repository: UserRepository
- ❌ 明示的でなく、テスト時の副作用の混入リスクが高い
メソッドインジェクション(限定された場面で有効)
fun process(repo: UserRepository) {
repo.doSomething()
}
- ✅ 短命な依存や一時的な差し替えに使える
設計判断フロー
① 依存する外部オブジェクトがあるか? → YES → インターフェース抽出
② テストで差し替えたいか? → YES → コンストラクタ or メソッドで注入
③ 実装に依存せず、契約だけで使いたいか? → YES → 抽象型を渡す
④ DIフレームワークを使うか? → 小規模では不要、明示的な設計で代替可
よくあるミスと対策
❌ newやstaticメソッドがテスト対象に直書きされている
→ ✅ 外部依存を注入可能な構造に変更する
❌ モックがうまく機能せず、テストが壊れる
→ ✅ インターフェース設計が甘い or 責務が混在しているサイン
❌ DIを使うが、何に注入すべきか分からない
→ ✅ 変化する or 副作用を持つ依存が対象。純粋な関数は不要
結語
TDDにおけるDIとは「テストのための小技」ではない。
それは**“構造を開き、依存を制御し、責務を明示するための設計戦略”**である。
- 依存は「隠す」のではなく「渡す」ことで設計は透明になる
- テストしやすさは、設計が分離できているかのリトマス試験紙
- DIは柔軟性ではなく、明示性と制御性をもたらす設計構造
TDDとは、
“依存を明示し、責務を最小化し、テスト可能な構造に導くための思考設計プロセスである。”