我创建了一个使用 ObservableObject
的视图,该视图使用 Timer
更新作为 @Published
属性的秒数。
class TimerService: ObservableObject {
@Published var seconds: Int
var timer: Timer?
convenience init() {
self.init(0)
}
init(_ seconds: Int){
self.seconds = seconds
}
func start() {
...
self.timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
self.seconds += 1 }
self.timer?.fire()
}
func stop() {...}
func reset() {...}
}
为了测试这个逻辑,我尝试订阅 seconds
变量。问题是 .sink 方法只触发一次,永远不会再触发,即使它应该触发。
class WorkTrackerTests: XCTestCase {
var timerService: TimerService!
override func setUpWithError() throws {
super.setUp()
timerService = TimerService()
}
override func tearDownWithError() throws {
super.tearDown()
timerService = nil
}
func test_start_timer() throws {
var countingArray: [Int] = []
timerService.start()
timerService.$seconds.sink(receiveValue: { value -> Void in
print(value) // 1 (called once with this value)
countingArray.append(value)
})
timerService.stop()
for index in 0...countingArray.count-1 {
if(index>0) {
XCTAssertTrue(countingArray[index] - 1 == countingArray[index-1])
}
}
}
}
是我做错了什么,还是 SwiftUI @Published Wrapper 不能被 SwiftUI 本身以外的其他东西订阅?
答案 0 :(得分:0)
我将首先重复我在评论中已经说过的内容。无需测试 Apple 的代码。不要测试定时器。你知道它的作用。测试您的代码。
至于您的实际示例测试工具,它从上到下都有缺陷。没有 sink
的 store
确实只会得到一个值,如果有的话。但问题更严重,因为您的行为就像您的代码会神奇地停止并等待计时器完成。不会。您在说 stop
后立即说 start
,因此计时器甚至从未运行。异步输入需要异步测试。你需要一个期待和一个服务员。
但是您完全不知道为什么要订阅出版商。你想知道什么?唯一感兴趣的问题似乎是,每次定时器触发时,您是否都在增加变量。您可以在不订阅出版商的情况下进行测试 - 正如我所说,没有计时器。
重复这么多。现在让我们演示一下。让我们从您实际展示的代码开始,这是唯一包含内容的代码:
class TimerService: ObservableObject {
@Published var seconds: Int
var timer: Timer?
convenience init() {
self.init(0)
}
init(_ seconds: Int){
self.seconds = seconds
}
func start() {
self.timer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
self.seconds += 1
}
self.timer?.fire()
}
}
现在查看发送给 Timer 的所有命令并将它们封装在 Timer 子类中:
class TimerMock : Timer {
var block : ((Timer) -> Void)?
convenience init(block: (@escaping (Timer) -> Void)) {
self.init()
self.block = block
}
override class func scheduledTimer(withTimeInterval interval: TimeInterval,
repeats: Bool,
block: @escaping (Timer) -> Void) -> Timer {
return TimerMock(block:block)
}
override func fire() {
self.block?(self)
}
}
现在让你的 TimerService 成为一个泛型,这样我们就可以在测试时注入 TimerMock:
class TimerService<T:Timer>: ObservableObject {
@Published var seconds: Int
var timer: Timer?
convenience init() {
self.init(0)
}
init(_ seconds: Int){
self.seconds = seconds
}
func start() {
self.timer = T.scheduledTimer(withTimeInterval: 1, repeats: true) { _ in
self.seconds += 1
}
self.timer?.fire()
}
}
所以现在我们可以测试您的逻辑,而无需费心运行计时器:
import XCTest
@testable import TestingTimer // whatever the name of your module is
class TestingTimerTests: XCTestCase {
func testExample() throws {
let timerService = TimerService<TimerMock>()
timerService.start()
if let timer = timerService.timer {
timer.fire()
timer.fire()
timer.fire()
}
XCTAssertEqual(timerService.seconds,4)
}
}
您的其他代码都不需要更改;您可以像以前一样继续使用 TimerService。我可以想到其他方法来做到这一点,但它们都涉及依赖注入,在“现实生活”中您使用 Timer,但在测试时使用 TimerMock。