简体   繁体   English

如何对异步API进行单元测试?

[英]How to unit test asynchronous APIs?

I have installed Google Toolbox for Mac into Xcode and followed the instructions to set up unit testing found here . 我已将Google Toolbox for Mac安装到Xcode中,并按照说明在此处设置单元测试。

It all works great, and I can test my synchronous methods on all my objects absolutely fine. 这一切都很好,我可以在我的所有对象上测试我的同步方法。 However, most of the complex APIs I actually want to test return results asynchronously via calling a method on a delegate - for example a call to a file download and update system will return immediately and then run a -fileDownloadDidComplete: method when the file finishes downloading. 但是,我实际想要测试的大多数复杂API通过调用委托上的方法异步返回结果 - 例如,对文件下载和更新系统的调用将立即返回,然后在文件完成下载时运行-fileDownloadDidComplete:方法。

How would I test this as a unit test? 我如何将其作为单元测试进行测试?

It seems like I'd want to the testDownload function, or at least the test framework to 'wait' for fileDownloadDidComplete: method to run. 好像我想要testDownload函数,或者至少要测试框架'等待'fileDownloadDidComplete:方法来运行。

EDIT: I've now switched to using the XCode built-in XCTest system and have found that TVRSMonitor on Github provides a dead easy way to use semaphores to wait for async operations to complete. 编辑:我现在已经切换到使用XCode内置XCTest系统,并发现Github上的TVRSMonitor提供了一种简单的方法来使用信号量等待异步操作完成。

For example: 例如:

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

I ran into the same question and found a different solution that works for me. 我遇到了同样的问题,发现了一个适合我的不同解决方案。

I use the "old school" approach for turning async operations into a sync flow by using a semaphore as follows: 我使用“旧学校”方法通过使用信号量将异步操作转换为同步流,如下所示:

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

Make sure to invoke 一定要调用

[self.theLock unlockWithCondition:1];

In the delegate(s) then. 然后在代表中。

I appreciate that this question was asked and answered almost a year ago, but I can't help but disagree with the given answers. 我很欣赏这个问题在一年前被提出并得到了回答,但我不禁对这些问题表示不同意见。 Testing asynchronous operations, particularly network operations, is a very common requirement, and is important to get right. 测试异步操作,特别是网络操作,是一个非常常见的要求,并且对于正确的操作非常重要。 In the given example, if you depend on actual network responses you lose some of the important value of your tests. 在给定的示例中,如果您依赖于实际的网络响应,则会丢失测试的一些重要值。 Specifically, your tests become dependent on the availability and functional correctness of the server you're communicating with; 具体来说,您的测试取决于您正在与之通信的服务器的可用性和功能正确性; this dependency makes your tests 这种依赖会使你的测试

  • more fragile (what happens if the server goes down?) 更脆弱(如果服务器出现故障会怎么样?)
  • less comprehensive (how do you consistently test a failure response, or network error?) 不太全面(如何一致地测试故障响应或网络错误?)
  • significantly slower imagine testing this: 想象测试这个显着慢:

Unit tests should run in fractions of a second. 单元测试应该在几分之一秒内完成。 If you have to wait for a multi-second network response each time you run your tests then you're less likely to run them frequently. 如果每次运行测试时都必须等待多秒的网络响应,那么您不太可能经常运行它们。

Unit testing is largely about encapsulating dependencies; 单元测试主要是关于封装依赖关系; from the point of view of your code under test, two things happen: 从您测试的代码的角度来看,有两件事情发生:

  1. Your method initiates a network request, probably by instantiating an NSURLConnection. 您的方法可能通过实例化NSURLConnection来启动网络请求。
  2. The delegate you specified receives a response via certain method calls. 您指定的委托通过某些方法调用接收响应。

Your delegate doesn't, or shouldn't, care where the response came from, whether from an actual response from a remote server or from your test code. 您的代表不会或不应该关注响应的来源,无论是来自远程服务器的实际响应还是来自您的测试代码。 You can take advantage of this to test asynchronous operations by simply generating the responses yourself. 您可以通过自己简单地生成响应来利用此功能来测试异步操作。 Your tests will run much faster, and you can reliably test success or failure responses. 您的测试运行得更快,您可以可靠地测试成功或失败响应。

