OCMock and block testing, executing

2019-03-15 12:08发布

Here's the method under test:

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

what I want to test is that in that success block the other dependency/OCMockObject _credStore is called with the appropriate methods. So currently the loginCtrl and credStore dependencies are OCMockObjects and I can stub/expect on those.

Would I stub loginController to somehow execute that block when called? I've looked at some of the questions on stubbing blocks with OCMock and I can't wrap my head around what they're doing and if it would be suitable for this situation.

In reality all I want to do is OCMock to fire the block ([success invoke]??) so that the code _credStore saveUserPass is done and can be verified on _credStore.

where I stopped:

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

ETA: here's what I have based on Ben's example below, but not working, getting a EXC_BAD_ACCESS exception:

// 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条回答
走好不送
2楼-- · 2019-03-15 12:25

I think you can do this with a spy. Reading this wiki page on spies it looks like you can capture the block passed in and invoke it yourself in the test.

查看更多
乱世女痞
3楼-- · 2019-03-15 12:29

If I follow you right, this may do what you want:

@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

This code allows forces the real success block to be invoked with the argument you specify, which you can then verify.

查看更多
甜甜的少女心
4楼-- · 2019-03-15 12:35

Code I'm working with is heavily based on blocks, so I'm super familiar with your question.

Just to rephrase problem a bit:

- (ReturnObject *)methodThatWeWantToTest:(Foobar *)bar
{    
       [self.somethingElse doSomethingAndCallBlock:^{
         // really want to test this code
       } secondBock:^{
        // even more code to test
       }];
}

To solve exactly the same problem you're bringing up here, we've created unit test helper class that has methods with identical signature to methods that call blocks that we need to test.

For you code sample, it is a mock method that returns nothing, takes one id argument and two blocks.

Below is example of the mock method you'd need and sample of the OCMock unit test that utilizes

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

Hope it helps! Let me know if you have any questions, we've got it working.

查看更多
登录 后发表回答