【问题标题】:How to wait for asyn operation in iOS unit test using NSConditionLock如何使用 NSCondition Lock 在 iOS 单元测试中等待异步操作
【发布时间】:2014-02-27 16:41:29
【问题描述】:

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

【问题讨论】:

  • -login 方法到底有什么作用?你能给我们看一些代码吗?
  • 简单的 AFNetworking 调用。出于测试目的,我使用 URLMock 来模拟响应。
  • 我也试过了:loginLock = [[NSConditionLock alloc] initWithCondition:0];没有运气
  • AFNetworking 是否尝试在等待条件的主队列上调度回调?
  • 不知道。我试图将整个调用放在另一个线程上,看看是否可以避免这种情况,但没有运气。

标签: ios unit-testing asynchronous nscondition


【解决方案1】:

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

由于您没有设置隐式创建的AFHTTPRequestOperationcompletionQueue 属性,它正在调度主队列上的回调,而您正在阻止它。

【讨论】:

  • 答案真的是改变代码以返回不同的队列,以便单元测试有效吗?这是使用 NSCondtionLock 的唯一方法吗?如果是这样,也许我需要重新审视并找出更好的方法等待完成。
  • 甚至不知道在我的示例中如何设置完成块。
  • @lostintranslation 您需要将完成处理程序参数添加到您的 login 方法。每个异步方法都应该(即 99% 的必须)有一个完成处理程序或其他一些方法来向调用站点发出完成信号。
  • 这就是我使用委托的目的。您是在谈论将块传递给登录方法吗?我不想这样做,因为 LoginModule 将尝试更新过期的令牌,我也想通知我的代表。在这种情况下,我真的看不出委托和块之间的区别,任何一个都表明稍后发生了一些事情。但我经常错,还在学习;)
  • @lostintranslation 在这种情况下,您的代表将收到完成的信号。还行吧;然而,测试就不同了:你需要访问委托,甚至是模拟委托。
【解决方案2】:

不幸的是,给定 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, @""):
}

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

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

【讨论】:

  • 这看起来很甜蜜。有没有想过创建一个 pod 规范,以便可以将其与可可豆荚一起使用?
  • @lostintranslation 我在项目中使用 Xcconfig 文件 - 这与 POD 冲突。不幸的是,这个问题没有解决方案。但是,安装 lib 非常容易。
  • 同意它可能很容易安装,但现在我必须将其签入我的 repo 并跟踪版本并自行处理升级。不要不同意它只是一个无赖的冲突,因为使用依赖管理系统来拉入 deps 是一个不错的方法。
  • @lostintranslation 嗯,添加 POD 规范怎么样?它的开源。我也很欣赏这个功能 - 只需删除 xcconfig 文件并确保它工作;)
  • @lostintranslation 顺便说一句,RXPromise 现在有一个 Podspec,尽情享受吧! ;)
猜你喜欢
  • 1970-01-01
  • 2019-12-09
  • 1970-01-01
  • 2017-06-01
  • 1970-01-01
  • 2018-03-01
  • 1970-01-01
  • 2012-05-20
相关资源
最近更新 更多