【问题标题】:How to Test Timer with @Published?如何使用@Published 测试计时器?
【发布时间】:2021-10-07 05:14:15
【问题描述】:

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

【问题讨论】:

  • 无需测试苹果的代码。不要测试计时器。你知道它的作用。测试你的代码。
  • 至于您的实际示例测试工具,它从上到下都有缺陷。没有storesink 确实只会得到一个值,如果它得到任何值的话。但是这个问题会更深层次,因为你的行为就像你的代码会神奇地停止并等待计时器完成。它不会。你说开始后立即停止。异步输入需要异步测试。你需要一个期望和一个服务员。
  • 但您完全不清楚为什么要订阅出版商。你想知道什么?似乎唯一感兴趣的问题是每次计时器触发时是否都在增加变量。您可以在没有计时器且无需订阅发布者的情况下进行测试。
  • @matt 我想确保计时器不会以某种方式工作。它基本上是一个进入 MacOS SwiftUI 测试的测试。你能提供一个如何测试这个异步的例子吗?我在网上找不到解决方案:(
  • 计时器如何停止工作?同样,您不能/不应该测试 Timer。你知道它的作用,它不是你的。

标签: swift swiftui


【解决方案1】:

我将首先重复我在 cmets 中已经说过的话。无需测试 Apple 的代码。不要测试计时器。你知道它的作用。测试你的代码。

至于您的实际示例测试工具,它从上到下都有缺陷。没有storesink 确实只会得到一个值,如果它得到任何值的话。但是这个问题会更深层次,因为你的行为就像你的代码会神奇地停止并等待计时器完成。它不会。你在说start 之后立即说stop,所以计时器甚至永远不会运行。异步输入需要异步测试。你需要一个期望和一个服务员。

但目前还不清楚您为什么要订阅出版商。你想知道什么?似乎唯一感兴趣的问题是每次计时器触发时是否都在增加变量。而且您可以在不订阅发布者的情况下进行测试——正如我所说,无需计时器。

重复就这么多。现在让我们演示一下。让我们从你实际展示的代码开始,唯一有内容的代码:

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。

【讨论】:

  • 在最初的测试中,计时器从未被触及,因此从测试的角度来看,每个被调用的函数都是一个黑盒子,因此说他们正在测试 Apple 的代码是没有意义的。但是,在这里您要进入要测试的服务内部,以便您可以测试计时器本身(可能应该是私有的)。计时器的要点是它会随着时间的推移而被调用,并且该测试基本上忽略了考虑到计时器的使用可能相关的时间方面。但我们也不知道 Timer 是如何设置的,这就是为什么它仍然与测试相关。
  • @keji 你可以这么说,但我不同意。如果重点是应用程序随着时间的推移如何表现,那么您也可以模拟时间。
  • @keji 我不同意测试做什么或服务做什么。如果函数调用 API,您不会反对我模拟服务器,Timer 也是如此。这个特定服务唯一要做的就是计算 Timer 触发了多少次,我们正在对其进行精确测试。
  • 我不明白为什么秒数不是从调用 start() 开始的秒数(加上初始数量)。这就是为什么它被称为秒。如果他们想测量它被调用的次数,那么他们应该称之为timescount。如果我的 TimerService 将计时器设置为每 0.5 秒触发一次怎么办?然后秒数最终是实际秒数的两倍,但这个测试仍然可以通过。
  • self 也被强烈捕获,并且由于无法停止代码中的服务,这总是会导致引用循环。
猜你喜欢
  • 2021-11-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-10-18
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-10-04
相关资源
最近更新 更多