2013-03-25 196 views
14

我在单元测试iOS中的异步调用时遇到问题。 (虽然它在视图控制器中工作正常)。在iOS的单元测试中测试异步调用

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

请建议一个很好的方法来做到这一点的一个例子。

回答

0

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

27

你需要旋转runloop直到你的回调被调用。尽管如此,确保它在主队列中被调用。

试试这个:

__block BOOL done = NO; 
doSomethingAsynchronouslyWithBlock(^{ 
    done = YES; 
}); 

while(!done) { 
    [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]; 
} 

你也可以使用一个信号量(如下图所示),但我更喜欢旋runloop允许分派到要处理的主队列异步块。

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

dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER); 
+0

感谢名单..它真的帮助 – 2013-03-25 08:06:35

+1

对于那些谁做与运行循环的方法的问题:它会无法正常工作:在方法'runMode:beforeDate:'在源事件处理完成之后才会返回。这可能永远不会发生(除非单元测试明确地在完成处理程序中以某种方式执行);) – CouchDeveloper 2013-10-14 13:38:15

+0

我使用了您的解决方案与在完成我的方法时已调用的通知的组合。谢谢! – 2015-03-25 12:36:22

1

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

FBTestBlocker:阻止当前线程退出指定超时的阻止程序。你可以将它拖放到你的项目中,但是如果你没有这个项目,你需要删除OCMock相关的东西。

FBTestBlocker.h

FBTestBlocker.m

FBURLConnectionTests:测试的例子,你应该看看。

FBURLConnectionTests.h

FBURLConnectionTests.m

这段代码应该给你一些想法

- (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"); 
} 
3

AGAsyncTestHelper是编写单元测试与异步操作一个C宏既SenTestingKit和XCTest工作。

简单,点

- (void)testAsyncBlockCallback 
{ 
    __block BOOL jobDone = NO; 

    [Manager doSomeOperationOnDone:^(id data) { 
     jobDone = YES; 
    }]; 

    WAIT_WHILE(!jobDone, 2.0); 
} 
3

这里的另一种选择,XCAsyncTestCase,与OCMock如果你需要使用它工作得很好。它基于GHUnit的异步测试器,但是使用常规的XCTest框架。 与Xcode机器人完全兼容。

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]; 
} 
0

我建议你连接信号+ runloop,我也写了方法,采取块:

// 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(); 
    }]; 

}); 
7

我想许多建议的解决方案我这篇文章有问题,如果异步操作没有完成,“完成”标志从不设置,并且测试将永远挂起。

我已经在我的许多测试中成功地使用了这种方法。

- (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; 
} 
13

下面是用于异步测试原生支持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); 
     } 

    }]; 
} 
2

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]; 
    }]; 
} 
0

可以使用异步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)") 
     } 
    } 
}