他のタイプと違い、インスタンスを作る時イニシャライザよりクラスメソッド(scheduledTimer)の方がよく使われ、作られたインスタンスからrepeatsであるかどうか確認できませんので、テストにはちょっと変わった方法が必要です。

間隔ごとにTimerが発火することをテストしにくいし、厳格にいえば不可能(永遠にテストが続ける)ですので、テストすべきなのはTimerはどういう引数で作られ、fire()invalidate()が実行されることです。

そのためTimerをモックする必要があります。Swiftにおいてモックといえば、サブクラスかプロトコルかになります。

サブクラス

ありがたいことにscheduledTimerが全てクラスメソッドでオーバーライドすることが可能ですし、Timerがclass clusters1でイニシャライザをオーバーライドするのはできないですが、NSObjectのおかげで引数なしのイニシャライザがありますので、イニシャライザまでモックする必要がありません。

モッククラスを作るには大体下記の形になります。

class MockTimer: Timer {
    override class func scheduledTimer(withTimeInterval interval: TimeInterval, repeats: Bool, block: @escaping (Timer) -> Void) -> Timer {
        MockTimer()
    }

    var fireCalled = false
    override func fire() {
        fireCalled = true
    }

    var invalidateCalled = false
    override func invalidate() {
        invalidateCalled = true
    }
}

必要に応じて他のscheduledTimerをオーバーライドしたり、fire()の実行回数をカウントしたりします。しかしTimerがclass clusters1のせいで、オーバーライドしていないものは全部使えません。

Timerを使う側にインジェクトできるようにします。

class SomeClass {
    private let timerType: Timer.Type
    init(timerType: Timer.Type = Timer.self) {
        self.timerType = timerType
    }
    ...
    func someMethod() {
        let timer = timerType.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
            ...
        }
        time.fire()
    }
    ...
}

普通にはこれでテスト書けるようになりますが、Timerのインスタンスが都度作られますので、それをキャッチしてテストする必要があります。

クラスメソッドですので、スタティックメンバーでキャッチしましょう。

class MockTimer {
    static var scheduledTimer: MockTimer?
    static var timerScheduledBy: (interval: TimeInterval, repeats: Bool, block: (Timer) -> Void)?

    override class func scheduledTimer(withTimeInterval interval: TimeInterval, repeats: Bool, block: @escaping (Timer) -> Void) -> Timer {
        let scheduledTimer = MockTimer()
        self.scheduledTimer = scheduledTimer
        self.timerScheduledBy = (interval, repeats, block)
        return scheduledTimer
    }
    ...
}

もっとリアルタイムにチャックする必要があれば、MockTimer?などを((MockTimer) -> Void)?などに変更すれば十分だと思います。

テストの一例:

override func tearDown(){
    MockTimer.scheduledTimer = nil
    MockTimer.timerScheduledBy = nil
}

func testSomeMethod() {
    let someClass = SomeClass(timerType: MockTimer.self)
    someClass.someMethod()

    let timer = MockTimer.scheduledTimer
    let (interval, repeats, _) = MockTimer.timerScheduledBy
    XCTAssertEqual(interval, 1)
    XCTAssertTrue(repeats)
    XCTAssertTrue(timer.fireCalled)
}

プロトコル

使う予定あるTimerのプロパティやメソッドを参照し、プロトコルを作ります。

protocol TimerProtocol {
    func fire()
    func invalidate()
    ...
}

extension Timer: TimerProtocol {}

scheduledTimerの方も同じようにしてみます。

protocol TimerScheduler {
    static func scheduledTimer(withTimeInterval interval: TimeInterval, repeats: Bool, block: @escaping (Self) -> Void) -> Self
}

残念ながらクラスメソッドはSelfの制約を満たすことができません。Timerのままの場合TimerProtocolを返すことができなくなります。

仕方ありませんので、ラッピングしてあげましょう。

protocol TimerScheduler {
    static func scheduledTimer(withTimeInterval interval: TimeInterval, repeats: Bool, block: @escaping (TimerProtocol) -> Void) -> TimerProtocol
}

extension Timer: TimerScheduler {
    static func scheduledTimer(withTimeInterval interval: TimeInterval, repeats: Bool, block: @escaping (TimerProtocol) -> Void) -> TimerProtocol {
        let timer: Timer = scheduledTimer(withTimeInterval: interval, repeats: repeats, block: block)
        return timer
    }
}

この上でモックのタイプを作れば2テスト可能になります。テストコードはサブクラスの方とあまり変わりませんので省略します。

モックの問題

上記の方法で確かにTimerを使うコードがテストできるようになりますが、場合により一つの前提が必要です。それはTimer内部の動きが把握できていることです。

例えばテストするコードはisValidにより違うことをする場合、invalidate()が実行されましたら、isValidの値をfalseにする必要があります。そして例え把握していても、その通りに実装できていなければ意味がありません。また、ほぼあり得ない話ですが、アップルがTimerの実装が変えたら、テストは無効になります。

そのため、実体を使うようにテストするのもありかと思います。

var blockCalled = false
let timer = Timer(timeInterval: 1, repeats: true, block: { _ in
    blockCalled = true
})

このようなTimerをモックしたscheduledTimerに返してあげることで、Timerのままでテストできます。RunLoopにスケジュールしていませんので、fire()しない限りにはblockは実行されません。

とはいえTimerの仕様上こういうことができるだけで、普通の場合モックでテストすれば十分だと思います。

まとめ

ちょっと変わった方法が必要とはいえ、モックの基本手法ーサブクラスとプロトコルで解決できます。

モックの問題について、Timerはそこまで複雑ではありまんので、問題ないでしょうが、Timerのようにあるメソッド(invalidate())他のアウトプット(isValid)に変化があり、かつロジックは複雑な場合、モックしない方が無難かもしれません。


  1. Class Clusters  2

  2. 自分で作るよりMockingbirdCuckooのようなモックフレームワークに任せましょう。(Cuckooはstaticをサポートしていないため、ラッピングの形を変える必要がある)