使用 XCTest 在 Xcode 中测试计时器

Posted

技术标签:

【中文标题】使用 XCTest 在 Xcode 中测试计时器【英文标题】:Testing a Timer in Xcode with XCTest 【发布时间】:2017-08-27 15:39:35 【问题描述】:

我有一个不需要每 10 秒调用一次的函数。每次调用该函数时,我都会将计时器重置为 10 秒。

class MyClass 
  var timer:Timer?

  func resetTimer() 
    self.timer?.invalidate()
    self.timer = Timer.scheduledTimer(withTimeInterval: 10.0, repeats: false) 
      (timer) -> Void in
      self.performAction()        
    
  

  func performAction() 
    // perform action, then
    self.resetTimer()
  

我想测试调用 performAction() 是否手动将计时器重置为 10 秒,但我似乎找不到任何好的方法来做到这一点。存根 resetTimer() 感觉测试并不能真正告诉我有关功能的足够信息。我错过了什么吗?

XCTest:

func testTimerResets() 
  let myObject = MyClass()
  myObject.resetTimer()
  myObject.performAction()

  // Test that my timer has been reset.

谢谢!

【问题讨论】:

如果你想让你的测试等待一些异步条件,你可以使用XCTestExpectation 【参考方案1】:

如果您想等待计时器触发,您仍然需要使用期望(或 Xcode 9 的新异步测试 API)。

问题是您要测试的究竟是什么。您可能不想只测试计时器是否触发,而是想测试计时器的处理程序实际上在做什么。 (大概你有一个计时器来执行一些有意义的事情,所以这就是我们应该测试的。)

WWDC 2017 视频Engineering for Testability 提供了一个很好的框架来思考如何为单元测试设计代码,这需要:

对输入的控制; 输出可见性;和 没有隐藏状态。

那么,您的测试输入是什么?而且,更重要的是,输出是什么。您想在单元测试中测试哪些断言?

该视频还展示了一些实际示例,说明如何通过明智地使用以下方法重构代码以实现这种结构:

协议和参数化;和 分离逻辑和效果。

在不知道计时器实际在做什么的情况下很难给出进一步的建议。也许您可以编辑您的问题并澄清一下。

【讨论】:

【参考方案2】:

首先,我要说的是,当您没有任何名为 refreshTimer 的成员时,我不知道您的对象是如何工作的。

class MyClass 
    private var timer:Timer?
    public var  starting:Int = -1 // to keep track of starting time of execution
    public var  ending:Int   = -1 // to keep track of ending time 


    init() 

    func invoke() 
       // timer would be executed every 10s 
        timer = Timer.scheduledTimer(timeInterval: 10.0, target: self, selector: #selector(performAction), userInfo: nil, repeats: true)
        starting = getSeconds()
        print("time init:: \(starting) second")

    

    @objc func performAction() 
        print("performing action ... ")
        /*
         say that the starting time was 55s, after 10s, we would get 05 seconds, which is correct. However for testing purpose if we get a number from 1 to 9 we'll add 60s. This analogy works because ending depends on starting time  
        */
        ending = (1...9).contains(getSeconds()) ? getSeconds() + 60 : getSeconds()
        print("time end:: \(ending) seconds")
        resetTimer()
    

    private func resetTimer() 
        print("timer is been reseted")
        timer?.invalidate()
        invoke()
    

    private func getSeconds()-> Int 
        let seconds = Calendar.current.component(.second, from: Date())
        return seconds 
    

    public func fullStop() 
        print("Full Stop here")
        timer?.invalidate()
    

测试(在 cmets 中有解释)

let testObj = MyClass()
    // at init both starting && ending should be -1
    XCTAssertEqual(testObj.starting, -1)
    XCTAssertEqual(testObj.ending, -1)

    testObj.invoke()
    // after invoking, the first member to be changed is starting
    let startTime = testObj.starting
    XCTAssertNotEqual(startTime, -1)
    /*
    - at first run, ending is still -1 
    - let's for wait 10 seconds 
    - you should use async  method, XCTWaiter and expectation here 
    - this is just to give you a perspective or way of structuring your solution
   */
    DispatchQueue.main.asyncAfter(deadline: .now() + 10 ) 
        let startTimeCopy = startTime
        let endingTime = testObj.ending
        XCTAssertNotEqual(endingTime, -1)
        // take the difference between start and end
        let diff = endingTime - startTime
        print("diff \(diff)")
        // no matter the time, diff should be 10
        XCTAssertEqual(diff, 10)

        testObj.fullStop()
    

这不是最好的方法,但是它可以让您了解或了解您应该如何实现这一目标:)

【讨论】:

【参考方案3】:

很高兴您找到了解决方案,但回答了标题中的问题;

要测试计时器是否真的有效(即运行和调用回调),我们可以这样做:

import XCTest
@testable import MyApp

class MyClassTest: XCTestCase 
    func testCallback_triggersCalledOnTime() throws 
        let exp = expectation(description: "Wait for timer to complete")
        
        // Dummy.
        let instance: MyClass! = MyClass()
        instance.delay = 2000; // Mili-sec equal 2 seconds.
        instance.callback =  _ in
            exp.fulfill();
        

        // Actual test.
        instance.startTimer();
        // With pause till completed (sleeps 5 seconds maximum,
        // else resumes as soon as "exp.fulfill()" is called).
        if XCTWaiter.wait(for: [exp], timeout: 5.0) == .timedOut 
            XCTFail("Timer didn't finish in time!")
        
    

当有这样的课程时:

public class MyClass 
    var delay: Int = 0;
    var callback: ((timer: Timer) -> Void)?
    
    func startTimer() 
        let myTimer = Timer(timeInterval: Double(self.delay) / 1000.0, repeats: false) 
            [weak self] timer in
            guard let that = self else 
                return
            
            that.callback?(timer)
        
        RunLoop.main.add(myTimer, forMode: .common)
    

【讨论】:

【参考方案4】:

我最终存储了原始 Timer 的 fireDate,然后检查执行操作后新的 fireDate 是否设置为比原始 fireDate 晚。

func testTimerResets() 
  let myObject = MyClass()
  myObject.resetTimer()
  let oldFireDate = myObject.timer!.fireDate
  myObject.performAction()

  // If timer did not reset, these will be equal
  XCTAssertGreaterThan(myObject.timer!.fireDate, oldFireDate)

【讨论】:

以上是关于使用 XCTest 在 Xcode 中测试计时器的主要内容,如果未能解决你的问题,请参考以下文章

xcode 5 xctest 测试异步登录网络服务

如何在 Xcode 中配置和运行目标 C 测试用例--XCTest

如何使用 Xcode 5 从命令行运行 xctest?

可以将 C++ 单元测试集成到 Xcode 的 XCTest 中吗?

iOS 单元测试- Xcode 7测试工具XCTest学习

XCTest:如果我只是对测试目标进行更改,如何防止 Xcode 不必要地重新编译我的项目