【问题标题】:Error with XCTestExpectation: API violation - multiple calls made to -[XCTestExpectation fulfill]XCTestExpectation 出错:API 违规 - 多次调用 -[XCTestExpectation 完成]
【发布时间】:2014-09-29 19:00:45
【问题描述】:

我在 Xcode 6(Beta 5)中使用 XCTestExpectations 进行异步测试。每次我运行它们时,我所有的异步测试都会单独通过。但是,当我尝试运行我的整个套件时,一些测试没有通过,并且应用程序崩溃了。

我得到的错误是API violation - multiple calls made to -[XCTestExpectation fulfill]。事实上,这在单一方法中是不正确的。我的测试的一般格式如下所示:

- (void) someTest {
    /* Declare Expectation */
    XCTestExpectation *expectation = [self expectationWithDescription:@"My Expectation"];
    [MyClass loginOnServerWithEmail:@"example@email.com" andPassword:@"asdfasdf" onSuccess:^void(User *user) {
        /* Make some assertions here about the object that was given. */

        /* Fulfill the expectation */
        [expectation fulfill];
    }];

    [self waitForExpectationsWithTimeout:5.0 handler:^(NSError *error) {
        /* Error handling here */
    }];
}

同样,这些测试在单独运行时确实通过了,并且它们实际上是在发出网络请求(完全按预期工作),但在一起时,测试集合无法运行。

我能够查看这篇帖子 here,但无法找到适合我的解决方案。

此外,我正在运行 OSX Mavericks 并使用 Xcode 6(Beta 5)。

【问题讨论】:

  • 在使用基本相同的格式之前,我已经运行了 100 多个测试套件,并且之前没有遇到过这个问题......你确定没有单独的测试用例有两个完成?
  • 或许升级到 beta 6 看看问题是否依然存在?
  • @Mihir 当我在 expectForNotification 处理程序中调用完成时遇到了这个问题。我的假设是您的测试多次调用完成。如果您在调用完成时添加日志语句,您将看到:)
  • 我之前也遇到过API violation - multiple calls made to 错误,但后来我意识到我错过了这个wait(for: [promise], timeout: 10) 函数。但是你确实添加了waitForExpectations

标签: unit-testing asynchronous ios8 xcode6 xctest


【解决方案1】:

这通常是由于您忘记调用return在调用completionBlock之后造成的。

