如何unit testingasynchronousAPI?

我已将Google Toolbox for Mac安装到Xcode中,并按照说明设置了在此处find的unit testing。

这一切都很好,我可以绝对好的testing我的所有对象的同步方法。 然而,我实际上想要testing的大部分复杂的API是通过在委托上调用一个方法来asynchronous返回结果 – 例如,对文件下载和更新系统的调用将立即返回,然后在文件结束下载时运行-fileDownloadDidComplete:方法。

我如何testing这个unit testing?

这似乎是我想要的testDownload函数,或者至less是testing框架'等待'fileDownloadDidComplete:方法来运行。

编辑:我现在已经切换到使用XCode内置XCTest系统,并发现Github上的TVRSMonitor提供了一个简单的方法来使用信号等待asynchronous操作来完成。

例如:

- (void)testLogin { TRVSMonitor *monitor = [TRVSMonitor monitor]; __block NSString *theToken; [[Server instance] loginWithUsername:@"foo" password:@"bar" success:^(NSString *token) { theToken = token; [monitor signal]; } failure:^(NSError *error) { [monitor signal]; }]; [monitor wait]; XCTAssert(theToken, @"Getting token"); } 

我遇到了同样的问题,并find了适合我的不同解决scheme。

我使用“老派”方法通过使用信号量将asynchronous操作转换为同步stream程,如下所示:

 // create the object that will perform an async operation MyConnection *conn = [MyConnection new]; STAssertNotNil (conn, @"MyConnection init failed"); // create the semaphore and lock it once before we start // the async operation NSConditionLock *tl = [NSConditionLock new]; self.theLock = tl; [tl release]; // start the async operation self.testState = 0; [conn doItAsyncWithDelegate:self]; // now lock the semaphore - which will block this thread until // [self.theLock unlockWithCondition:1] gets invoked [self.theLock lockWhenCondition:1]; // make sure the async callback did in fact happen by // checking whether it modified a variable STAssertTrue (self.testState != 0, @"delegate did not get called"); // we're done [self.theLock release]; self.theLock = nil; [conn release]; 

确保调用

 [self.theLock unlockWithCondition:1]; 

那么在代表(S)。

我很欣赏这个问题在一年前被问及回答,但是我不禁不同意这个答案。 testingasynchronous操作,尤其是networking操作是非常普遍的要求,对于正确的操作非常重要。 在给定的例子中,如果你依赖于实际的networking响应,你将失去testing的一些重要价值。 具体而言,您的testing取决于您与之通信的服务器的可用性和function正确性; 这个依赖使得你的testing

  • 更脆弱(如果服务器停机,会发生什么?)
  • 不太全面(如何持续testing失败响应或networking错误?)
  • 想象一下testing这个要慢得多

unit testing应该在几分之一秒内运行。 如果您每次运行testing都必须等待多秒的networking响应,那么您不太可能经常运行它们。

unit testing主要是关于封装依赖关系; 从你的代码的angular度来看,有两件事情会发生:

  1. 你的方法启动一个networking请求,可能是通过实例化一个NSURLConnection。
  2. 您指定的委托通过某些方法调用接收响应。

您的委托不会或不应该关心响应的来源,无论是来自远程服务器的实际响应还是来自您的testing代码。 您可以利用这一点通过简单地自行生成响应来testingasynchronous操作。 您的testing运行速度会更快,您可以可靠地testing成功或失败响应。

这并不是说您不应该针对您正在使用的真实Web服务运行testing,而是那些集成testing,并且属于他们自己的testing套件。 该套件中的故障可能意味着Web服务发生了更改,或者简单地停机。 由于它们更脆弱,自动化它们往往比自动执行unit testing更有价值。

关于如何testing对networking请求的asynchronous响应,您有几个选项。 你可以简单地通过直接调用方法来testing委托(例如[someDelegate连接:连接didReceiveResponse:someResponse])。 这将有所作为,但稍有不妥。 您的对象提供的委托可能只是针对特定NSURLConnection对象的委托链中的多个对象中的一个; 如果直接调用委托方法,则可能会遗漏另一个委托代理提供的一些关键function。 作为一个更好的select,你可以对你创build的NSURLConnection对象进行存根,并将响应消息发送给它的整个委托链。 有些库会重新打开NSURLConnection(其他类),并为你做这个。 例如: https : //github.com/pivotal/PivotalCoreKit/blob/master/SpecHelperLib/Extensions/NSURLConnection%2BSpec.m

