LoginSignup
5
2

More than 3 years have passed since last update.

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

Posted at

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が書けないことになります。

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

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

5
2
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
2