func load(complete: ((Result)->Void)) {
   urlSession.dataTask(request) { (error, response, data) in 
      if let error = error {
         complete(.failure(error) // 1st time calls complete(_) 
         // missing return statement here
      }
      complete(.success(data!) // 2nd time calls complete(_)
   }.resume()
}

【讨论】:

    【解决方案2】:

    尽管这个问题已经很老了,但我认为它对根本问题没有正确的解释。 这肯定会遇到 XCTestExpectation 可能遇到的问题之一。 如需完整的深入解释,请访问 Jeremy W. Sherman post where he explained XCTestExpectation gotchas perfectly.

    总结

    • 始终将您的期望分配给弱参考,然后放弃 如果它是 nil,你的回调。
    • 在极少数情况下,您希望回调被触发更多 不止一次,你可以通过消灭你的弱者来避免满足 完成后参考,然后忽略未来的电话。更多的 很可能,你知道你应该被叫多少次,你会想要 仅在最后一次通话时履行承诺。但解决方法是 如果您需要,就在那儿。
    • 如果您已经在使用基于 Promise 的 API,则可以跳过 XCTestExpectation 并使用由 这个承诺而不是 XCTest 自己的承诺。这有一个额外的好处 通过消除处理的需要线性化您的测试代码 在闭包中传递值(或手动将其发送出去以断言 在 XCTest 等待完成后反对)。

    正确避免错误和崩溃的代码示例:

    func testPreparedForNotWaitingLongEnough() {
        weak var promiseToCallBack = expectationWithDescription("calls back")
        after(seconds: callBackDelay) { () -> Void in
            guard let promise = promiseToCallBack else {
                print("too late, buckaroo")
                return
            }
    
            print("I knew you'd call!")
            promise.fulfill()
        }
    
        waitForExpectationsWithTimeout(callBackDelay / 2) { error in
            print("Aww, we timed out: \(error)")
        }
    }
    
    func testSafelyDoubleTheFulfillment() {
        weak var promiseToCallBack = expectationWithDescription("calls back")
        let callBackDelay: NSTimeInterval = 1
    
        twiceAfter(seconds: callBackDelay) {
            guard let promise = promiseToCallBack else {
                print("once was enough, thanks!")
                return
            }
    
            promise.fulfill()
            promiseToCallBack = nil
        }
    
        let afterCallBack = 2 * callBackDelay
        waitForExpectationsWithTimeout(afterCallBack, handler: nil)
    }
    

    【讨论】:

      【解决方案3】:

      API 违规 - 多次调用 -[XCTestExpectation 实现]

      我在设置expectation.expectedFulfillmentCount时遇到了同样的错误。

      expectation.assertForOverFulfill = false
      

      如果 expectation.assertForOverFulfill = truefulfill() 在已经完成时被调用,那么它会抛出异常

      【讨论】:

        【解决方案4】:

        我的问题也是我的代码的线程问题。我没有删除完成处理程序字典中的值。然后我的下一个测试再次调用完成处理程序(并再次满足期望)重用了该值,这导致了错误。

        请注意:查找/修复这些问题符合您的最大利益,不要假设预期或 Xcode 存在问题。

        【讨论】:

          【解决方案5】:

          在 Xcode 12.5 / Swift 5 上遇到同样的问题。

          检查您是否没有覆盖 setUp 和 tearDown 'class' 方法。

          应该是:

          override func setUp()
          override func tearDown()
          

          代替:

          override class func setUp()
          override class func tearDown()
          

          【讨论】:

            【解决方案6】:

            我不确定为什么它多次调用已完成,我通过执行以下操作解决了这个问题:

            let tokenReceived = expectation(description: "Token Received")
            var fulfilled = false
            mymanager.fetchToken(task: "token", callbackBlock: {token, error in
                        DispatchQueue.main.async {
                            tokenString = token
                            debugPrint("Token found:\(token)")
                            if(!fulfilled)
                            {
                                fulfilled.toggle()
                                tokenReceived.fulfill()
                            }
                        }
                    })
            

            【讨论】:

              【解决方案7】:

              遇到 API 违规错误的人,可能self.expectation() 不是你的朋友。相反,您可以尝试常规初始化:

              let expectation = XCTestExpectation(description: "")
              

              查看this答案了解详情。

              【讨论】:

                【解决方案8】:

                由于未捕获的异常而终止应用程序 'NSInternalInconsistencyException',原因:'API 违规 - 多个 对 -[XCTestExpectation 完成] 的任何调用。'

                我使用以下代码收到上述错误:

                func testMultipleWaits() {
                    let exp = expectation(description: "whatever")
                    for _ in 0...10 {
                        DispatchQueue.main.async {
                            exp.fulfill()
                        }
                        wait(for: [exp], timeout: 1)
                    }
                }
                

                基本上给定的期望可以实现一次,也可以等待一次。

                意味着以下更改仍然无法修复它,因为您仍然等待了多次。它会给你以下错误。

                失败:捕获“NSInternalInconsistencyException”、“API 违规 - 期待只能等一次,whatever已经 等等”

                func testMultipleWaits() {
                    let exp = expectation(description: "whatever")
                    for i in 0...10 {
                        DispatchQueue.main.async {
                            if i == 6 {
                                exp.fulfill()
                            }
                        }
                        wait(for: [exp], timeout: 1)
                    }
                }
                

                令人困惑的是,虽然上述更改仍然崩溃,但测试确实完成并给您“异步等待失败:超过 1 秒的超时,未实现预期:“随便”。

                这是一种误导,因为您可能会花时间修复测试,而您应该花时间修复作为根本原因的崩溃


                这里的修复是在for循环内部设置期望。这样,期望就会得到满足,并且每个期望都会等待一次。

                func testMultipleWaits() {        
                    for _ in 0...10 {
                        let exp = expectation(description: "whatever")
                        DispatchQueue.main.async {
                            exp.fulfill()
                        }
                        wait(for: [exp], timeout: 1)
                    }
                }
                

                【讨论】:

                  【解决方案9】:

                  我不认为使用__weak__block 是一个好方法。一段时间以来,我已经使用XCTestExpectation 编写了许多单元测试,但直到现在才遇到这个问题。我终于发现了问题的真正原因,这可能会导致我的应用程序出现错误。我的问题的根本原因是startAsynchronousTaskWithDuration 多次调用completionHandler。在我修复它之后,API 违规就消失了!

                  [self startAsynchronousTaskWithDuration:4 completionHandler:^(id result, NSError *error) {
                      XCTAssertNotNil(result);
                      XCTAssertNil(error);
                      [expectation fulfill];
                  }];
                  

                  虽然我花了几个小时来修复我的单元测试,但我开始欣赏 API 违规错误,这将帮助我避免将来在我的应用中出现运行时问题。

                  【讨论】:

                    【解决方案10】:

                    我认为您可能在某处遇到保留周期问题,阻止释放您的对象,该对象正在调用多次满足期望的块。

                    否则,如果您的期望被多次调用是一种预期行为,我编写了一个小扩展,允许指定期望计数:

                    import XCTest
                    
                    extension XCTestExpectation {
                        private class IntWrapper {
                            let value :Int!
                            init?(value:Int?) {
                                self.value = value
                                if (value == nil) {
                                    return nil
                                }
                            }
                        }
                    
                        private struct AssociatedKey {
                            static var expectationCountKey = ""
                        }
                    
                        var expectationCount:Int? {
                            get {
                                return objc_getAssociatedObject(self, &AssociatedKey.expectationCountKey) as? Int
                            }
                            set {
                                objc_setAssociatedObject(self, &AssociatedKey.expectationCountKey, newValue, .OBJC_ASSOCIATION_RETAIN)
                            }
                        }
                    
                        func decrementalFulfill() {
                            guard let expectationCount = self.expectationCount else {
                                fulfill()
                                return
                            }
                            self.expectationCount = expectationCount - 1
                            if self.expectationCount <= 0 {
                                fulfill()
                            }
                        }
                    }
                    

                    完整代码(带测试:)在这里:https://gist.github.com/huguesbr/7d110bffd043e4d11f2886693c680b06

                    【讨论】:

                      【解决方案11】:

                      尝试将您的expectationWithDescription 声明为weak 并打开可选的“expected”变量。

                       weak var asyncExpectation = expectationWithDescription("expectation")
                      
                       check for options in the block.
                       if let asyncExpectation = asyncExpectation{
                          asyncExpectation.fulfill()
                       }
                      

                      这避免了 asyncExpectation 变量的释放和调用你对 nil 的期望。

                      【讨论】:

                      • 老兄......你的语言甚至不是同一种语言......你谈到了一个objective-c问题的可选包装......
                      【解决方案12】:

                      这可能是您正在寻找的答案:

                      XCTestExpectation: how to avoid calling the fulfill method after the wait context has ended?

                      它至少为我解决了这个问题。

                      【讨论】:

                      • 虽然这可能是一个很好的答案,但它本质上是一个“仅链接”的答案。您应该包含链接中的一些信息,以便如果链接背后的信息曾经更改/丢失,答案仍然有意义。见this link
                      猜你喜欢
                      • 2015-03-27
                      • 2021-12-06
                      • 2015-02-06
                      • 2020-07-18
                      • 2017-07-04
                      • 2015-12-24
                      • 1970-01-01
                      • 2019-04-08
                      • 2016-11-14
                      相关资源
                      最近更新 更多