LoginSignup
2
1

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-01-26

はじめに

XCTestでTimerのテストコードを書く機会があったので、備忘録として記載します。

XCTestとは

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

引用:iPhoneアプリ自動テスト入門 ~ XCTest+Swiftの単体テストを調べてみた

環境

  • Xcode 11.1
  • Swift5

①テストコードの書けるクラス(プロダクトコード側)のイニシャライザを作る

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はクラスなので、.Typeを設定する必要があるみたいです。
Timer.selfで実際に値にアクセスしています。

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

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

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

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

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:)・・・タイマーを作成するタイプメソッド。

@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()・・・タイマーを削除するインスタンスメソッド。

@interface NSTimer : NSObject
- (void)invalidate;

③MockTimerクラスを作る

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

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

④テストコードを書く

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

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:)を使用しました。

補足(テストコード基本のき)

  • setUp()・・・各テストの開始前に呼ばれるメソッド。
  • tearDown()・・・各テスト実行後に呼ばれるメソッド。

まとめ

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

参考

2
1
0

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
2
1