在iOS中testingunit testing中的asynchronous调用
我在unit testingiOS中的asynchronous调用时遇到问题。 (虽然它在视图控制器中工作正常。)
有没有人遇到过这个问题? 我曾尝试使用等待function,但我仍然面临同样的问题。
请build议一个很好的方法来做到这一点的例子。
尝试KIWI框架。 它function强大,可能会帮助您进行其他types的testing。
你需要旋转runloop直到你的callback被调用。 确保它在主队列上被调用。
尝试这个:
__block BOOL done = NO; doSomethingAsynchronouslyWithBlock(^{ done = YES; }); while(!done) { [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]; }
你也可以使用一个信号量(下面的例子),但是我更喜欢旋转runloop来允许分派到主队列的asynchronous块被处理。
dispatch_semaphore_t sem = dispatch_semaphore_create(0); doSomethingAsynchronouslyWithBlock(^{ //... dispatch_semaphore_signal(sem); }); dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
这里是苹果公司对asynchronoustesting本地支持的描述 。
TL; DR手册:
看看XCTextCase+AsynchronousTesting.h
只有一个公共方法有特殊的类XCTestExpectation
: - (void)fulfill;
你应该初始化这个类的实例,并在成功的情况下调用fulfill
方法。 否则,您的testing将在超时后失败,您在该方法中指定:
- (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); } }]; }
我认为这篇文章中的很多build议的解决scheme都存在这样的问题:如果asynchronous操作没有完成,“完成”标志从不设置,testing将永远挂起。
我在许多testing中成功地使用了这种方法。
- (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; }
由于Xcode 6内置于XCTest作为一个类别:
这里有另外一个替代scheme,XCAsyncTestCase,如果你需要使用OCMock,它可以很好地工作。 它基于GHUnit的asynchronoustesting器,而是使用常规的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, eg login operation failed // kXCTUnitWaitStatusCancelled, indicates the operation was cancelled [self waitForStatus:kXCTUnitWaitStatusSuccess timeout:2.0]; }
AGAsyncTestHelper是一个Cmacros,用于编写与asynchronous操作相关的unit testing,并与SenTestingKit和XCTest一起使用。
简单而重要
- (void)testAsyncBlockCallback { __block BOOL jobDone = NO; [Manager doSomeOperationOnDone:^(id data) { jobDone = YES; }]; WAIT_WHILE(!jobDone, 2.0); }
山姆·布罗德金已经给出了正确的答案 。
只是为了让答案看起来更好看,我把这里的示例代码。
使用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]; }]; }
我build议你应该看看Facebook-ios-sdk的testing 。 这是如何在iOS上testingasynchronousunit testing的一个很好的例子,虽然我个人认为asynchronoustesting应该进入同步testing。
FBTestBlocker :阻止当前线程退出指定超时的阻止程序。 你可以把它拖放到你的项目中,但如果你没有这个项目,你需要删除OCMock相关的东西。
FBTestBlocker.h
FBTestBlocker.m
FBURLConnectionTests :你应该看看testing的例子。
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"); }
我build议你连接信号量+ 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(); }]; });
如果您使用的是XCode 6,则可以像这样testingasynchronousnetworking呼叫:
XCT 6中的XCTest和asynchronoustesting
你可以像这样用swift来调用asynchronousAPI
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)") } } }