SwiftでiOS開発している時に、Timerを利用したコードを、モダンでいい感じにUnitTestする方法

## 0. 初めに

画面タップイベントや、APIアクセスといった処理は、(ユーザーが連打するなどの場合を除けば)多くの場合一度きりの処理であることが多く、これらのロジックをUnitTestでテストするのは、そこまで大変ではないケースが多くを占めると思います。

しかしながら、一度きりではない処理を実装しないといけない(画面上にカウントダウンを表示させる場合など)ケースがあるのもまた事実かと思います。

いざTimerを使ったコードを書いてみると、実際に動かす場合はまぁよいにしろ、UnitTestを書くところで、

「どうやってテストしたらいいんだろう？ :thinking: まさか待つわけにはいかないしな...」

といった感じで、思った以上にTimer部分のテストを書くのが難しく、Timer部分のUnitTestをスキップしてしまうことがあったり・・・するのではないでしょうか😇😇😇

この記事では、そんなTimerのUnitTestが難しいなと思っている皆さんに向けて、僕はこんなふうにやったよという感じで、僕なりのテスト方法を提案しようと思います。

大まかに分けて下記のような流れで進めていきます。

1. `ApplicationTimerProviderProtocol` を作る
2. `Timer` そのものではなく、 `ApplicationTimerProviderProtocol` に依存するようにする(差し替え可能にする)
3. UnitTest用の `FakeApplcationiTimerProvider` を作る
4. 実際のテストを書いてみる(ここではQuick/Nimbleを利用したコードを載せます)

## 1. `ApplicationTimerProviderProtocol` を作る

UnitTestが書きやすいプログラムとは外部から差し替え可能なクラスに依存しているクラスである、と僕は思います。

Timerを使おう〜！と思って下記のようにTimerを使ってしまっても実際の動きとしては良いのですが、Testコードを書こうとすると、TimeInterval分待つしか無くなってしまい、綺麗なUnitTestが書けないことになります。 


```swift
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` への依存を外しましょう。

```swift
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` のコードを下記のように修正します、

```swift
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` を作成します。

```swift
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を書くことができるようになっていると思います。


```swift
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の書き方がわからず、悩んでおりました。

僕なりのテスト方法ではありますが皆様のお力になれれば幸いです。