【Swift5】Timerのテストコードの備忘録

#はじめに
XCTestで[Timer](https://developer.apple.com/documentation/foundation/timer)のテストコードを書く機会があったので、備忘録として記載します。

##XCTestとは
>iPhoneの単体テスト用のフレームワークです。
Xcodeでプロジェクトを生成した時にデフォルトで生成されます。

引用：[iPhoneアプリ自動テスト入門 ~ XCTest+Swiftの単体テストを調べてみた](https://llcc.hatenablog.com/entry/2015/06/28/172353)

#環境
- Xcode 11.1
- Swift5

#①テストコードの書けるクラス(プロダクトコード側)のイニシャライザを作る
```.swift
final class TimerManager: NSObject {
        
    private let timerProvider: Timer.Type
    private var timer: Timer?
    
    init(timerProvider: Timer.Type = Timer.self) {
        self.timerProvider = timerProvider
    }
}
```
MockTimer(Timerのモック(テストに使うTimerの))側から使えるように、
イニシャライザの引数に```.Type```(メタタイプ)を設定します。

(https://dev.classmethod.jp/smartphone/iphone/swift-3-type-of/)
[Timer](https://developer.apple.com/documentation/foundation/timer)はクラスなので、```.Type```を設定する必要があるみたいです。
```Timer.self```で実際に値にアクセスしています。

>メタタイプはクラスやストラクチャ、列挙型、プロトコルが何であるか判別するための特別な型です
引用：[[Swift 3] 型名を取得する]

#②テストするメソッドを作る

今回は、Timerを使用した、簡単な２つの機能を実装しています。

①タイマーを削除するプライベートメソッド
②タイマーをメインスレッドで発火させるメソッド(10秒後に①を１度だけ呼ぶ)
追記：テスト時にタイマーが発火されない事象が発生したため、今回はメインスレッドで発火させています。

```.swift
import Foundation

final class TimerManager: NSObject {
    
    private let timerProvider: Timer.Type
    private var timer: Timer?
    
    init(timerProvider: Timer.Type = Timer.self) {
        self.timerProvider = timerProvider
    }
    
    /// タイマー削除
    @objc private func stopTimer() {
        self.timer?.invalidate()
        self.timer = nil
    }
    
    /// タイマー作成
    /// - Parameter flg: タイマー
    func scheduledTimer(flg: Bool) {
        if flg {
            // タイマーはメインスレッドで発火する
            DispatchQueue.main.async {
                self.timer = self.timerProvider.scheduledTimer(withTimeInterval: TimeInterval(10),
                                                                      repeats: false,
                                                                      block: { _ in self.stopTimer()
                })
            }
        }
    }
    
}
```

テストするメソッドに使用したメソッドは以下になります。

◆ [scheduledTimer(withTimeInterval:repeats:block:)](https://developer.apple.com/documentation/foundation/timer/2091889-scheduledtimer)・・・タイマーを作成するタイプメソッド。

```.swift
@interface NSTimer : NSObject
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));

```

◆ [invalidate()](https://developer.apple.com/documentation/foundation/timer/1415405-invalidate)・・・タイマーを削除するインスタンスメソッド。

```.swift
@interface NSTimer : NSObject
- (void)invalidate;
```

#③MockTimerクラスを作る

Timerを継承しているMockTimerクラスを作ります。

```.swift
import Foundation

class MockTimer: Timer {

    var block: ((Timer) -> Void)!
    static var timer: MockTimer?
    
    static var callCountInvalidate = 0
    static var callCountScheduledTimer = 0
    
    override func fire() {
        print("MockTimer発火")
        block(self)
    }
    
    override open class func scheduledTimer(withTimeInterval interval: TimeInterval,
                                            repeats: Bool,
                                            block: @escaping (Timer) -> Void) -> Timer {
        let mockTimer = MockTimer()
        mockTimer.block = block
        
        MockTimer.timer = mockTimer
        MockTimer.callCountScheduledTimer += 1
        print("ScheduledTimer呼ばれた回数:\(MockTimer.callCountScheduledTimer)")
        
        return mockTimer
    }
    
    override open func invalidate() {
        MockTimer.callCountInvalidate += 1
        print("Invalidateが呼ばれた回数:\(MockTimer.callCountInvalidate)")
    }
    
}
```

@escapeingを使用することで、受け取ったクロージャをメソッド内ですぐに実行せずに、
メソッドの外側で実行することができるようになるみたいです。

#④テストコードを書く

実際に書いたテストコードです。(これでテスト通りました。)

```.swift
import XCTest

class TimerManagerTests: XCTestCase {

    override func setUp() {
        super.setUp()
        // Put setup code here. This method is called before the invocation of each test method in the class.
    }

    override func tearDown() {
        super.tearDown()
        // Put teardown code here. This method is called after the invocation of each test method in the class.
    }

    func test_stopTimer() {
        // stopTimerを間接的に呼び出す
        let timerManager = TimerManager(timerProvider: MockTimer.self)
        timerManager.scheduledTimer(flg: true)
        
        let beforeCallCountInvalidate = MockTimer.callCountInvalidate
        
        let exp = expectation(description: "StopTimer")
        DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1) {
            MockTimer.timer?.fire()
            exp.fulfill()
        }
        wait(for: [exp], timeout: 0.1)
        
        _ = XCTContext.runActivity(named: "MockTimer.invalidate is called.", block: { _ in
            XCTAssertEqual(beforeCallCountInvalidate + 1, MockTimer.callCountInvalidate)
        })
        // 初期化する
        MockTimer.callCountInvalidate = 0
    }
    
    func test_scheduledTimer() {
        let beforeCallCountScheduledTimer = MockTimer.callCountScheduledTimer
        
        let timerManager = TimerManager(timerProvider: MockTimer.self)

        XCTContext.runActivity(named: "引数のflgがtrueの時", block: { _ in
            timerManager.scheduledTimer(flg: true)
            let exp = expectation(description: "ScheduledTimer Fire.")
            DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1) {
                MockTimer.timer?.fire()
                exp.fulfill()
            }
            wait(for: [exp], timeout: 0.1)
            
            _ = XCTContext.runActivity(named: "WearBluetoothService.stopTimer is called.", block: { _ in
                XCTAssertEqual(beforeCallCountScheduledTimer + 1, MockTimer.callCountScheduledTimer)
            })
            // 初期化する
            MockTimer.callCountScheduledTimer = 0
        })
        XCTContext.runActivity(named: "引数のflgがfalseの時", block: { _ in
            timerManager.scheduledTimer(flg: false)
            let exp = expectation(description: "ScheduledTimer Fire.")
            DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.1) {
                MockTimer.timer?.fire()
                exp.fulfill()
            }
            wait(for: [exp], timeout: 0.1)
            
            _ = XCTContext.runActivity(named: "WearBluetoothService.stopTimer is called.", block: { _ in
                XCTAssertEqual(0, MockTimer.callCountScheduledTimer)
            })
            // 初期化する
            MockTimer.callCountScheduledTimer = 0
        })
    }

}
```

今回は、タイマーをメインスレッドで使用していたため、
タイマーを発火するタイミングを考慮し、遅延処理周りには[wait(for:timeout:)](https://developer.apple.com/documentation/xctest/xctestcase/2806856-wait)を使用しました。

#補足(テストコード基本のき)
- [setUp()](https://developer.apple.com/documentation/xctest/xctest/1500341-setup)・・・各テストの開始前に呼ばれるメソッド。
- [tearDown()](https://developer.apple.com/documentation/xctest/xctest/1500463-teardown)・・・各テスト実行後に呼ばれるメソッド。

#まとめ
Timerのテストコードは、元々自力で書けなかったのもあり、
先輩に教わったのですが、私には雰囲気を理解(?)するのも難しかったです。
今回調べて、理解し切れていない部分もあったり、調べ切れなかった部分もあるので、
もう少し学習を進めたいと思います。

#参考
- [XCTest入門Part1 (Swift)](https://qiita.com/okuderap/items/97f9d6026d0da59fef38)
- [SwiftでClassを表す方法(.Type、.self)](http://nerd0geek1.hatenablog.com/entry/2016/01/31/200000)
- [Swift 3からのアクセスコントロール](https://qiita.com/hironytic/items/6cc68ca93b428ba9194e)
- [【Swift】@escaping属性のクロージャとは](https://qiita.com/chino_tweet/items/48564b0c23a64f539060)
- [Swiftでの遅延実行](https://qiita.com/itoru257/items/ee3150d2ba25aa013d7d)