This isn't to say you shouldn't run tests against the real web service you're working with, but those are integration tests and belong in their own test suite. 这并不是说您不应该针对您正在使用的真实Web服务运行测试,而是那些是集成测试并且属于他们自己的测试套件。 Failures in that suite may mean the web service has changes, or is simply down. 该套件中的失败可能意味着Web服务发生了变化,或者只是简单地失败了。 Since they're more fragile, automating them tends to have less value than automating your unit tests. 由于它们更脆弱,因此自动化它们的价值往往低于自动化单元测试。

Regarding how exactly to go about testing asynchronous responses to a network request, you have a couple options. 关于如何测试对网络请求的异步响应,您有几个选择。 You could simply test the delegate in isolation by calling the methods directly (eg [someDelegate connection:connection didReceiveResponse:someResponse]). 您可以通过直接调用方法来单独测试委托(例如[someDelegate connection:connection didReceiveResponse:someResponse])。 This will work somewhat, but is slightly wrong. 这会有所作为,但稍有不妥。 The delegate your object provides may be just one of multiple objects in the delegate chain for a specific NSURLConnection object; 您的对象提供的委托可能只是特定NSURLConnection对象的委托链中的多个对象之一; if you call your delegate's methods directly you may be missing some key piece of functionality provided by another delegate further up the chain. 如果你直接调用你的代理人的方法,你可能会错过由链上的另一个代表提供的一些关键功能。 As a better alternative, you can stub the NSURLConnection object you create and have it send the response messages to its entire delegate chain. 作为更好的替代方法,您可以存根您创建的NSURLConnection对象,并让它将响应消息发送到其整个委托链。 There are libraries that will reopen NSURLConnection (amongst other classes) and do this for you. 有些库将重新打开NSURLConnection(以及其他类)并为您执行此操作。 For example: https://github.com/pivotal/PivotalCoreKit/blob/master/SpecHelperLib/Extensions/NSURLConnection%2BSpec.m 例如: https//github.com/pivotal/PivotalCoreKit/blob/master/SpecHelperLib/Extensions/NSURLConnection%2BSpec.m

St3fan, you are a genius. St3fan,你是个天才。 Thanks a lot! 非常感谢!

This is how I did it using your suggestion. 这就是我用你的建议做到的。

'Downloader' defines a protocol with a method DownloadDidComplete that fires on completion. 'Downloader'使用方法DownloadDidComplete定义一个协议,该方法在完成时触发。 There's a BOOL member variable 'downloadComplete' that is used to terminate the run loop. 有一个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!");
}

The run-loop could potentially run forever of course.. I'll improve that later! 当然,运行循环可能会永远运行。我稍后会改进它!

I wrote a little helper that makes it easy to test asynchronous API. 我写了一个小助手,可以很容易地测试异步API。 First the helper: 首先是帮手:

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

You can use it like this: 你可以像这样使用它:

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

It will only continue if done becomes TRUE , so make sure to set it once completed. 它只会在done变为TRUE ,因此请务必在完成后设置它。 Of course you could add a timeout to the helper if you like, 当然,如果你愿意,可以给助手添加一个超时,

This is tricky. 这很棘手。 I think you will need to setup a runloop in your test and also the ability to specify that runloop to your async code. 我认为你需要在测试中设置一个runloop,并且能够为你的异步代码指定runloop。 Otherwise the callbacks won't happen since they are executed on a runloop. 否则回调将不会发生,因为它们是在runloop上执行的。

I guess you could just run the runloop for s short duration in a loop. 我猜你可以在一个循环中短时间运行runloop。 And let the callback set some shared status variable. 让回调设置一些共享状态变量。 Or maybe even simply ask the callback to terminate the runloop. 或者甚至可以简单地要求回调终止runloop。 That way you you know the test is over. 那样你就知道测试结束了。 You should be able to check for timeouts by stoppng the loop after a certain time. 您应该能够通过在一段时间后停止循环来检查超时。 If that happens then a timeout ocurred. 如果发生这种情况,则会发生超时。

I've never done this but I will have to soon I think. 我从来没有这样做,但我想不久就会想到。 Please do share your results :-) 请分享你的结果:-)

