【问题标题】:XCTest - Unable to understand / implement expectations in unit tests (to test aysnc code)XCTest - 无法理解/实现单元测试中的期望(测试异步代码)
【发布时间】:2020-08-27 07:08:37
【问题描述】:

(注意 - 我正在为 macOS 开发,所以请...针对 iOS 的建议对我没有帮助)

我正在尝试做的事情: 我有一个应用程序组件,它在后台线程上执行一个简短的任务,然后,如果满足某些条件,则在主线程上异步发送一个通知。

注意 - 我没有在我的应用代码中使用 NSNotification。我正在使用自己的自定义通知机制。所以,任何与 NSNotification 相关的解决方案都不适用于我。

我正在为上述应用程序组件编写一个单元测试,并且只是想检查该通知是否确实已发送。在执行断言之前,我的测试必须能够等待一秒钟左右才能让通知有时间到达其订阅者/观察者。

我希望能够在我的测试中测试两种可能的情况:两者都是正常情况。

  • 通知已发送。

  • 未发送通知。

在阅读了几个文档和代码示例数小时后,我不明白如何实现这一目标。

我只想在测试中等待一秒钟。真的有这么复杂吗?

  • sleep() 不起作用
  • DispatchQueue.main.asyncAfter(time) 不起作用
  • 定时器不工作

这里是需要测试的应用组件,以及它的单元测试:

在下面的代码中,expectation.fulfill() 应该放在哪里???

class ComponentBeingTested {

    func methodBeingTested() {

        doSomeWork()
        if certainConditionsAreMet {
             DispatchQueue.main.async {sendOutNotification()}
        }
    }
}

...

class UnitTestForComponentBeingTested: XCTestCase {

    let objectBeingTested = ComponentBeingTested()

    func testMethodBeingTested() {

          let expectation = self.expectation(description: "Notification was sent")

          // Call the code being tested
          objectBeingTested.methodBeingTested()

          // How do I do this with expectations ??? Where does expectation.fulfill() go ?
          waitForOneSecond()

          XCTAssertTrue(notificationSent)      // Assume the value of notificationSent is available

    }
}

【问题讨论】:

    标签: swift macos cocoa xctest


    【解决方案1】:

    这是一种方法

    func testMethodBeingTested() {
    
          // create expectation
          let expectation = self.expectation(description: "Notification was sent")
    
          // set expectation condition
          var notificationSent = false
          let observer = NotificationCenter.default
                .addObserver(forName: _Your_Notification_Name, object: nil, queue: nil) { _ in
                notificationSent = true
                expectation.fulfill()
            }
    
          // Call the code being tested
          objectBeingTested.methodBeingTested()
    
          // wait for expectation
          self.wait(for: [expectation], timeout: 5)
    
          XCTAssertTrue(notificationSent)
    }
    

    【讨论】:

    • 谢谢,但我也希望能够测试不应该发送通知的某些情况。在这些情况下,您的测试将失败,因为尚未调用 fully()。那么,如果未发送通知,我会将fulfill() 放在哪里?换句话说,我希望能够测试这两种应用场景 - 已发送通知,未发送通知。在这两种情况下,都需要在某个地方实现期望。如何 ?另外,我不能使用 NotificationCenter,因为 ComponentBeingTested 不使用 NotificationCenter 来发布通知。我正在使用我自己的自定义通知逻辑。
    【解决方案2】:

    查看XCTNSNotificationExpectation,当匹配的通知发布时就会实现。可以使用不同的初始化程序,具体取决于您希望对实现期​​望的限制程度。

    要检查通知是否发送,请将期望对象上的isInverted 设置为true

    然后只需在测试结束时添加对waitForExpectations(timeout:handler:) 的调用即可。

    【讨论】:

    • 感谢您的信息。唯一的问题是我没有在我的应用程序中使用 NSNotification。我有自己的自定义通知机制。我猜在我的情况下不能使用 XCTNSNotificationException。
    • 尝试用 waitForExpectations 替换您的 waitForOneSecond 方法,有帮助吗?
    • 在发布此问题之前,我尝试了 waitForExpectations(所有不同的方法变体)。我面临的根本问题是我需要在两种情况下都满足期望 - 无论是否发送通知。我没有尝试根据期望设置 isInverted 。我会尝试一下,让你知道它是否有效。
    • 如果你不关心期望的结果,那么我建议按照@Asperi 的方法使用 XCTWaiter 等待期望实现或超时,然后不检查结果。
    • 您好 Oletha,我找到了一个非常适合我需求的解决方案。我已将此作为答案发布。感谢您的帮助。
    【解决方案3】:

    好的,经过大量试验和错误,这对我来说非常有用:

    描述:我基本上在我的测试用例类中创建了一个辅助函数,其中包含所有样板期望/等待代码。它执行以下操作:

    1 - 创建一个期望(即 XCTestExpectation)作为一种形式。

    2 - 在预期的延迟期之后,在某个全局队列线程上调用我的(任意)测试用例断言代码(作为闭包传入)。一旦这个断言代码完成,期望就实现了(同样是一种形式)。

    3 - 通过调用 XCTestCase.wait(timeout) 等待期望。这确保了主线程/运行循环在我的断言代码在另一个线程上完成时保持活动状态。

    然后,在我的测试用例中,我只需调用该辅助函数,为其提供等待时间和一些要执行的代码(即我的断言)。

    这样,我就有了一个简单而富有表现力的可重用函数,它隐藏了我从一开始就认为没有必要的所有过度丑陋的期望。

    我可以把这个帮助器放在像 MyAppTestCase: XCTestCase 这样的基类中,这样我的所有测试用例类都可以使用它。

    注意 - 此解决方案可以增强并使其更加通用/可重用,但到目前为止,这对于最初发布的问题来说已经足够了。

    解决方案

    class ComponentBeingTested {
    
        func methodBeingTested() {
    
            doSomeWork()
            if certainConditionsAreMet {
                 DispatchQueue.main.async {sendOutNotification()}
            }
        }
    }
    
    ...
    
    class UnitTestForComponentBeingTested: XCTestCase {
    
        let objectBeingTested = ComponentBeingTested()
    
        // Helper function that uses expectation/wait to execute arbitrary
        // test code (passed in as a closure) after some delay period.
        func executeAfter(_ timeSeconds: Double, _ work: (@escaping () -> Void)) {
    
            let theExpectation = expectation(description: "some expectation")
    
            // Execute work() after timeSeconds seconds
            DispatchQueue.global(qos: .userInteractive).asyncAfter(deadline: .now() + timeSeconds) {
    
                // The call to work() will execute my test assertions
                work()
    
                // As a formality, fulfill the expectation
                theExpectation.fulfill()
            }
    
            // Wait for (timeSeconds + 1) seconds to give the work() call 
            // some time to perform the assertions
            wait(for: [theExpectation], timeout: timeSeconds + 1)
        }
    
        func testMethodBeingTested() {
    
              // Call the code being tested
              objectBeingTested.methodBeingTested()
    
              // Call the helper function above, to do the waiting before executing
              // the assertions
              executeAfter(0.5) {
    
                  // Assume the value of notificationSent is computed elsewhere
                  // and is available to assert at this point
                  XCTAssertTrue(notificationSent)      
              }
        }
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2014-09-29
      • 2016-02-23
      • 2017-10-31
      • 2014-09-02
      • 2020-07-14
      • 1970-01-01
      • 1970-01-01
      • 2018-02-11
      相关资源
      最近更新 更多