2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

DI(依存性注入)とTDD:テスタブルな構造はどう生まれるか

Posted at

概要

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とは、
“依存を明示し、責務を最小化し、テスト可能な構造に導くための思考設計プロセスである。”

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?