OCMock和块测试、执行

11

以下是待测试的方法:

- (void)loginWithUser:(NSString *)userName andPass:(NSString *)pass {

    NSDictionary *userPassD = @{@"user":userName,
                                @"pass":pass};
    [_loginCntrl loginWithUserPass:userPassD withSuccess:^(NSString *authToken){
        // save authToken to credential store
    } failure:^(NSString *errorMessage) {
        // alert user pass was wrong
    }];    
}

我想要测试的是,在成功块中是否调用了其他依赖项/OCMockObject _credStore 的适当方法。目前,loginCtrl和credStore依赖项都是OCMockObjects,我可以在这些依赖项上进行存根/期望。

我应该如何存根loginController以使其在调用时执行该块?我查看了一些关于使用OCMock存根块的问题,但我无法理解他们在做什么,以及它是否适用于这种情况。

实际上,我只想让OCMock触发该块([success invoke]??),以便完成代码_credStore saveUserPass并且可以在_credStore上进行验证。

我的进展:

- (void)test_loginWithuserPass_succeeds_should_call_credStore_setAuthToken {

    NSDictionary *userPassD = @{@"user":@"mark",
                                @"pass":@"test"};
    id successBlock = ^ {
        // ??? isn't this done in the SUT?
    };

    [[[_loginController stub] andDo:successBlock] loginWithUserPass:userPassD withSuccess:OCMOCK_ANY failure:OCMOCK_ANY];
    [[_credentialStore expect] setAuthToken:@"passed back value from block"];
    [_docServiceSUT loginWithUser:@"mark" andPass:@"test"];
    [_credentialStore verify];
}

更新:根据Ben下面的示例,我已经做了以下工作,但是无法正常工作,出现EXC_BAD_ACCESS异常:

// OCUnit test method
- (void)test_loginWithUserPass_success_block_should_call_credentials_setAuthToken {

    void (^proxyBlock)(NSInvocation*) = ^(NSInvocation *invocation) {
        void(^successBlock)(NSString *authToken);
        [invocation getArgument:&successBlock atIndex:3]; // should be 3 because my block is the second param
        successBlock(@"myAuthToken");
    };

    [[[_loginController expect] andDo:proxyBlock] loginWithUserPass:OCMOCK_ANY withSuccess:OCMOCK_ANY failure:OCMOCK_ANY];
    [[_credentialStore expect] setAuthToken:@"myAuthToken"];
    [_docServiceSUT loginWithUser:@"mark" andPass:@"myPass"];
    [_loginController verify];
    [_credentialStore verify];
}

//method under test
- (void)loginWithUser:(NSString *)userName andPass:(NSString *)pass {

    NSDictionary *userPassD = @{@"user":userName,
                                @"pass":pass};

    void(^onSuccess)(NSString *) = ^(NSString *authToken){

        [SVProgressHUD dismiss];
        [_credentials setAuthToken:authToken];

        // Ask user to enter the 6 digit authenticator key
        [self askUserForAuthenticatorKey];
    };

    void(^onFailure)(NSString *) = ^(NSString *errorMessage) {

        [SVProgressHUD dismiss];
        [_alertSender sendAlertWithMessage:errorMessage andTitle:@"Login failed"];
    };

    [SVProgressHUD show];
    [_loginCntrl loginWithUserPass:userPassD withSuccess:onSuccess
      failure:onFailure];
}
3个回答

13

如果我理解正确,这可能会实现您想要的:

@interface ExampleLC : NSObject
- (void)loginWithUserPass:userPassD withSuccess:(void (^)(NSString *authToken))successBlock failure:(void (^)(NSString *errorMessage))failureBlock;
@end
@implementation ExampleLC
- (void)loginWithUserPass:userPassD withSuccess:(void (^)(NSString *authToken))successBlock failure:(void (^)(NSString *errorMessage))failureBlock
{
}
@end
@interface Example : NSObject {
    @public
    ExampleLC *_loginCntrl;
}
- (void)saveToken:(NSString *)authToken;
- (void)loginWithUser:(NSString *)userName andPass:(NSString *)pass;
@end
@implementation Example
- (void)saveToken:(NSString *)authToken
{
}
- (void)loginWithUser:(NSString *)userName andPass:(NSString *)pass {

    NSDictionary *userPassD = @{@"user":userName,
                                @"pass":pass};
    [_loginCntrl loginWithUserPass:userPassD withSuccess:^(NSString *authToken){
        // save authToken to credential store
        [self saveToken:authToken];
    } failure:^(NSString *errorMessage) {
        // alert user pass was wrong
    }];
}
@end


@interface loginTest : SenTestCase

@end

@implementation loginTest

- (void)testExample
{
    Example *exampleOrig = [[Example alloc] init];
    id loginCntrl = [OCMockObject mockForClass:[ExampleLC class]];
    [[[loginCntrl expect] andDo:^(NSInvocation *invocation) {
        void (^successBlock)(NSString *authToken) = [invocation getArgumentAtIndexAsObject:3];
        successBlock(@"Dummy");
    }] loginWithUserPass:OCMOCK_ANY withSuccess:OCMOCK_ANY failure:OCMOCK_ANY];
    exampleOrig->_loginCntrl = loginCntrl;
    id example = [OCMockObject partialMockForObject:exampleOrig];
    [[example expect] saveToken:@"Dummy"];
    [example loginWithUser:@"ABC" andPass:@"DEF"];
    [loginCntrl verify];
    [example verify];
}
@end

这段代码允许您强制调用指定参数的真实成功块,然后您可以验证该参数。


实际代码无法编译,出现错误在这一行:void(^successBlock)(NSString *authToken),基本上是语法不正确,我已经仔细检查了语法,因为我只是复制并修改了它以适应我的变量。 - Mark W
2
没有getArgumentAtIndex方法,但是有一个getArgument:atIndex:方法,getArgument中应该放什么?上面已经更新了我编译和运行时遇到的异常。 - Mark W
你需要确保在你的项目中包含了NSInvocation+OCMAdditions.h文件 (https://github.com/erikdoe/ocmock/blob/master/Source/OCMock/NSInvocation%2BOCMAdditions.h)。这将使你可以使用getArgumentAtIndexAsObject方法。前两个参数是`self`和`cmd`,所以如果你想要获取成功回调块,你需要使用第三个参数。 - Ben Flynn
而“third”指的是索引3... self:cmd:loginWithUserPass:withSuccess:failure。我已经在本地运行了上面的示例,所以它“应该”可以工作。 - Ben Flynn
关注“[_loginCntrl loginWithUserPass:userPassD withSuccess:^(NSString *authToken){ // save authToken to credential store [self saveToken:authToken]; } failure:^(NSString *errorMessage) { // alert user pass was wrong }];”。 '_loginCntrl'是一个强引用,在块内部调用self将会导致保留周期。使用'__weak Example wself = self;' 可以解决这个问题,虽然在此示例中不重要,但仍是一个好的实践。 ;) - L_Sonic
太好了,它对我有用,但我更改为 void (^successBlock)(id success); [invocation getArgument:&successBlock atIndex:4]; successBlock(OCMOCK_ANY); - Bruno

7
我正在处理的代码非常基于块,所以我对你的问题非常熟悉。
稍微重新表述一下问题:
- (ReturnObject *)methodThatWeWantToTest:(Foobar *)bar
{    
       [self.somethingElse doSomethingAndCallBlock:^{
         // really want to test this code
       } secondBock:^{
        // even more code to test
       }];
}

为了解决你在这里提出的完全相同的问题,我们创建了一个单元测试辅助类,该类具有与需要测试的块调用方法相同签名的方法。
对于你的代码示例,它是一个模拟方法,返回空值,接受一个id参数和两个块。
以下是所需的模拟方法示例以及使用OCMock单元测试的示例。
- (void)mockSuccessBlockWithOneArgumentTwoBlocks:(id)firstArgument
                                    successBlock:(void (^)(NSString *authtoken))successBlock
                                    failureBlock:(void (^)(NSString *errorMessage))failureBlock    
{
    successBlock(@"mocked unit test auth token");
}

- (void)testLoginWithUserCallsSomethingInCredStoreOnLoginSuccess
{
    LoginService *loginService = [[LoginService alloc] init];

    // init mocks we need for this test
    id credStoreMock = [OCMockObject niceMockForClass:[MyCredStore class]];
    id loginCtrlMock = [OCMockObject niceMockForClass:[MyLoginCtrl class]];

    // force login controller to call success block when called with loginWithUserPass
    // onObject:self - if mock method is in the same unit test, use self. if it is helper object, point to the helper object.
    [[[loginCtrlMock stub] andCall:@selector(mockSuccessBlockWithOneArgumentTwoBlocks:successBlock:failureBlock::) onObject:self] loginWithUserPass:OCMOCK_ANY withSuccess:OCMOCK_ANY failure:OCMOCK_ANY];

    // setup mocks
    loginService.credStore = credStoreMock;
    loginService.loginCtrl = loginCtrlMock;

    // expect/run/verify
    [[credStore expect] callSomethingFromSuccessBlock];
    [loginService loginWithUser:@"testUser" andPass:@"testPass"];
    [credStore verify];
}

希望它有所帮助!如果你有任何问题,请告诉我,我们已经让它工作了。

0

我认为你可以使用 spy 来完成这个任务。通过阅读spy页面上的维基百科,看起来你可以捕捉传递的代码块并在测试中自己调用它。


1
这看起来很方便,但意味着要从OCMock切换到Kiwi以获得这个语法。 - Ben Flynn
1
啊,我可能忽略了他在使用OCMock。那么Kiwi就加一分吧 :) - Ben Scheirman

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接