0

我有一个单元测试,我需要等待异步任务完成。我正在尝试使用 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];
}

我试图在这里遵循解决方案: 如何对异步 API 进行单元测试?

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

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

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

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

2 回答 2

0

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

由于您没有设置completionQueue隐式 created 的属性AFHTTPRequestOperation,因此它正在调度您正在阻塞的主队列上的回调。

于 2014-02-27T17:26:46.137 回答
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>Unit testing Parse framework iOS<wait on the run loop until completion>

于 2014-02-27T17:27:51.687 回答