【问题标题】:How Can I Unit Test Swift Timer Controller?如何对 Swift 计时器控制器进行单元测试?
【发布时间】:2017-01-20 03:49:58
【问题描述】:

我正在开发一个使用 Swift 的 Timer 类的项目。我的TimerController 类将通过启动、暂停、恢复和重置来控制Timer 实例。

TimerController由以下代码组成:

internal final class TimerController {

    // MARK: - Properties

    private var timer = Timer()
    private let timerIntervalInSeconds = TimeInterval(1)
    internal private(set) var durationInSeconds: TimeInterval

    // MARK: - Initialization

    internal init(seconds: Double) {
        durationInSeconds = TimeInterval(seconds)
    }

    // MARK: - Timer Control

    // Starts and resumes the timer
    internal func startTimer() {
        timer = Timer.scheduledTimer(timeInterval: timerIntervalInSeconds, target: self, selector: #selector(handleTimerFire), userInfo: nil, repeats: true)
    }

    internal func pauseTimer() {
        invalidateTimer()
    }

    internal func resetTimer() {
        invalidateTimer()
        durationInSeconds = 0
    }

    // MARK: - Helpers

    @objc private func handleTimerFire() {
        durationInSeconds += 1
    }

    private func invalidateTimer() {
        timer.invalidate()
    }

}

目前,我的TimerControllerTests 包含以下代码:

class TimerControllerTests: XCTestCase {

    func test_TimerController_DurationInSeconds_IsSet() {
        let expected: TimeInterval = 60
        let controller = TimerController(seconds: 60)
        XCTAssertEqual(controller.durationInSeconds, expected, "'durationInSeconds' is not set to correct value.")
    }

}

在初始化TimerController 的实例时,我能够测试计时器的预期持续时间设置是否正确。但是,我不知道从哪里开始测试TimerController 的其余部分。

我想确保该类成功处理startTimer()pauseTimer()resetTimer()。我希望我的单元测试尽可能快地运行,但我认为我需要实际启动、暂停和停止计时器以测试在调用适当的方法后是否更新了 durationInSeconds 属性。

TimerController 中实际创建计时器并调用我的单元测试中的方法来验证durationInSeconds 是否已正确更新是否合适?

我意识到它会减慢我的单元测试速度,但我不知道另一种方法可以适当地测试这个类以及它的预期操作。

更新

我一直在做一些研究,并且我发现了我认为的解决方案,就我的测试而言似乎可以完成工作。但是,我不确定这种实现是否足够。

我重新实现了我的TimerController,如下所示:

internal final class TimerController {

    // MARK: - Properties

    private var timer = Timer()
    private let timerIntervalInSeconds = TimeInterval(1)
    internal private(set) var durationInSeconds: TimeInterval
    internal var isTimerValid: Bool {
        return timer.isValid
    }

    // MARK: - Initialization

    internal init(seconds: Double) {
        durationInSeconds = TimeInterval(seconds)
    }

    // MARK: - Timer Control

    internal func startTimer(fireCompletion: (() -> Void)?) {
        timer = Timer.scheduledTimer(withTimeInterval: timerIntervalInSeconds, repeats: true, block: { [unowned self] _ in
            self.durationInSeconds -= 1
            fireCompletion?()
        })
    }

    internal func pauseTimer() {
        invalidateTimer()
    }

    internal func resetTimer() {
        invalidateTimer()
        durationInSeconds = 0
    }

    // MARK: - Helpers

    private func invalidateTimer() {
        timer.invalidate()
    }

}

另外,我的测试文件通过了测试:

class TimerControllerTests: XCTestCase {

    // MARK: - Properties

    var timerController: TimerController!

    // MARK: - Setup

    override func setUp() {
        timerController = TimerController(seconds: 1)
    }

    // MARK: - Teardown

    override func tearDown() {
        timerController.resetTimer()
        super.tearDown()
    }

    // MARK: - Time

    func test_TimerController_DurationInSeconds_IsSet() {
        let expected: TimeInterval = 60
        let timerController = TimerController(seconds: 60)
        XCTAssertEqual(timerController.durationInSeconds, expected, "'durationInSeconds' is not set to correct value.")
    }