St3fan,你是一个天才。 非常感谢!

这是我用你的build议做的。

“下载程序”定义了一个协议,其中包含一个完成时触发的方法DownloadDidComplete。 有一个BOOL成员variables“downloadComplete”用于终止运行循环。

 -(void) testDownloader { downloadComplete = NO; Downloader* downloader = [[Downloader alloc] init] delegate:self]; // ... irrelevant downloader setup code removed ... NSRunLoop *theRL = [NSRunLoop currentRunLoop]; // Begin a run loop terminated when the downloadComplete it set to true while (!downloadComplete && [theRL runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]); } -(void) DownloaderDidComplete:(Downloader*) downloader withErrors:(int) errors { downloadComplete = YES; STAssertNotEquals(errors, 0, @"There were errors downloading!"); } 

运行循环当然可能会永远运行..我会稍后改进!

我写了一个小助手,可以轻松testingasynchronousAPI。 首先帮手:

 static inline void hxRunInMainLoop(void(^block)(BOOL *done)) { __block BOOL done = NO; block(&done); while (!done) { [[NSRunLoop mainRunLoop] runUntilDate: [NSDate dateWithTimeIntervalSinceNow:.1]]; } } 

你可以像这样使用它:

 hxRunInMainLoop(^(BOOL *done) { [MyAsyncThingWithBlock block:^() { /* Your test conditions */ *done = YES; }]; }); 

它只会继续,如果done TRUE ,所以一定要完成设置。 当然,如果你愿意的话,你可以给助手添加一个超时时间,

这是棘手的。 我认为你需要在你的testing中设置一个runloop,并且能够指定你的asynchronous代码的runloop。 否则,callback将不会发生,因为它们是在runloop上执行的。

我想你可以在一个循环中短时间运行runloop。 并让callback设置一些共享状态variables。 或者甚至可以简单地要求callback终止runloop。 那样你就知道testing结束了。 你应该能够通过在一段时间之后停止循环来检查超时。 如果发生这种情况,则会发生超时。

我从来没有这样做过,但我很快就会想到。 请分享你的结果:-)

如果你正在使用像AFNetworking或ASIHTTPRequest这样的库,并且通过一个NSOperation(或者这些库的子类)pipe理你的请求,那么使用NSOperationQueue对一个testing/ dev服务器进行testing是很容易的。

在testing中:

 // create request operation NSOperationQueue* queue = [[NSOperationQueue alloc] init]; [queue addOperation:request]; [queue waitUntilAllOperationsAreFinished]; // verify response 

这本质上运行一个runloop直到操作完成,允许所有的callback发生在后台线程,因为他们通常会。

为了详细说明@St3fan的解决scheme,可以在启动请求之后尝试:

 - (BOOL)waitForCompletion:(NSTimeInterval)timeoutSecs { NSDate *timeoutDate = [NSDate dateWithTimeIntervalSinceNow:timeoutSecs]; do { [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:timeoutDate]; if ([timeoutDate timeIntervalSinceNow] < 0.0) { break; } } while (!done); return done; } 

其他方式:

 //block the thread in 0.1 second increment, until one of callbacks is received. NSRunLoop *theRL = [NSRunLoop currentRunLoop]; //setup timeout float waitIncrement = 0.1f; int timeoutCounter = (int)(30 / waitIncrement); //30 sec timeout BOOL controlConditionReached = NO; // Begin a run loop terminated when the downloadComplete it set to true while (controlConditionReached == NO) { [theRL runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:waitIncrement]]; //control condition is set in one of your async operation delegate methods or blocks controlConditionReached = self.downloadComplete || self.downloadFailed ; //if there's no response - timeout after some time if(--timeoutCounter <= 0) { break; } } 

我发现使用https://github.com/premosystems/XCAsyncTestCase非常方便;

