使用 XCTest 在 Xcode 中测试计时器
Testing a Timer in Xcode with XCTest
我有一个函数不需要超过每 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 秒,但我似乎找不到任何好的方法来执行此操作。 Stubbing resetTimer() 感觉测试并没有真正告诉我足够的功能。我错过了什么吗?
XC测试:
func testTimerResets() {
let myObject = MyClass()
myObject.resetTimer()
myObject.performAction()
// Test that my timer has been reset.
}
谢谢!
首先,我想说当您没有任何成员调用 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()
}
}
Testing (explanation in the comments)
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()
}
这不是最好的方法,但它可以让您了解应该如何实现这一点:)
如果你想等待计时器触发,你仍然需要使用期望(或 Xcode 9 的新异步测试 API)。
问题是您要测试的到底是什么。您可能不想只测试计时器是否触发,而是想测试计时器的处理程序实际在做什么。 (大概你有一个计时器来执行一些有意义的事情,所以这就是我们应该测试的。)
WWDC 2017 视频 Engineering for Testability 提供了一个很好的框架来思考如何为单元测试设计代码,这需要:
- 控制输入;
- 输出可见性;
- 无隐藏状态。
那么,您的测试输入是什么?而且,更重要的是,输出是什么。您想在单元测试中测试哪些断言?
该视频还展示了一些实际示例,说明如何通过明智地使用重构代码来实现此结构:
- 协议和参数化;和
- 分离逻辑和效果。
如果不知道计时器实际在做什么,就很难提供进一步的建议。也许你可以编辑你的问题并澄清。
我最终存储了原始 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)
}
很高兴您找到了解决方案,但回答了标题中的问题;
为了测试定时器是否真的工作(即运行和调用回调),我们可以做类似的事情:
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!")
}
}
}
当 class 像:
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)
}
}
我有一个函数不需要超过每 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 秒,但我似乎找不到任何好的方法来执行此操作。 Stubbing resetTimer() 感觉测试并没有真正告诉我足够的功能。我错过了什么吗?
XC测试:
func testTimerResets() {
let myObject = MyClass()
myObject.resetTimer()
myObject.performAction()
// Test that my timer has been reset.
}
谢谢!
首先,我想说当您没有任何成员调用 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()
}
}
Testing (explanation in the comments)
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()
}
这不是最好的方法,但它可以让您了解应该如何实现这一点:)
如果你想等待计时器触发,你仍然需要使用期望(或 Xcode 9 的新异步测试 API)。
问题是您要测试的到底是什么。您可能不想只测试计时器是否触发,而是想测试计时器的处理程序实际在做什么。 (大概你有一个计时器来执行一些有意义的事情,所以这就是我们应该测试的。)
WWDC 2017 视频 Engineering for Testability 提供了一个很好的框架来思考如何为单元测试设计代码,这需要:
- 控制输入;
- 输出可见性;
- 无隐藏状态。
那么,您的测试输入是什么?而且,更重要的是,输出是什么。您想在单元测试中测试哪些断言?
该视频还展示了一些实际示例,说明如何通过明智地使用重构代码来实现此结构:
- 协议和参数化;和
- 分离逻辑和效果。
如果不知道计时器实际在做什么,就很难提供进一步的建议。也许你可以编辑你的问题并澄清。
我最终存储了原始 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)
}
很高兴您找到了解决方案,但回答了标题中的问题;
为了测试定时器是否真的工作(即运行和调用回调),我们可以做类似的事情:
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!")
}
}
}
当 class 像:
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)
}
}