6

我正在使用 OCMockito,我想在我的 ViewController 中测试一个使用 NetworkFetcher 对象和块的方法:

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

特别是,我想模拟fetchInfo:,以便它返回一个虚拟result数组而不访问网络,并验证该reloadData方法是否已在 上调用UITableView并且模型是它应该是什么。

由于这段代码是异步的,我假设我应该以某种方式捕获该块并从我的测试中手动调用它。

我怎样才能做到这一点?

4

2 回答 2

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

我将所有内容都放在一个测试方法中,但移动创建mockedFetchermockedTabletosetUp将在其他测试中为您节省类似代码的行。

于 2014-05-26T19:17:23.090 回答
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

有了这个,你可以为你的测试创建辅助函数。我假设您的被测系统位于测试夹具中,名为 ivar 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——这并不能反映异步时真正发生的情况。通过转移到捕获块然后根据 Eugen 的回答调用它,该块将在reloadTableViewContents完成后执行。这要好得多。

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

    [self runForShortTime];
    [self assertThatCellForRow:0 showsText:@"FOO"];
    [self assertThatCellForRow:1 showsText:@"BAR"];
}
于 2014-05-26T18:50:06.117 回答