在Xcode中使用XCTest测试一个计时器

4

我有一个函数,不需要每隔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.
}

谢谢!


2
如果您希望测试等待某些异步条件,可以使用XCTestExpectation - Rob
4个回答

2
如果您想等待定时器触发,仍然需要使用期望(或Xcode 9的新异步测试API)。
问题在于您要测试什么。您可能不只是想测试定时器是否触发,而是想测试定时器的处理程序实际上正在执行什么操作。(假设您有一个定时器来执行某些有意义的操作,那么这就是我们应该测试的内容。)
WWDC 2017视频Engineering for Testability提供了一个很好的框架,可以考虑如何设计用于单元测试的代码,需要:
  • 控制输入;
  • 可见输出;和
  • 没有隐藏状态。
那么,您的测试输入是什么?更重要的是,输出是什么。您想在单元测试中测试哪些断言?
视频还展示了一些实际的示例,说明人们如何通过谨慎地使用以下方法来重构代码以实现此结构:
  • 协议和参数化;以及
  • 分离逻辑和效果。
没有了解定时器具体功能,很难进一步提供建议。也许您可以编辑您的问题并进行澄清。

2

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

为了测试计时器是否实际工作(即运行并调用回调函数),我们可以做一些类似于以下的操作:

import XCTest
@testable import MyApp

class MyClassTest: XCTestCase {
    func testStartTimer_shouldTriggerCallbackOnTime() 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) != .completed {
            XCTFail("Timer didn't finish in time.")
        }
    }
}

当有一个类如下:

public class MyClass {
    public var delay: Int = 0;
    public var callback: ((timer: Timer) -> Void)?
    
    public 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)
    }
}

1

首先,我想说的是,当你没有任何名为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()
    }
}

测试(在注释中解释)
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()
    }

这并不是做事情的最佳方式,但它可以为您提供一种实现此目标的视角或思路 :)

0

我最终存储了原始计时器的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)
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接