64

我已将Google Toolbox for Mac安装到 Xcode 中,并按照此处的说明设置单元测试。

这一切都很好,我可以在我的所有对象上测试我的同步方法绝对没问题。但是,我实际上想测试的大多数复杂 API 通过调用委托上的方法异步返回结果 - 例如,对文件下载和更新系统的调用将立即返回,然后在文件完成下载时运行 -fileDownloadDidComplete: 方法.

我将如何将其作为单元测试进行测试?

似乎我想要 testDownload 函数,或者至少是测试框架“等待” fileDownloadDidComplete: 方法运行。

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

例如:

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

13 回答 13

52

我遇到了同样的问题,并找到了适合我的不同解决方案。

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

// 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];

然后在代表中。

于 2010-04-11T18:29:30.370 回答
44

我很感激这个问题是在大约一年前被问到和回答的,但我不禁不同意给定的答案。测试异步操作,尤其是网络操作,是一个非常普遍的要求,并且正确地进行测试很重要。在给定的示例中,如果您依赖于实际的网络响应,您将失去测试的一些重要价值。具体来说,您的测试依赖于您正在与之通信的服务器的可用性和功能正确性;这种依赖性使您的测试

  • 更脆弱(如果服务器宕机会发生什么?)
  • 不太全面(您如何始终如一地测试故障响应或网络错误?)
  • 想象一下测试这个要慢得多:

单元测试应该在几分之一秒内运行。如果您每次运行测试时都必须等待数秒的网络响应,那么您不太可能经常运行它们。

单元测试主要是关于封装依赖关系;从被测代码的角度来看,发生了两件事:

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

您的委托不关心或不应该关心响应来自何处,无论是来自远程服务器的实际响应还是来自您的测试代码。您可以利用这一点来测试异步操作,只需自己生成响应即可。您的测试将运行得更快,并且您可以可靠地测试成功或失败响应。

这并不是说您不应该针对您正在使用的真实 Web 服务运行测试,但这些是集成测试并且属于它们自己的测试套件。该套件中的故障可能意味着 Web 服务发生了变化,或者只是停机。由于它们更脆弱,自动化它们的价值往往低于自动化单元测试。

关于如何准确地测试对网络请求的异步响应,您有几个选择。您可以通过直接调用方法(例如 [someDelegate connection:connection didReceiveResponse:someResponse])简单地单独测试委托。这会有点工作,但有点错误。您的对象提供的委托可能只是特定 NSURLConnection 对象的委托链中的多个对象之一;如果你直接调用你的委托的方法,你可能会错过另一个委托提供的一些关键功能。作为更好的选择,您可以存根您创建的 NSURLConnection 对象,并让它将响应消息发送到其整个委托链。有些库会重新打开 NSURLConnection(在其他类中)并为您执行此操作。https://github.com/pivotal/PivotalCoreKit/blob/master/SpecHelperLib/Extensions/NSURLConnection%2BSpec.m

于 2010-11-14T22:28:16.033 回答
19

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

这就是我使用您的建议的方式。

'Downloader' 定义了一个协议,它的方法 DownloadDidComplete 在完成时触发。有一个 BOOL 成员变量 '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!");
}

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

于 2010-01-29T15:13:43.563 回答
16

我写了一个小助手,可以很容易地测试异步 API。首先是助手:

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,因此请确保在完成后设置它。当然,如果您愿意,您可以为助手添加超时,

于 2012-12-31T09:38:51.613 回答
8

这很棘手。我认为您需要在测试中设置一个运行循环,并且还需要能够为您的异步代码指定该运行循环。否则回调将不会发生,因为它们是在运行循环上执行的。

我想你可以在一个循环中运行 runloop 短时间。并让回调设置一些共享状态变量。或者甚至可以简单地要求回调终止运行循环。这样你就知道考试结束了。您应该能够在特定时间后通过 stoppng 循环来检查超时。如果发生这种情况,则会发生超时。

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

于 2010-01-29T14:36:44.927 回答
6

如果您使用诸如 AFNetworking 或 ASIHTTPRequest 之类的库,并且通过 NSOperation(或这些库的子类)管理您的请求,那么很容易使用 NSOperationQueue 对测试/开发服务器进行测试:

在测试中:

// create request operation

NSOperationQueue* queue = [[NSOperationQueue alloc] init];
[queue addOperation:request];
[queue waitUntilAllOperationsAreFinished];

// verify response

这实质上是运行一个运行循环,直到操作完成,允许所有回调像往常一样在后台线程上发生。

于 2012-05-17T13:15:19.443 回答
6

要详细说明@St3fan的解决方案,您可以在发起请求后尝试:

- (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;
        }
    }
于 2012-05-19T20:27:24.783 回答
3

我发现使用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

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

- (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];
}
于 2014-04-08T20:36:17.577 回答
2

我实施了 Thomas Tempelmann 提出的解决方案,总的来说它对我来说很好。

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

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

选择器可能永远不会被调用,因为我们告诉主线程在测试完成之前锁定:

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

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

于 2013-01-10T10:12:45.620 回答
1

我刚刚写了一篇关于这个的博客文章(事实上我开始写博客是因为我认为这是一个有趣的话题)。我最终使用了方法调配,因此我可以使用我想要的任何参数调用完成处理程序而无需等待,这对于单元测试来说似乎很好。像这样的东西:

- (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. 
}

任何关心的人的博客条目。

于 2013-04-15T10:37:33.407 回答
0

我的回答是,从概念上讲,单元测试不适合测试异步操作。异步操作,例如对服务器的请求和响应的处理,不是在一个单元中发生,而是在两个单元中发生。

要将响应与请求相关联,您必须以某种方式阻止两个单元之间的执行,或者维护全局数据。如果您阻止执行,那么您的程序将无法正常执行,并且如果您维护全局数据,则您添加了本身可能包含错误的无关功能。任何一种解决方案都违反了单元测试的整体理念,并且需要您在应用程序中插入特殊的测试代码;然后在您的单元测试之后,您仍然必须关闭您的测试代码并进行老式的“手动”测试。花费在单元测试上的时间和精力至少部分被浪费了。

于 2012-05-30T23:30:29.850 回答
0

我发现这篇文章是一个 muc http://dadabeatnik.wordpress.com/2013/09/12/xcode-and-asynchronous-unit-testing/

于 2014-01-28T10:02:42.920 回答