If you're using a library such as AFNetworking or ASIHTTPRequest and have your requests managed via a NSOperation (or subclass with those libraries) then it's easy to test them against a test/dev server with an NSOperationQueue: 如果您正在使用AFNetworking或ASIHTTPRequest等库并通过NSOperation(或具有这些库的子类)管理您的请求,那么可以使用NSOperationQueue对测试/ dev服务器进行测试:

In test: 在测试中:

// create request operation

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

// verify response

This essentially runs a runloop until the operation has completed, allowing all callbacks to occur on background threads as they normally would. 这基本上运行runloop直到操作完成,允许所有回调在正常情况下在后台线程上发生。

To elaborate on @St3fan's solution, you can try this after initiating the request: 要详细说明@ 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;
}

Another way: 其他方式:

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

I find it very convenient to use https://github.com/premosystems/XCAsyncTestCase 我发现使用https://github.com/premosystems/XCAsyncTestCase非常方便

It adds three very handy methods to XCTestCase 它为XCTestCase添加了三个非常方便的方法

@interface XCTestCase (AsyncTesting)

- (void)waitForStatus:(XCTAsyncTestCaseStatus)status timeout:(NSTimeInterval)timeout;
- (void)waitForTimeout:(NSTimeInterval)timeout;
- (void)notify:(XCTAsyncTestCaseStatus)status;

@end

that allow very clean tests. 允许非常干净的测试。 An example from the project itself: 项目本身的一个例子:

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

I implemented the solution proposed by Thomas Tempelmann and overall it works fine for me. 我实施了Thomas Tempelmann提出的解决方案,总体而言,它对我来说很好。

However, there is a gotcha. 但是,有一个问题。 Suppose the unit to be tested contains the following code: 假设要测试的单元包含以下代码:

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

The selector may never be called as we told the main thread to lock until the test completes: 可能永远不会调用选择器,因为我们告诉主线程在测试完成之前锁定:

[testBase.lock lockWhenCondition:1];

Overall, we could get rid of the NSConditionLock altogether and simply use the GHAsyncTestCase class instead. 总的来说,我们可以完全摆脱NSConditionLock并简单地使用GHAsyncTestCase类。

This is how I use it in my code: 这是我在我的代码中使用它的方式:

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

Much cleaner and doesn't block the main thread. 更干净,并没有阻止主线程。

I just wrote a blog entry about this (in fact I started a blog because I thought this was an interesting topic). 我刚刚写了一篇关于此的博客文章(事实上我开了一个博客,因为我认为这是一个有趣的话题)。 I ended up using method swizzling so I can call the completion handler using any arguments I want without waiting, which seemed good for unit testing. 我最终使用方法调配,所以我可以使用我想要的任何参数调用完成处理程序而无需等待,这对于单元测试似乎很好。 Something like this: 像这样的东西:

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

blog entry for anyone that cares. 任何关心的人的博客条目

My answer is that unit testing, conceptually, is not suitable for testing asynch operations. 我的答案是,概念上,单元测试不适合测试异步操作。 An asynch operation, such as a request to the server and the handling of the response, happens not in one unit but in two units. 异步操作(例如对服务器的请求和响应的处理)不是在一个单元中发生,而是在两个单元中发生。

To relate the response to the request you must either somehow block execution between the two units, or maintain global data. 要将响应与请求相关联,您必须以某种方式阻止两个单元之间的执行,或者维护全局数据。 If you block execution then your program is not executing normally, and if you maintain global data you have added extraneous functionality that may itself contain errors. 如果阻止执行,那么您的程序没有正常执行,如果您维护全局数据,则添加了可能本身包含错误的无关功能。 Either solution violates the whole idea of unit testing and requires you to insert special testing code into your application; 这两种解决方案都违反了单元测试的整体思路,并要求您在应用程序中插入特殊的测试代码; and then after your unit testing, you will still have to turn off your testing code and do old-fashioned "manual" testing. 然后在进行单元测试后,您仍然需要关闭测试代码并进行老式的“手动”测试。 The time and effort spent on unit testing is then at least partly wasted. 然后,花在单元测试上的时间和精力至少部分地被浪费了。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM