テストがないプロジェクトにテスト慣れしてない人がテストを追加しようとすると、構造が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に変換することという場合です。
class UseCase {
let repository = RepositoryImpl()
func fetchString() -> String {
let number = repository.fetchInt()
let numberString = String(number)
return numberString
}
}
class RepositoryImpl {
func fetchInt() -> Int {
// 通信する
let api = API()
return api.fetch()
}
}
ここで、UseCaseがIntからStringに想定通りに変換しているかのテストを追加したいとします。
しかし!!、その時にいちいちRepositoryImpl().fetchInt
してしまうと通信コストがかかるし、通信すること自体は今やりたいテストじゃないので、通信しないでダミーのIntを返してくれる代わりのメソッドが欲しいわけです。
そこでカンタンにDIしてみます。
カンタンDI
テストの下準備
もともとあったRepsisotryImplに対して、通信をしないでダミーのIntを返すメソッドを持ったmockを作ります。
class TestRepsisotry {
func fetchInt() -> Int {
// mockを返す
return 5
}
}
次に、UseCaseに対してこのTestRepositoryをDI(注入)できるようにしてみましょう。
injectメソッドの登場です。
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メソッドを書き換えます。
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に適切に変換できているかのテストを、通信せずに行えるわけです。
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
それではみなさん筋トレ頑張っていきましょう!