我正在使用OCMockito,我想在我的ViewController中测试一个使用NetworkFetcher对象和块的方法:
- (void)reloadTableViewContents
{
[self.networkFetcher fetchInfo:^(NSArray *result, BOOL success) {
if (success) {
self.model = result;
[self.tableView reloadData];
}
}];
}
特别是,我想模拟fetchInfo:
,以便它返回一个虚拟result
数组,而不会访问网络,并验证reloadData
方法是否在{{UITableView
上调用1}}和模型应该是它。
由于此代码是异步的,我假设我应该以某种方式捕获块并从我的测试中手动调用它。
我该如何做到这一点?
答案 0 :(得分:5)
这很简单:
- (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
会在其他测试中保存类似代码的行。
答案 1 :(得分:4)
(编辑:请参阅Eugen的回答和我的评论。他对OCMockito的MKTArgumentCaptor的使用不仅消除了对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
的ivar:
- (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
的中间执行 - 这并不能反映出它在异步时会发生什么。通过转移到捕获块然后根据Eugen的答案调用它,该块将在reloadTableViewContents
完成后执行。这要好得多。
- (void)testReloadTableViewContents_withSuccess_ShouldShowResultsInCells {
[sut reloadTableViewContents];
[self simulateNetworkFetcherSucceedingWithResult:@[@"FOO", @"BAR"]];
[self runForShortTime];
[self assertThatCellForRow:0 showsText:@"FOO"];
[self assertThatCellForRow:1 showsText:@"BAR"];
}