OCMock and block testing, executing
Asked Answered
A

3

11

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];
}
Avaunt answered 14/7, 2013 at 23:20 Comment(0)
S
13

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.

Speiss answered 15/7, 2013 at 17:28 Comment(6)
the actual code won't compile, has an error at the line: void(^successBlock)(NSString *authToken), basically incorrect syntax and I've double checked the syntax is the same since I copied it, modifying it for my variables.Avaunt
there's also no getArgumentAtIndex method, there is however a getArgument:atIndex: method, what should go in the getArgument? Updated above with what I have that's compiling and running but throwing an exception.Avaunt
You'll need to make sure the NSInvocation+OCMAdditions.h is included in your project (github.com/erikdoe/ocmock/blob/master/Source/OCMock/…) This give you access to getArgumentAtIndexAsObject. The first two arguments are the self and cmd so you'll want the third if you want your success block.Speiss
*and by "third" I mean index 3... self:cmd:loginWithUserPass:withSuccess:failure I had the above example running locally, so it should work.Speiss
Take care of the '[_loginCntrl loginWithUserPass:userPassD withSuccess:^(NSString *authToken){ // save authToken to credential store [self saveToken:authToken]; } failure:^(NSString *errorMessage) { // alert user pass was wrong }];' The '_loginCntrl' is a strong reference and calling self inside the block will cause a retain cycle. Using '__weak Example wself = self;' would fix it Not important for this example but good practice non the less. ;)Antidisestablishmentarianism
Great , it work for me but I change to void (^successBlock)(id success); [invocation getArgument:&successBlock atIndex:4]; successBlock(OCMOCK_ANY);Starstudded
T
7

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.

Tat answered 6/10, 2013 at 4:1 Comment(0)
N
0

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.

Nardoo answered 15/7, 2013 at 2:26 Comment(2)
This looks handy, but would mean switching from OCMock to Kiwi to get this syntax.Speiss
Ah, I must've overlooked the fact he was using OCMock. +1 for Kiwi then :)Nardoo

© 2022 - 2024 — McMap. All rights reserved.