Некоторая проверка с использованием блочных методов и 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"];
}