How to unit test asynchronous APIs?

后端 未结 13 949
孤城傲影
孤城傲影 2020-11-30 17:34

I have installed Google Toolbox for Mac into Xcode and followed the instructions to set up unit testing found here.

It all works great, and I can test my synchronous

相关标签:
13条回答
  • 2020-11-30 18:02

    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.

    0 讨论(0)
  • 2020-11-30 18:04

    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.

    0 讨论(0)
  • 2020-11-30 18:05

    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.
    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. Failures in that suite may mean the web service has changes, or is simply down. 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 (e.g. [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; 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. There are libraries that will reopen NSURLConnection (amongst other classes) and do this for you. For example: https://github.com/pivotal/PivotalCoreKit/blob/master/SpecHelperLib/Extensions/NSURLConnection%2BSpec.m

    0 讨论(0)
  • I implemented the solution proposed by Thomas Tempelmann and overall it works fine for me.

    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.

    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.

    0 讨论(0)
  • 2020-11-30 18:09

    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:

    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.

    0 讨论(0)
  • 2020-11-30 18:14

    I found this article on this which is a muc http://dadabeatnik.wordpress.com/2013/09/12/xcode-and-asynchronous-unit-testing/

    0 讨论(0)
提交回复
热议问题