テスト初心者がswiftでカンタンにDIするならinjectメソッドを作ろう

テストがないプロジェクトにテスト慣れしてない人がテストを追加しようとすると、構造がTestableじゃないことに気付いて挫折します。（僕です）

今記事では、そんな人のためにお**手軽にDIを使ってテストをするためのカンタンなTips**を紹介します。

## DIとは

>「依存していた部分を、外から注入すること」です。
[猿でも分かる! Dependency Injection: 依存性の注入](https://qiita.com/hshimo/items/1136087e1c6e5c5b0d9f)

本番のアプリでは通信をしてデータを取りに行くけど、テストの時はローカルにあるテストデータを使ったり、ダミーで作り出したデータの方がいいよね！ってときに、Mockを注入するっていう感じの使い方をします。

## しかし抽象化がむずい

テストしやすくするためにDIを検討しますが、経験が浅いと実装が抽象的すぎて難しく、挫折します。

protocol、associateTypeを使っていい感じに抽象化して、Testクラスのmockを作ってみても、xcodeに 

```
protocol 'Fugable' can only be used as a generic constraint because it has Self or associated type requirements
```

って言われて、

**よしType Crasureや！**

> Type Crasure参考：[Swiftのジェネリックなプロトコルの変数はなぜ作れないのか、コンパイル後の中間言語を見て考えた](https://qiita.com/omochimetaru/items/b41e7699ea25a324aefa)

って思ったら今度は、

```
as a concrete type conforming to protocol '~' is not supported
```

とか

```
"Generic parameter 'T' could not be inferred
```

いう感じで型まわりでxcodeに怒られちゃうわけであります。

もちろん、ちゃんと設計すればTestableな抽象化ができると思いますが、抽象化するために時間を使いすぎてテストを書くための時間がなくなっては本末転倒では？と考えています。

ってことで、初心者にも理解しやすいであろう**お手軽DI**を紹介します。

## テストがない状態

テストがない状態で以下のようになっていたとします。

UseCaseの仕事は、**データを扱うRepositoryImplからIntを得て、それをStringに変換すること**という場合です。

```swift:UseCase
class UseCase {
    let repository = RepositoryImpl()
    
    func fetchString() -> String {
        let number = repository.fetchInt()
        let numberString = String(number)
        return numberString
    }
}
```

```swift:Repository
class RepositoryImpl {
    
    func fetchInt() -> Int {
        // 通信する
        let api = API()
        return api.fetch()
    }
}
```

ここで、**UseCaseがIntからStringに想定通りに変換しているかのテスト**を追加したいとします。

**しかし！！**、その時にいちいち`RepositoryImpl().fetchInt`してしまうと通信コストがかかるし、通信すること自体は今やりたいテストじゃないので、**通信しないでダミーのIntを返してくれる代わりのメソッドが欲しい**わけです。

そこでカンタンにDIしてみます。

## カンタンDI

### テストの下準備

もともとあったRepsisotryImplに対して、通信をしないでダミーのIntを返すメソッドを持ったmockを作ります。

```swift:TestRepsisotry
class TestRepsisotry {
    
    func fetchInt() -> Int {
        // mockを返す
        return 5
    }
}
```

次に、UseCaseに対してこのTestRepositoryをDI（注入）できるようにしてみましょう。

**injectメソッドの登場です。**

```swift:UseCase
class UseCase {
    let repository = RepositoryImpl()
    var testRepository: TestRepsisotry?
    
    func inject(testRepository: TestRepsisotry){
        self.testRepository = testRepository
    }
}
```

nilを許容する `var testRepository: TestRepsisotry?`という変数を用意して、

injectというメソッドで外からtestRepositoryを差し込めるようにします。

そのあとは、injectされた時だけrepositoryじゃなくてtestRepositoryを使うようにfetchStringメソッドを書き換えます。

```swift:UseCase
    func fetchString() -> String {
        let number: Int
        if let testRepository = testRepository {
            number = testRepository.fetchInt()
        } else {
            number = repository.fetchInt()
        }
        let numberString = String(number)
        return numberString
    }
```

これで準備ができました。testRepositoryがnilじゃない時だけrepositoryじゃなくてtestRepositoryを使います。

### テストコードを書く

useCaseのインスタンスを作った後に、テストじゃなければそのまま`fetchString`するところを、`useCase.inject`をしてtestRepositoryを差し込んでいます。

これを差し込むことで**IntをStringに適切に変換できているかのテスト**を、通信せずに行えるわけです。

```swift:UseCaseTest
class UseCaseTest {
    
    // UseCaseでIntを適切にStringに変換できているかのテスト
    func testFetch(){
        let testRepo = TestRepsisotry()
        let useCase = UseCase()
        useCase.inject(testRepository: testRepo)
        let result = useCase.fetchString()
        assert(result == "5")
    }
}
```

以上がカンタンDIの方法でした。

## なぜDIの時にprotocolを使うか

DIできる設計にしたい時にはprotocolを使います。

```swift
protocol Repository {
    func fetchInt() -> Int
}
```

こんな感じにして、RepositoryImplとTestRepsisotryはRepositoryに準拠させます。

```swift
class RepositoryImpl: Repository {
    
    func fetchInt() -> Int {
        // 通信する
        let api = API()
        return api.fetch()
    }
}

class TestRepsisotry: Repository {
    
    func fetchInt() -> Int {
        // mockを返す
        return 5
    }
}
```

こうすることで、メソッドの形が同じだけど中身が違うmockを作れるわけです。

## まとめ

以上、お手軽DIでした。

初心者がテストのためにmockを作る際には参考になるかもしれません。

今回記事にするためにplaygroundで作ったサンプルコードは以下です。https://gist.github.com/kboy-silvergym/c7496a0c0fa76d13dd008fda27f470f8

それではみなさん筋トレ頑張っていきましょう！