【问题标题】:Testing asynchronous call in unit test in iOS在 iOS 的单元测试中测试异步调用
【发布时间】:2013-03-14 14:30:25
【问题描述】:

我在 iOS 中对异步调用进行单元测试时遇到问题。 (虽然它在视图控制器中运行良好。)

以前有人遇到过这个问题吗?我尝试过使用等待功能,但我仍然面临同样的问题。

请提出一个这样做的好方法的例子。

【问题讨论】:

    标签: ios unit-testing


    【解决方案1】:

    在调用回调之前,您需要旋转运行循环。不过,请确保它在主队列上被调用。

    试试这个:

    __block BOOL done = NO;
    doSomethingAsynchronouslyWithBlock(^{
        done = YES;
    });
    
    while(!done) {
       [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
    }
    

    您也可以使用信号量(下面的示例),但我更喜欢旋转运行循环以允许处理分派到主队列的异步块。

    dispatch_semaphore_t sem = dispatch_semaphore_create(0);
    doSomethingAsynchronouslyWithBlock(^{
        //...
        dispatch_semaphore_signal(sem);
    });
    
    dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
    

    【讨论】:

    • 对于那些对运行循环方法有问题的人:它不能正常工作:方法runMode:beforeDate: 直到处理完源事件后才会返回。这可能永远不会发生(除非单元测试以某种方式在完成处理程序中显式执行);)
    • 我将您的解决方案与已在我的方法结束时调用的通知结合使用。谢谢!
    【解决方案2】:

    这里是 Apple's description 对异步测试的原生支持。

    TL;DR 手册:

    XCTextCase+AsynchronousTesting.h

    有一个特殊的类XCTestExpectation只有一个公共方法:- (void)fulfill;

    您应该初始化此类的实例,并在成功的情况下调用fulfill 方法。否则,您的测试将在您在该方法中指定的超时后失败:

    - (void)waitForExpectationsWithTimeout:(NSTimeInterval)timeout handler:(XCWaitCompletionHandler)handlerOrNil;
    

    例子:

    - (void)testAsyncMethod
    {
    
        //Expectation
        XCTestExpectation *expectation = [self expectationWithDescription:@"Testing Async Method Works Correctly!"];
    
        [MyClass asyncMethodWithCompletionBlock:^(NSError *error) {        
            if(error)
                NSLog(@"error is: %@", error);
            else
                [expectation fulfill];
        }];
    
        //Wait 1 second for fulfill method called, otherwise fail:    
        [self waitForExpectationsWithTimeout:1 handler:^(NSError *error) {
    
            if(error)
            {
                XCTFail(@"Expectation Failed with error: %@", error);
            }
    
        }];
    }
    

    【讨论】:

      【解决方案3】:

      我认为这篇文章中许多建议的解决方案都存在这样的问题,即如果异步操作未完成,则永远不会设置“完成”标志,并且测试将永远挂起。

      我在很多测试中都成功地使用了这种方法。

      - (void)testSomething {
          __block BOOL done = NO;
      
          [obj asyncMethodUnderTestWithCompletionBlock:^{
              done = YES;
          }];
      
          XCTAssertTrue([self waitFor:&done timeout:2],
                         @"Timed out waiting for response asynch method completion");
      }
      
      
      - (BOOL)waitFor:(BOOL *)flag timeout:(NSTimeInterval)timeoutSecs {
          NSDate *timeoutDate = [NSDate dateWithTimeIntervalSinceNow:timeoutSecs];
      
          do {
              [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:timeoutDate];
              if ([timeoutDate timeIntervalSinceNow] < 0.0) {
                  break;
              }
          }
          while (!*flag);
          return *flag;
      }
      

      【讨论】:

        【解决方案4】:

        从 Xcode 6 开始,这个内置到 XCTest 作为一个类别:

        https://stackoverflow.com/a/24705283/88164

        【讨论】:

          【解决方案5】:

          这是另一个替代方案 XCAsyncTestCase,如果您需要使用 OCMock,它可以很好地工作。它基于 GHUnit 的异步测试器,但使用常规的 XCTest 框架。 与 Xcode Bots 完全兼容。

          https://github.com/iheartradio/xctest-additions

          用法一样,只是导入XCAsyncTestCase并继承子类。

          @implementation TestAsync
          - (void)testBlockSample
          {
              [self prepare];
              dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(){
                  sleep(1.0);
                  [self notify:kXCTUnitWaitStatusSuccess];
              });
              // Will wait for 2 seconds before expecting the test to have status success
              // Potential statuses are:
              //    kXCTUnitWaitStatusUnknown,    initial status
              //    kXCTUnitWaitStatusSuccess,    indicates a successful callback
              //    kXCTUnitWaitStatusFailure,    indicates a failed callback, e.g login operation failed
              //    kXCTUnitWaitStatusCancelled,  indicates the operation was cancelled
              [self waitForStatus:kXCTUnitWaitStatusSuccess timeout:2.0];
          }
          

          【讨论】:

            【解决方案6】:

            AGAsyncTestHelper 是一个 C 宏,用于编写具有异步操作的单元测试,可与 SenTestingKit 和 XCTest 一起使用。

            简单明了

            - (void)testAsyncBlockCallback
            {
                __block BOOL jobDone = NO;
            
                [Manager doSomeOperationOnDone:^(id data) {
                    jobDone = YES; 
                }];
            
                WAIT_WHILE(!jobDone, 2.0);
            }
            

            【讨论】:

              【解决方案7】:

              Sam Brodkin 已经给了right answer

              为了让答案看起来更好看,我把示例代码放在这里。

              使用 XCTestExpectation。

              // Test that the document is opened. Because opening is asynchronous,
              // use XCTestCase's asynchronous APIs to wait until the document has
              // finished opening.
              
              - (void)testDocumentOpening
              {
                  // Create an expectation object.
                  // This test only has one, but it's possible to wait on multiple expectations.
                  XCTestExpectation *documentOpenExpectation = [self expectationWithDescription:@"document open"];
              
                  NSURL *URL = [[NSBundle bundleForClass:[self class]]
                                          URLForResource:@"TestDocument" withExtension:@"mydoc"];
                  UIDocument *doc = [[UIDocument alloc] initWithFileURL:URL];
                  [doc openWithCompletionHandler:^(BOOL success) {
                      XCTAssert(success);
                      // Possibly assert other things here about the document after it has opened...
              
                      // Fulfill the expectation-this will cause -waitForExpectation
                      // to invoke its completion handler and then return.
                      [documentOpenExpectation fulfill];
                  }];
              
                  // The test will pause here, running the run loop, until the timeout is hit
                  // or all expectations are fulfilled.
                  [self waitForExpectationsWithTimeout:1 handler:^(NSError *error) {
                      [doc closeWithCompletionHandler:nil];
                  }];
              }
              

              【讨论】:

                【解决方案8】:

                你可以像这样在swift中使用异步api调用

                private let serverCommunicationManager : ServerCommunicationManager = {
                    let instance = ServerCommunicationManager()
                    return instance
                }()
                
                var expectation:XCTestExpectation?
                func testAsyncApiCall()  {
                    expectation = self.expectation(description: "async request")
                
                    let header = ["Authorization":"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImp0aSI6ImQ4MmY1MTcxNzI4YTA5MjI3NWIzYWI3OWNkOTZjMGExOTI4MmM2NDEyZjMyYWQzM2ZjMzY4NmU2MjlhOWY2YWY1NGE0MDI4MmZiNzY2NWQ3In0.eyJhdWQiOiIxIiwianRpIjoiZDgyZjUxNzE3MjhhMDkyMjc1YjNhYjc5Y2Q5NmMwYTE5MjgyYzY0MTJmMzJhZDMzZmMzNjg2ZTYyOWE5ZjZhZjU0YTQwMjgyZmI3NjY1ZDciLCJpYXQiOjE1MDg4MjU1NTEsIm5iZiI6MTUwODgyNTU1MSwiZXhwIjoxNTQwMzYxNTUxLCJzdWIiOiIiLCJzY29wZXMiOltdfQ.osoMQgiY7TY7fFrh5r9JRQLQ6AZhIuEbrIvghF0VH4wmkqRUE6oZWjE5l0jx1ZpXsaYUhci6EDngnSTqs1tZwFTQ3srWxdXns2R1hRWUFkAN0ri32W0apywY6BrahdtiVZa9LQloD1VRMT1_QUnljMXKsLX36gXUsNGU6Bov689-bCbugK6RC3n4LjFRqJ3zD9gvkRaODuOQkqsNlS50b5tLm8AD5aIB4jYv3WQ4-1L74xXU0ZyBTAsLs8LOwvLB_2B9Qdm8XMP118h7A_ddLo9Cyw-WqiCZzeZPNcCvjymNK8cfli5_LZBOyjZT06v8mMqg3zszWzP6jOxuL9H1JjBF7WrPpz23m7dhEwa0a-t3q05tc1RQRUb16W1WhbRJi1ufdMa29uyhX8w_f4fmWdAnBeHZ960kjCss98FA73o0JP5F0GVsHbyCMO-0GOHxow3-BqyPOsmcDrI4ay006fd-TJk52Gol0GteDgdntvTMIrMCdG2jw8rfosV6BgoJAeRbqvvCpJ4OTj6DwQnV-diKoaHdQ8vHKe-4X7hbYn_Bdfl52gMdteb3_ielcVXIaHmQ-Dw3E2LSVt_cSt4tAHy3OCd7WORDY8uek4Paw8Pof0OiuqQ0EB40xX5hlYqZ7P_tXpm-W-8ucrIIxgpZb0uh-wC3EzBGPjpPD2j9CDo"]
                    serverCommunicationManager.sendServerRequest(httpMethodType: .get, baseURL: "http://192.168.2.132:8000/api/v1/user-role-by-company-id/2", param: nil, header: header) { (isSuccess, msg , response) in
                        if isSuccess
                        {
                            let array = response as! NSArray
                
                            if  array.count == 8
                            {
                                XCTAssertTrue(true)
                                self.expectation?.fulfill()
                            }
                            else
                            {
                                XCTAssertFalse(false)
                                XCTFail("array count fail")
                            }
                        }
                    }
                    waitForExpectations(timeout: 5) { (error) in
                        if let error = error{
                            XCTFail("waiting with error: \(error.localizedDescription)")
                        }
                    }
                }
                

                【讨论】:

                  【解决方案9】:

                  我建议你看看tests of Facebook-ios-sdk。这是如何在 iOS 上测试异步单元测试的一个很好的例子,虽然我个人认为异步测试应该分成同步测试。

                  FBTestBlocker:阻止当前线程在指定超时后退出。您可以将其拖放到您的项目中,但如果您的项目中没有与 OCMock 相关的内容,则需要删除它。

                  FBTestBlocker.h

                  FBTestBlocker.m

                  FBURLConnectionTests:您应该查看的测试示例。

                  FBURLConnectionTests.h

                  FBURLConnectionTests.m

                  这段代码 sn-p 应该会给你一些想法

                  - (void)testExample
                  {
                      FBTestBlocker *_blocker = [[FBTestBlocker alloc] initWithExpectedSignalCount:1];
                      __block BOOL excuted = NO;
                      [testcase test:^(BOOL testResult) {
                          XCTAssert(testResult, @"Should be true");
                          excuted = YES;
                          [_blocker signal];
                      }];
                  
                      [_blocker waitWithTimeout:4];
                      XCTAssertTrue(excuted, @"Not executed");
                  }
                  

                  【讨论】:

                    【解决方案10】:

                    试试 KIWI 框架。它功能强大,可以帮助您进行其他类型的测试。

                    【讨论】:

                      【解决方案11】:

                      我推荐你连接信号量+runloop,我也写了take block的方法:

                      // Set the flag to stop the loop
                      #define FLEND() dispatch_semaphore_signal(semaphore);
                      
                      // Wait and loop until flag is set
                      #define FLWAIT() WAITWHILE(dispatch_semaphore_wait(semaphore, DISPATCH_TIME_NOW))
                      
                      // Macro - Wait for condition to be NO/false in blocks and asynchronous calls
                      #define WAITWHILE(condition) \
                      do { \
                      while(condition) { \
                      [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:1]]; \
                      } \
                      } while(0)
                      

                      方法:

                      typedef void(^FLTestAsynchronousBlock)(void(^completion)(void));
                      
                      void FLTestAsynchronous(FLTestAsynchronousBlock block) {
                          FLSTART();
                          block(^{
                              FLEND();
                          });
                          FLWAIT();
                      };
                      

                      然后打电话

                      FLTestAsynchronous(^(void(^completion)()){
                      
                          [networkManager signOutUser:^{
                              expect(networkManager.currentUser).to.beNil();
                              completion();
                          } errorBlock:^(NSError *error) {
                              expect(networkManager.currentUser).to.beNil();
                              completion();
                          }];
                      
                      });
                      

                      【讨论】:

                        【解决方案12】:

                        如果您使用的是 XCode 6,您可以像这样测试异步网络调用:

                        XCTest and asynchronous testing in Xcode 6

                        【讨论】:

                          猜你喜欢
                          • 1970-01-01
                          • 1970-01-01
                          • 2012-04-28
                          • 1970-01-01
                          • 1970-01-01
                          • 2020-02-19
                          • 2017-06-01
                          • 2012-07-15
                          • 2021-10-13
                          相关资源
                          最近更新 更多