0. 初めに
画面タップイベントや、APIアクセスといった処理は、(ユーザーが連打するなどの場合を除けば)多くの場合一度きりの処理であることが多く、これらのロジックをUnitTestでテストするのは、そこまで大変ではないケースが多くを占めると思います。
しかしながら、一度きりではない処理を実装しないといけない(画面上にカウントダウンを表示させる場合など)ケースがあるのもまた事実かと思います。
いざTimerを使ったコードを書いてみると、実際に動かす場合はまぁよいにしろ、UnitTestを書くところで、
「どうやってテストしたらいいんだろう? まさか待つわけにはいかないしな...」
といった感じで、思った以上にTimer部分のテストを書くのが難しく、Timer部分のUnitTestをスキップしてしまうことがあったり・・・するのではないでしょうか😇😇😇
この記事では、そんなTimerのUnitTestが難しいなと思っている皆さんに向けて、僕はこんなふうにやったよという感じで、僕なりのテスト方法を提案しようと思います。
大まかに分けて下記のような流れで進めていきます。
-
ApplicationTimerProviderProtocol
を作る -
Timer
そのものではなく、ApplicationTimerProviderProtocol
に依存するようにする(差し替え可能にする) - UnitTest用の
FakeApplcationiTimerProvider
を作る - 実際のテストを書いてみる(ここではQuick/Nimbleを利用したコードを載せます)
1. ApplicationTimerProviderProtocol
を作る
UnitTestが書きやすいプログラムとは外部から差し替え可能なクラスに依存しているクラスである、と僕は思います。
Timerを使おう〜!と思って下記のようにTimerを使ってしまっても実際の動きとしては良いのですが、Testコードを書こうとすると、TimeInterval分待つしか無くなってしまい、綺麗なUnitTestが書けないことになります。
import Foundation
final class TimerClass {
var timer: Timer?
func check() {
timer = Timer.scheduledTimer(
withTimeInterval: 1.0,
repeats: false,
block: { timer in
// do something...
})
}
}
「実際にTimeInterval分待たなくてはならない」問題は、上記のプログラムが Timer
自体に依存しているため発生しています。
そのため、下記のような ApplicationTimerProviderProtocol
を作成し、 Timer
への依存を外しましょう。
protocol ApplicationTimerProviderProtocol {
func scheduledTimer(withTimeInterval interval: TimeInterval, repeats: Bool, block: @escaping (Timer) -> Void) -> Timer
}
struct ApplicationTimerProvider {
}
extension ApplicationTimerProvider: ApplicationTimerProviderProtocol {
func scheduledTimer(withTimeInterval interval: TimeInterval, repeats: Bool, block: @escaping (Timer) -> Void) -> Timer {
return Timer.scheduledTimer(withTimeInterval: interval, repeats: repeats, block: block)
}
}
2. ApplicationTimerProviderProtocol
に依存させる
作成した、 ApplicationTimerProviderProtocol
を利用することで、UnitTestの時に差し替えを行うことが可能になります。
UnitTestの時に差し替えを行うため、作成した ApplicationTimerProvider
を利用するように TimerClass
のコードを下記のように修正します、
final class TimerClass {
let timerProvider: ApplicationTimerProviderProtocol
var timer: Timer?
init(timerProvider: ApplicationTimerProviderProtocol) {
self.timerProvider = timerProvider
}
func check() {
timer = Timer.scheduledTimer(
withTimeInterval: 1.0,
repeats: false,
block: { timer in
// do something...
})
}
}
3. UnitTest用の FakeApplcationiTimerProvider
を作る
下記のようにUnitTestの際に利用する FakeApplicationTimerProvider
を作成します。
class FakeApplcationiTimerProvider: ApplcationiTimerProviderProtocol {
final class DummyTimer: Timer {
override func invalidate() { }
}
var blocks = [(Timer) -> Void]()
var scheduledTimer_callCount = 0
func scheduledTimer(withTimeInterval interval: TimeInterval, repeats: Bool, block: @escaping (Timer) -> Void) -> Timer {
scheduledTimer_callCount += 1
blocks.append(block)
return DummyTimer()
}
var fire_callCount = 0
func fire() {
fire_callCount += 1
guard let first = blocks.first else { return }
first(DummyTimer())
_ = blocks.removeFirst()
}
}
4. 実際のテストを書いてみる
ここまでくれば下記のように、UnitTestを書くことができるようになっていると思います。
class TimerClassSpec: QuickSpec {
override func spec() {
var fakeTimerProvider: FakeApplcationiTimerProvider!
var subject: TimerClass!
beforeEach {
fakeTimerProvider = FakeOrderAppliTimerProvider()
subject = TimerClass(timerProvider: fakeTimerProvider)
}
it("check") {
subject.check()
fakeTimerProvider.fire()
// ここまでくればクロージャーが発火しているのでテストをすることができる
}
}
}
終わりに
Timerを利用したことは何度かあったのですが、上手なUnitTestの書き方がわからず、悩んでおりました。
僕なりのテスト方法ではありますが皆様のお力になれれば幸いです。