如何使用NSConditionLock在iOS单元测试中等待异步操作

时间:2014-02-27 16:41:30

标签: ios unit-testing asynchronous nscondition

我有一个单元测试,我需要等待异步任务完成。我正在尝试使用NSConditionLock,因为它似乎是一个非常干净的解决方案,但我无法让它工作。

一些测试代码:

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

我试图在这里遵循解决方案: How to unit test asynchronous APIs?

如果我锁定,我的委托永远不会被调用。如果我取出锁码并放入一个简单的计时器,它就能正常工作。

我是否锁定整个线程并且不让登录代码运行并实际进行异步调用?

我也尝试过将登录调用放在另一个线程上,这样它就不会被锁定。

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

我做错了什么?

编辑添加登录代码。为了便于阅读,修剪了代码。基本上只需使用AFNetworking来执行POST。完成后将调用委托方法。 登录发出http请求:

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 个答案:

答案 0 :(得分:0)

答案可以在https://github.com/AFNetworking/AFNetworking/blob/master/AFNetworking/AFHTTPRequestOperation.m中找到。

由于您没有设置隐式创建的completionQueue的{​​{1}}属性,因此它会调度您阻止的主队列上的回调。

答案 1 :(得分:0)

不幸的是,给定的SO线程中的许多答案(不是全部)(“如何对异步API进行单元测试?”)都是假的,并且包含微妙的问题。大多数作者不关心线程安全性,访问共享变量时需要内存屏障,以及运行循环如何实际工作。实际上,这会导致代码不可靠和无效。

在您的示例中,可能是罪魁祸首,您的委托方法将在主线程上调度。由于您正在等待主线程上的条件锁定,因此会导致死锁。有一点,最接受的答案表明这个解决方案根本没有提及。

可能的解决方案:

首先,更改您的login方法,使其具有正确的完成处理程序参数,调用站点可以设置该参数以确定登录过程是否已完成:

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

- (void) loginWithCompletion:(completion_t)completion;

编辑后:

您可以按如下方式实施登录方法:

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

可能的用法:

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

现在,您有一个可以测试的实际方法。实际上并不确定你要测试的是什么,但是例如:

-(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, @""):
}

对于任何其他进一步的步骤,这可能有所帮助:

如果您不介意使用第三方框架,则可以实施<signal completion><wait on the run loop until completion>任务以及此答案中所述的其他内容:Unit testing Parse framework iOS