它向XCTestCase添加了三个非常方便的方法

 @interface XCTestCase (AsyncTesting) - (void)waitForStatus:(XCTAsyncTestCaseStatus)status timeout:(NSTimeInterval)timeout; - (void)waitForTimeout:(NSTimeInterval)timeout; - (void)notify:(XCTAsyncTestCaseStatus)status; @end 

这允许非常干净的testing。 项目本身的一个例子:

 - (void)testAsyncWithDelegate { NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://www.google.com"]]; [NSURLConnection connectionWithRequest:request delegate:self]; [self waitForStatus:XCTAsyncTestCaseStatusSucceeded timeout:10.0]; } - (void)connectionDidFinishLoading:(NSURLConnection *)connection { NSLog(@"Request Finished!"); [self notify:XCTAsyncTestCaseStatusSucceeded]; } - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error { NSLog(@"Request failed with error: %@", error); [self notify:XCTAsyncTestCaseStatusFailed]; } 

我实施了Thomas Tempelmann提出的解决scheme,总的来说对我来说工作得很好。

但是,有一个问题。 假设要testing的单位包含以下代码:

 dispatch_async(dispatch_get_main_queue(), ^{ [self performSelector:selector withObject:nil afterDelay:1.0]; }); 

select器可能永远不会被调用,因为我们告诉主线程locking,直到testing完成:

 [testBase.lock lockWhenCondition:1]; 

总的来说,我们可以完全摆脱NSConditionLock,而只是简单地使用GHAsyncTestCase类。

这是我在我的代码中使用它的方式:

 @interface NumericTestTests : GHAsyncTestCase { } @end @implementation NumericTestTests { BOOL passed; } - (void)setUp { passed = NO; } - (void)testMe { [self prepare]; MyTest *test = [MyTest new]; [test run: ^(NSError *error, double value) { passed = YES; [self notify:kGHUnitWaitStatusSuccess]; }]; [test runTest:fakeTest]; [self waitForStatus:kGHUnitWaitStatusSuccess timeout:5.0]; GHAssertTrue(passed, @"Completion handler not called"); } 

更干净,不会阻塞主线程。

我刚刚写了一篇关于这个的博客文章(实际上我开了一个博客,因为我认为这是一个有趣的话题)。 我结束了使用方法swizzling,所以我可以调用完成处理程序使用任何我想要的参数而不等待,这对unit testing似乎很好。 像这样的东西:

 - (void)swizzledGeocodeAddressString:(NSString *)addressString completionHandler:(CLGeocodeCompletionHandler)completionHandler { completionHandler(nil, nil); //You can test various arguments for the handler here. } - (void)testGeocodeFlagsComplete { //Swizzle the geocodeAddressString with our own method. Method originalMethod = class_getInstanceMethod([CLGeocoder class], @selector(geocodeAddressString:completionHandler:)); Method swizzleMethod = class_getInstanceMethod([self class], @selector(swizzledGeocodeAddressString:completionHandler:)); method_exchangeImplementations(originalMethod, swizzleMethod); MyGeocoder * myGeocoder = [[MyGeocoder alloc] init]; [myGeocoder geocodeAddress]; //the completion handler is called synchronously in here. //Deswizzle the methods! method_exchangeImplementations(swizzleMethod, originalMethod); STAssertTrue(myGeocoder.geocoded, @"Should flag as geocoded when complete.");//You can test the completion handler code here. } 

博客条目 ,关心任何人。

我的答案是,unit testing在概念上不适合testingasynchronous操作。 一个asynchronous操作,例如对服务器的请求和对响应的处理,不是在一个单元中发生,而是在两个单元中发生。

要将响应与请求联系起来,必须以某种方式阻止两个单元之间的执行,或者维护全局数据。 如果你阻止执行,那么你的程序没有正常执行,如果你维护全局数据,你已经添加了本身可能包含错误的无关function。 这两种解决scheme都违反了unit testing的整个思路,并要求您在应用程序中插入特殊的testing代码; 然后在你的unit testing之后,你将不得不closures你的testing代码,并进行老式的“手动”testing。 花费在unit testing上的时间和精力至less部分是浪费的。