13

I am facing a problem while unit testing an asynchronous call in iOS. (Although it is working fine in view controllers.)

Has anyone faced this issue before? I have tried using a wait function but I'm still facing the same problem.

Please suggest an example of a good way to do this.

0

12 Answers 12

28

You'll need to spin the runloop until your callback is invoked. Make sure that it gets invoked on the main queue, though.

Try this:

__block BOOL done = NO;
doSomethingAsynchronouslyWithBlock(^{
    done = YES;
});

while(!done) {
   [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}

You can also use a semaphore (example below), but I prefer to spin the runloop to allow asynchronous blocks dispatched to the main queue to be processed.

dispatch_semaphore_t sem = dispatch_semaphore_create(0);
doSomethingAsynchronouslyWithBlock(^{
    //...
    dispatch_semaphore_signal(sem);
});

dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
2
  • 1
    For those who do have issues with the run loop approach: it won't work correctly: the method runMode:beforeDate: will not return until after a source's event has been processed. That might never happen (unless the unit test explicitly performs that in the completion handler somehow) ;) Commented Oct 14, 2013 at 13:38
  • I used the combination of your solution with a Notification that's already called at finish of my method. Thanks! Commented Mar 25, 2015 at 12:36
14

Here is Apple's description of native support for async testing.

TL;DR manual:

Look at XCTextCase+AsynchronousTesting.h

There is special class XCTestExpectation with only one public method: - (void)fulfill;

You should init instance of this class and in success case call fulfill method. Otherwise your test will fail after timeout that you specify in that method:

- (void)waitForExpectationsWithTimeout:(NSTimeInterval)timeout handler:(XCWaitCompletionHandler)handlerOrNil;

Example:

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

    }];
}
8

I think many of the suggested solutions in this post has the problem that if the asynchronous operation does not complete the "done" flag is never set, and the test will hang forever.

I have successfully used this approach in many of my test.

- (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;
}
0
5

Since Xcode 6 this built in to XCTest as a category:

See https://stackoverflow.com/a/24705283/88164

0
3

Here's another alternative, XCAsyncTestCase, that works well with OCMock if you need to use it. It's based on GHUnit's async tester, but is uses the regular XCTest framework instead. Fully compatible with Xcode Bots.

https://github.com/iheartradio/xctest-additions

Usage is the same, just import and subclass 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, e.g login operation failed
    //    kXCTUnitWaitStatusCancelled,  indicates the operation was cancelled
    [self waitForStatus:kXCTUnitWaitStatusSuccess timeout:2.0];
}
0
3

AGAsyncTestHelper is a C macro for writing unit tests with asynchronous operations and works with both SenTestingKit and XCTest.

Simple and to the point

- (void)testAsyncBlockCallback
{
    __block BOOL jobDone = NO;

    [Manager doSomeOperationOnDone:^(id data) {
        jobDone = YES; 
    }];

    WAIT_WHILE(!jobDone, 2.0);
}
3

Sam Brodkin already gave the right answer.

Just to make the answer looks better at first sight, I bring the sample code here.

Use 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];
    }];
}
2

you can use async api calling in swift like this

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)")
        }
    }
}
1

I suggest you should have a look on the tests of Facebook-ios-sdk. It's a good example of how to test async unit test on iOS, though personally I think async tests should be break into sync tests.

FBTestBlocker: a blocker that prevent current thread exits with specified timeout. You can drag and drop this to your project, but you need to remove OCMock related stuff if you don't have that in you project.

FBTestBlocker.h

FBTestBlocker.m

FBURLConnectionTests: test examples you should look at.

FBURLConnectionTests.h

FBURLConnectionTests.m

This code snippet should give you some idea

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

Try KIWI framework. It's powerful and might help you with other kinds of tests.

0

I recommend you connection semaphore + runloop, i also wrote method which take block:

// 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)

method:

typedef void(^FLTestAsynchronousBlock)(void(^completion)(void));

void FLTestAsynchronous(FLTestAsynchronousBlock block) {
    FLSTART();
    block(^{
        FLEND();
    });
    FLWAIT();
};

and call

FLTestAsynchronous(^(void(^completion)()){

    [networkManager signOutUser:^{
        expect(networkManager.currentUser).to.beNil();
        completion();
    } errorBlock:^(NSError *error) {
        expect(networkManager.currentUser).to.beNil();
        completion();
    }];

});
0

If you are using XCode 6, you can test async network calls like this:

XCTest and asynchronous testing in Xcode 6

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.