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

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

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

DIとは

「依存していた部分を、外から注入すること」です。
猿でも分かる! Dependency Injection: 依存性の注入

本番のアプリでは通信をしてデータを取りに行くけど、テストの時はローカルにあるテストデータを使ったり、ダミーで作り出したデータの方がいいよね!ってときに、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のジェネリックなプロトコルの変数はなぜ作れないのか、コンパイル後の中間言語を見て考えた

って思ったら今度は、

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

とか

"Generic parameter 'T' could not be inferred

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

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

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

テストがない状態

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

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

UseCase
class UseCase {
    let repository = RepositoryImpl()

    func fetchString() -> String {
        let number = repository.fetchInt()
        let numberString = String(number)
        return numberString
    }
}
Repository
class RepositoryImpl {

    func fetchInt() -> Int {
        // 通信する
        let api = API()
        return api.fetch()
    }
}

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

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

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

カンタンDI

テストの下準備

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

TestRepsisotry
class TestRepsisotry {

    func fetchInt() -> Int {
        // mockを返す
        return 5
    }
}

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

injectメソッドの登場です。

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メソッドを書き換えます。

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に適切に変換できているかのテストを、通信せずに行えるわけです。

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を使います。

protocol Repository {
    func fetchInt() -> Int
}

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

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

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


『 Swift 』Article List