    func test_TimerController_DurationInSeconds_IsZeroAfterTimerIsFinished() {
        let numberOfSeconds: TimeInterval = 1
        let durationExpectation = expectation(description: "durationExpectation")
        timerController = TimerController(seconds: numberOfSeconds)
        timerController.startTimer(fireCompletion: nil)
        DispatchQueue.main.asyncAfter(deadline: .now() + numberOfSeconds, execute: {
            durationExpectation.fulfill()
            XCTAssertEqual(0, self.timerController.durationInSeconds, "'durationInSeconds' is not set to correct value.")
        })
        waitForExpectations(timeout: numberOfSeconds + 1, handler: nil)
    }

    // MARK: - Timer State

    func test_TimerController_TimerIsValidAfterTimerStarts() {
        let timerValidityExpectation = expectation(description: "timerValidity")
        timerController.startTimer {
            timerValidityExpectation.fulfill()
            XCTAssertTrue(self.timerController.isTimerValid, "Timer is invalid.")
        }
        waitForExpectations(timeout: 5, handler: nil)
    }

    func test_TimerController_TimerIsInvalidAfterTimerIsPaused() {
        let timerValidityExpectation = expectation(description: "timerValidity")
        timerController.startTimer {
            self.timerController.pauseTimer()
            timerValidityExpectation.fulfill()
            XCTAssertFalse(self.timerController.isTimerValid, "Timer is valid")
        }
        waitForExpectations(timeout: 5, handler: nil)
    }

    func test_TimerController_TimerIsInvalidAfterTimerIsReset() {
        let timerValidityExpectation = expectation(description: "timerValidity")
        timerController.startTimer {
            self.timerController.resetTimer()
            timerValidityExpectation.fulfill()
            XCTAssertFalse(self.timerController.isTimerValid, "Timer is valid")
        }
        waitForExpectations(timeout: 5, handler: nil)
    }

}

我能想到的让测试更快的唯一方法就是模拟类并将let timerIntervalInSeconds = TimeInterval(1) 更改为private let timerIntervalInSeconds = TimeInterval(0.1)

为了使用更短的时间间隔进行测试而模拟类是否过大?

【问题讨论】:

    标签: swift unit-testing timer mocking


    【解决方案1】:

    我们可以验证对测试替身的调用,而不是使用真正的计时器(这会很慢)。

    挑战在于代码调用了工厂方法Timer.scheduledTimer(…)。这锁定了一个依赖。如果测试可以提供一个模拟计时器,测试会更容易。

    通常,注入工厂的一个好方法是提供一个闭包。我们可以在初始化程序中执行此操作,并提供默认值。然后,默认情况下,闭包将实际调用工厂方法。

    在这种情况下,它有点复杂,因为对Timer.scheduledTimer(…) 的调用本身需要一个闭包:

    internal init(seconds: Double,
                  makeRepeatingTimer: @escaping (TimeInterval, @escaping (TimerProtocol) -> Void) -> TimerProtocol = {
                      return Timer.scheduledTimer(withTimeInterval: $0, repeats: true, block: $1)
                  }) {
        durationInSeconds = TimeInterval(seconds)
        self.makeRepeatingTimer = makeRepeatingTimer
    }
    

    请注意,我删除了对 Timer 的所有引用,除了块内部。其他地方都使用新定义的TimerProtocol

    self.makeRepeatingTimer 是一个闭包属性。从startTimer(…) 拨打电话。

    现在测试代码可以提供不同的闭包:

    class TimerControllerTests: XCTestCase {
        var makeRepeatingTimerCallCount = 0
        var lastMockTimer: MockTimer?
    
        func testSomething() {
            let sut = TimerController(seconds: 12, makeRepeatingTimer: { [unowned self] interval, closure in
                self.makeRepeatingTimerCallCount += 1
                self.lastMockTimer = MockTimer(interval: interval, closure: closure)
                return self.lastMockTimer!
            })
    
            // call something on sut
    
            // verify against makeRepeatingTimerCallCount and lastMockTimer
        }
    }
    

    【讨论】:

      猜你喜欢
      • 2012-04-24
      • 2018-09-19
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-05-31
      • 2017-06-02
      相关资源
      最近更新 更多