Некоторая проверка с использованием блочных методов и OCMockito

Я использую OCMockito и хочу протестировать метод в моем ViewController, который использует объект NetworkFetcher и блок:

- (void)reloadTableViewContents
{
    [self.networkFetcher fetchInfo:^(NSArray *result, BOOL success) {
        if (success) {
            self.model = result;
            [self.tableView reloadData];
        }
    }];
}

В частности, я бы хотел издеваться fetchInfo: так что возвращает манекен result массив, не попадая в сеть, и убедитесь, что reloadData метод был вызван на UITableView и модель такая, какой она должна быть.

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

Как я могу сделать это?

2 ответа

Это довольно просто:

- (void) testDataWasReloadAfterInfoFetched 
{
    NetworkFetcher mockedFetcher = mock([NetowrkFetcher class]);
    sut.networkFetcher = mockedFetcher;

    UITableView mockedTable = mock([UITableView class]);
    sut.tableView = mockedTable;

    [sut reloadTableViewContents];

    MKTArgumentCaptor captor = [MKTArgumentCaptor new];
    [verify(mockedFetcher) fetchInfo:[captor capture]];

    void (^callback)(NSArray*, BOOL success) = [captor value];

    NSArray* result = [NSArray new];
    callback(result, YES);

    assertThat(sut.model, equalTo(result));
    [verify(mockedTable) reloadData];
}

Я положил все в один метод испытаний, но перемещая создание mockedFetcher а также mockedTable в setUp сэкономит вам строки аналогичного кода в других тестах.

(Изменить: см. Ответ Евгения и мой комментарий. Его использование MKTArgumentCaptor от OCMockito не только устраняет необходимость в FakeNetworkFetcher, но в результате получается лучший тестовый поток, который отражает фактический поток. См. Мою заметку "Редактировать" в конце.)

Ваш реальный код асинхронный только из-за реального networkFetcher, Замените это подделкой. В этом случае я бы использовал раскрученную вручную подделку вместо OCMockito:

@interface FakeNetworkFetcher : NSObject
@property (nonatomic, strong) NSArray *fakeResult;
@property (nonatomic) BOOL fakeSuccess;
@end

@implementation FakeNetworkFetcher

- (void)fetchInfo:(void (^)(NSArray *result, BOOL success))block {
    if (block)
        block(self.fakeResult, self.fakeSuccess);
}

@end

При этом вы можете создавать вспомогательные функции для ваших тестов. Я предполагаю, что ваша тестируемая система находится в тестовом устройстве как ивар с именем sut:

- (void)setUpFakeNetworkFetcherToSucceedWithResult:(NSArray *)fakeResult {
    sut.networkFetcher = [[FakeNetworkFetcher alloc] init];
    sut.networkFetcher.fakeSuccess = YES;
    sut.networkFetcher.fakeResult = fakeResult;
}

- (void)setUpFakeNetworkFetcherToFail
    sut.networkFetcher = [[FakeNetworkFetcher alloc] init];
    sut.networkFetcher.fakeSuccess = NO;
}

Теперь ваш тест пути успеха должен убедиться, что ваше табличное представление перезагружено с обновленной моделью. Вот первая наивная попытка:

- (void)testReloadTableViewContents_withSuccess_ShouldReloadTableWithResult {
    // given
    [self setUpFakeNetworkFetcherToSucceedWithResult:@[@"RESULT"]];
    sut.tableView = mock([UITablewView class]);

    // when
    [sut reloadTableViewContents];

    // then
    assertThat(sut.model, is(@[@"RESULT"]));
    [verify(sut.tableView) reloadData];
}

К сожалению, это не гарантирует обновления модели до reloadData сообщение. Но вам все равно понадобится другой тест, чтобы убедиться, что полученный результат представлен в ячейках таблицы. Это можно сделать, сохраняя реальный UITableView и позволяя циклу выполнения продвигаться с помощью этого вспомогательного метода:

- (void)runForShortTime {
    [[NSRunLoop currentRunLoop] runUntilDate:[NSDate date]];
}

Наконец, вот тест, который начинает хорошо выглядеть для меня:

- (void)testReloadTableViewContents_withSuccess_ShouldShowResultInCell {
    // given
    [self setUpFakeNetworkFetcherToSucceedWithResult:@[@"RESULT"]];

    // when
    [sut reloadTableViewContents];

    // then
    [self runForShortTime];
    NSIndexPath *firstRow = [NSIndexPath indexPathForRow:0 inSection:0];
    UITableViewCell *firstCell = [sut.tableView cellForRowAtIndexPath:firstRow];
    assertThat(firstCell.textLabel.text, is(@"RESULT"));
}

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

- (void)assertThatCellForRow:(NSInteger)row showsText:(NSString *)text {
    NSIndexPath *indexPath = [NSIndexPath indexPathForRow:row inSection:0];
    UITableViewCell *cell = [sut.tableView cellForRowAtIndexPath:indexPath];
    assertThat(cell.textLabel.text, is(equalTo(text)));
}

С этим вот тест, который использует наши различные вспомогательные методы, чтобы быть выразительным и довольно устойчивым:

- (void)testReloadTableViewContents_withSuccess_ShouldShowResultsInCells {
    [self setUpFakeNetworkFetcherToSucceedWithResult:@[@"FOO", @"BAR"]];

    [sut reloadTableViewContents];

    [self runForShortTime];
    [self assertThatCellForRow:0 showsText:@"FOO"];
    [self assertThatCellForRow:1 showsText:@"BAR"];
}

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

Редактировать: теперь я вижу, что с моим FakeNetworkFetcher, блок выполняется в середине reloadTableViewContents - что не отражает того, что действительно произойдет, когда оно асинхронно. Если перейти к захвату блока, а затем вызвать его в соответствии с ответом Евгения, блок будет выполнен после reloadTableViewContents завершается. Это намного лучше.

- (void)testReloadTableViewContents_withSuccess_ShouldShowResultsInCells {
    [sut reloadTableViewContents];
    [self simulateNetworkFetcherSucceedingWithResult:@[@"FOO", @"BAR"]];

    [self runForShortTime];
    [self assertThatCellForRow:0 showsText:@"FOO"];
    [self assertThatCellForRow:1 showsText:@"BAR"];
}
Другие вопросы по тегам