依存性の注入(DI)とは
依存性の注入(Dependency Injection、以下DI)とは、あるオブジェクトの依存関係を外部から注入するという設計パターン。
依存関係とはあるオブジェクトが自身の機能を成立させるために他のオブジェクトに依存しているという関係である。例えば以下のDIを利用しない例では、GreeterはDefaultGreetingProviderと依存関係にある。なぜならGreeterはDefaultGreetingProviderを保持しておりそれをgreet()内で利用していることから、GreeterはDefaultGreetingProviderが無くてはgreet()を成立させることができないからである。
DIを利用しない例
struct DefaultGreetingProvider {
func greeting() -> String {
return "Hello"
}
}
final class Greeter {
// DefaultGreetingProviderのインスタンスを直接生成、保持
private let provider = DefaultGreetingProvider()
func greet() -> String {
// DefaultGreetingProviderのメソッドを使用している
return provider.greeting()
}
}
なお、この例ではGreeterは依存関係を内部で直接生成しているが、これを以下のDIを利用する例のように、外部から受け取るようにした設計が依存性の注入である。
DIを利用する例
// 依存関係の抽象化のためにプロトコルを定義
protocol GreetingProvider {
func greeting() -> String
}
// GreetingProviderに準拠させる
struct DefaultGreetingProvider: GreetingProvider {
func greeting() -> String {
return "Hello"
}
}
final class Greeter {
// 具体的な型ではなく、GreetingProviderとして保持
// これにより、GreetingProviderに準拠していればどんな依存関係でも保持可能
private let provider: GreetingProvider
// 初期化時に依存関係を外部から引数として受け取っている
init(provider: GreetingProvider) {
self.provider = provider
}
func greet() -> String {
return provider.greeting()
}
}
外部から依存関係を受け取る際には、GreetingProviderという抽象的な型として受け取る。これにより、受け取る依存先はGreetingProviderに準拠していればなんでも良く具体的な型は問わないということになり、後述のテストのために依存関係を差し替えるという作業が可能になる。
DIを使うとなぜ単体テストがしやすくなるのか
DIを利用すると、あるオブジェクトが依存している他のオブジェクトを外部から与える設計になるため、テスト時に依存オブジェクトをテストを容易にするための専用オブジェクト(テストダブル)に差し替えることができる。
依存しているオブジェクトを差し替えられないと、テスト対象の振る舞いが依存オブジェクトの実装に影響されてしまい、不安定になったり、統合テスト的になってしまう。しかしDIを利用すれば依存先をテスト対象のみを検証するために都合の良い振る舞いをするテストダブルに差し替えることができるためテストが容易になる。
このことを以下のコードで考える。
DIを利用した単体テストの例
本番コード
protocol GreetFetching {
func fetchGreet() throws -> String
}
enum APIError: Error {
case randomFailure
}
struct APIClient: GreetFetching {
// 1/4の確率でErrorを投げるfetchGreet()
func fetchGreet() throws -> String {
let random = Int.random(in: 1...4)
if random == 1 {
throw APIError.randomFailure
}
return "Hello"
}
}
struct MessageService {
private let fetcher: GreetFetching
// 初期化時に依存先を引数で受け取る
// 本番ではAPIClient、テストではSuccessStubFetcherを抽象化して受け取れる
init(fetcher: GreetFetching) {
self.fetcher = fetcher
}
func getMessage() throws -> String {
// 本番ではAPIClientのfetchGreet()が実行され、
// テストではSuccessStubFetcherのfetchGreet()が実行される
let greet = try fetcher.fetchGreet()
return greet + " World"
}
}
テストコード
// 失敗しないfetchGreet()を持つスタブ
// スタブとはテストダブルの一つで、テストに都合の良い設定をするもの
struct SuccessStubFetcher: GreetFetching {
func fetchGreet() throws -> String {
// 今回のテストではfetchGreet()が成功することを前提とした検証を行うので、
// 必ず"Hello"が返される処理を実装
return "Hello"
}
}
func test_getMessage_outputsHelloWorld_whenFetchSucceeds() throws {
let stub = SuccessStubFetcher()
// スタブを注入する
let service = MessageService(fetcher: stub)
// スタブを渡したため、service.getMessage()は必ず"Hello World"を返す
let result = try service.getMessage()
XCTAssertEqual(result, "Hello World")
}
テストの内容
テスト対象:getMessage()
検証内容:fetchGreet()が成功したときに、getMessage()の返り値が"Hello World"になること。
解説
この例ではDIを利用し、MessageServiceの初期化時に依存関係を外部から受け取る事ができるようになっているため、依存先を本番ではAPIClient、テストではSuccessStubFetcher(テストダブル)にそれぞれ差し替えられる。
もし仮にこの例が依存関係を差し替えられない設計になっていた場合、上記のテストケースはうまく機能しない。なぜなら、差し替えられない場合テスト時にも本番用の依存先であるAPIClient.fetchGreet()が使われることになるが、このAPIClient.fetchGreet()は一定確率でErrorを投げるため、その場合はテストが成立せず失敗してしまう。つまり、実行する度に成功したり、失敗したりするという再現性のないテストケースになってしまうからである。
しかし、繰り返しになるがこの例では依存先をSuccessStubFetcherに差し替えることができる。このSuccessStubFetcher.fetchGreet()は必ず成功するように都合よく作られているため、このテストケースは確実に成功する。これにより、fetchGreet()が成功することを前提としている今回の検証を行うことができている。