OCMock и блочное тестирование, выполнение

Вот тестируемый метод:

- (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 запустить блок ([успешный вызов]??), чтобы код _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];
}

ETA: вот что у меня есть на основе приведенного ниже примера Бена, но не работает, получая исключение 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];
}

person Mark W    schedule 14.07.2013    source источник


Ответы (3)


Если я пойду за вами правильно, это может сделать то, что вы хотите:

@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

Этот код позволяет вызвать реальный блок успеха с указанным аргументом, который затем можно проверить.

person Ben Flynn    schedule 15.07.2013
comment
фактический код не скомпилируется, имеет ошибку в строке: void(^successBlock)(NSString *authToken), в основном неверный синтаксис, и я дважды проверил, синтаксис тот же, так как я скопировал его, изменив его для своих переменных . - person Mark W; 20.07.2013
comment
также нет метода getArgumentAtIndex, однако есть метод getArgument:atIndex:, что должно быть в getArgument? Обновлено выше тем, что у меня есть, которое компилируется и работает, но выдает исключение. - person Mark W; 20.07.2013
comment
Вам необходимо убедиться, что NSInvocation+OCMAdditions.h включен в ваш проект (github.com/erikdoe/ocmock/blob/master/Source/OCMock/) Это дает вам доступ к getArgumentAtIndexAsObject. Первые два аргумента — это self и cmd, поэтому вам понадобится третий, если вы хотите получить блок успеха. - person Ben Flynn; 21.07.2013
comment
* и под третьим я подразумеваю индекс 3... self:cmd:loginWithUserPass:withSuccess:failure У меня был приведенный выше пример, работающий локально, поэтому он должен работать. - person Ben Flynn; 21.07.2013
comment
Позаботьтесь о '[_loginCntrl loginWithUserPass:userPassD withSuccess:^(NSString *authToken){ // сохраните authToken в хранилище учетных данных [self saveToken:authToken]; } failure:^(NSString *errorMessage) { // оповещение о неправильном проходе пользователя }];' «_loginCntrl» — это сильная ссылка, и вызов self внутри блока вызовет цикл сохранения. Используя '__weak Example wself = self;' исправит это. Не важно для этого примера, но, тем не менее, это хорошая практика. ;) - person L_Sonic; 12.12.2013
comment
Отлично, у меня это работает, но я меняю на void (^successBlock)(id success); [вызов getArgument:&successBlock atIndex:4]; успехБлок(OCMOCK_ANY); - person Bruno; 31.05.2017

Код, с которым я работаю, в значительной степени основан на блоках, поэтому я очень хорошо знаком с вашим вопросом.

Просто немного перефразирую проблему:

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

Надеюсь, поможет! Дайте мне знать, если у вас есть какие-либо вопросы, у нас все работает.

person Boris Suvorov    schedule 06.10.2013

Я думаю, вы можете сделать это со шпионом. Читая эту вики-страницу о шпионах, вы можете захватить переданный блок in и вызвать его самостоятельно в тесте.

person Ben Scheirman    schedule 15.07.2013
comment
Это выглядит удобно, но для получения такого синтаксиса потребуется переключиться с OCMock на Kiwi. - person Ben Flynn; 16.07.2013
comment
Ах, я, должно быть, упустил из виду тот факт, что он использовал OCMock. +1 за киви тогда :) - person Ben Scheirman; 16.07.2013