如何使用@Published 测试计时器?

我创建了一个使用 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 本身以外的其他东西订阅?

tcj222022 回答:如何使用@Published 测试计时器?

我将首先重复我在评论中已经说过的内容。无需测试 Apple 的代码。不要测试定时器。你知道它的作用。测试您的代码。

至于您的实际示例测试工具,它从上到下都有缺陷。没有 sinkstore 确实只会得到一个值,如果有的话。但问题更严重,因为您的行为就像您的代码会神奇地停止并等待计时器完成。不会。您在说 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。

本文链接:https://www.f2er.com/1622.html

大家都在问