How to wait for asyn operation in iOS unit test using NSConditionLock

677 Views Asked by At

I have a unit test in which I need to wait for an async task to finish. I am trying to use NSConditionLock as it seems to be a pretty clean solution but I cannot get it to work.

Some test code:

- (void)testSuccess
{  
loginLock = [[NSConditionLock alloc] init];

    Login login = [[Login alloc] init];
    login.delegate = self;

    // The login method will make an async call.
    // I have setup myself as the delegate.
    // I would like to wait to the delegate method to get called
    // before my test finishes
    [login login];

        // try to lock to wait for delegate to get called
    [loginLock lockWhenCondition:1];

        // At this point I can do some verification

    NSLog(@"Done running login test");
}

// delegate method that gets called after login success
- (void) loginSuccess {
    NSLog(@"login success");

    // Cool the delegate was called this should let the test continue
    [loginLock unlockWithCondition:1];
}

I was trying to follow the solution here: How to unit test asynchronous APIs?

My delegate never gets called if I lock. If I take out the lock code and put in a simple timer it works fine.

Am I locking the entire thread and not letting the login code run and actually make the async call?

I also tried this to put the login call on a different thread so it does not get locked.

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
     [login login];
});

What am I doing wrong?

EDIT adding login code. Trimmed do the code for readability sake. Basically just use AFNetworking to execute a POST. When done will call delegate methods. Login make a http request:

NSString *url = [NSString stringWithFormat:@"%@/%@", [_baseURL absoluteString], @"api/login"];
[manager POST:url parameters:parameters success:^(AFHTTPRequestOperation *operation, id responseObject) {
    if (_delegate) {
        [_delegate loginSuccess];
    }
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
    if (_delegate) {
        [_delegate loginFailure];
    }
}];
2

There are 2 best solutions below

7
On

The answer can be found in https://github.com/AFNetworking/AFNetworking/blob/master/AFNetworking/AFHTTPRequestOperation.m.

Since you are not setting the completionQueue property of the implicitly created AFHTTPRequestOperation, it is scheduling the callbacks on the main queue, which you are blocking.

5
On

Unfortunately, many answers (not all) in the given SO thread ("How to unit test asynchronous APIs?") are bogus and contain subtle issues. Most authors don't care about thread-safity, the need for memory-barriers when accessing shared variables, and how run loops do work actually. In effect, this leads to unreliable and ineffective code.

In your example, the culprit is likely, that your delegate methods are dispatched on the main thread. Since you are waiting on the condition lock on the main thread as well, this leads to a dead lock. One thing, the most accepted answer that suggests this solution does not mention at all.

A possible solution:

First, change your login method so that it has a proper completion handler parameter, which a call-site can set in order to figure that the login process is complete:

typedef void (^void)(completion_t)(id result, NSError* error);

- (void) loginWithCompletion:(completion_t)completion;

After your Edit:

You could implement your login method as follows:

- (void) loginWithCompletion:(completion_t)completion 
{
    NSString *url = [NSString stringWithFormat:@"%@/%@", [_baseURL absoluteString], @"api/login"];
    [manager POST:url parameters:parameters success:^(AFHTTPRequestOperation *operation, id responseObject) {
        if (completion) {
            completion(responseObject, nil);
        }
    } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
        if (completion) {
            completion(nil, error);
        }
    }];

Possible usage:

[self loginWithCompletion:^(id result, NSError* error){
    if (error) {
        [_delegate loginFailure:error];
    }
    else {
         // Login succeeded with "result"
        [_delegate loginSuccess];
    }
}];

Now, you have an actual method which you can test. Not actually sure WHAT you are trying to test, but for example:

-(void) testLoginController {

     // setup Network MOCK and/or loginController so that it fails:
     ...
     [loginController loginWithCompletion:^(id result, NSError*error){
         XCTAssertNotNil(error, @"");
         XCTAssert(...);

         <signal completion>
     }];


     <wait on the run loop until completion>

     // Test possible side effects:
     XCTAssert(loginController.isLoggedIn == NO, @""):
}

For any other further steps, this may help:

If you don't mind to utilize a third party framework, you can then implement the <signal completion> and <wait on the run loop until completion> tasks and other things as described here in this answer: Unit testing Parse framework iOS