0

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];
    }
}];
Community
  • 1
  • 1
lostintranslation
  • 23,756
  • 50
  • 159
  • 262

2 Answers2

0

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.

Stefan Fisk
  • 1,563
  • 13
  • 19
  • Is the answer really to change the code to return on a different queue just so the unit test works? Is that the only way this would work using NSCondtionLock? If so maybe I need to revisit and figure out a better way wait for completion. – lostintranslation Feb 27 '14 at 17:30
  • Not even sure how I would set the completion block in my example. – lostintranslation Feb 27 '14 at 17:33
  • @lostintranslation You need to add a completion handler parameter to your `login` method. Every asynchronous method SHOULD (that is 99% a _must_) have a completion handler OR some other means to signal the completion to the call site. – CouchDeveloper Feb 27 '14 at 17:58
  • that is what I am using the delegate for. Are you talking about passing blocks to the login method? I don't want to do this because the LoginModule will try to renew expired tokens and I want to inform my delegate of that as well. I really don't see the difference between delegate and block in this case, either one is signaling that something happened at a later time. But I am often wrong, still learning ;) – lostintranslation Feb 27 '14 at 18:27
  • @lostintranslation In this case, your delegate will be signaled the completion. This is OK; however, testing is then different: you need to access the delegate, or even MOCK the delegate. – CouchDeveloper Feb 27 '14 at 18:34
  • This is where I get confused. Mock the delegate? If I do that then I cannot ensure my login method is fully tested. That method (when URL request is done) calls the delegate. If I mock that all and then a dev comes in and takes out the real call to the delegate in the login method all tests will still pass but the code is broken. I really do want to test that my login method really does call a delegate method. – lostintranslation Feb 27 '14 at 18:49
  • @lostintranslation When you mock the delegate, then you can test whether the delegate will be correctly invoked and parameters have been passed through correctly as well. So basically, you test the LoginController. In another unit test, you can separately test the possibly various LoginController Delegates. If suitable, you may then mock the LoginController and network. Of course, there's for sure a way to test everything in concert. – CouchDeveloper Feb 28 '14 at 18:41
0

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

Community
  • 1
  • 1
CouchDeveloper
  • 18,174
  • 3
  • 45
  • 67
  • That looks pretty sweet. Ever thought of creating a pod spec such that this could be pulled in with cocoa pods? – lostintranslation Feb 27 '14 at 18:23
  • @lostintranslation I'm using Xcconfig files in the project(s) - and this clashes with PODs. Unfortunately, there is no solution for this issue. However, installing the lib is quite easy. – CouchDeveloper Feb 27 '14 at 18:26
  • agreed it may be easy to install, but now I have to check that in to my repo and track version and handle upgrades on my own. Don't disagree that it conflicts just a bummer, as using a dependency management system to pull in deps is a nice way to go. – lostintranslation Feb 27 '14 at 18:32
  • @lostintranslation Uhm, how about adding the POD spec? Its Open Source. I would appreciate that feature, too - just remove the xcconfig files and make sure it works ;) – CouchDeveloper Feb 27 '14 at 18:36
  • @lostintranslation By the way, RXPromise now has a Podspec, enjoy! ;) – CouchDeveloper Mar 03 '14 at 21:28