一些验证涉及块方法和OCMockito

Jua*_*iaz 6 unit-testing ios ocmockito ochamcrest

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

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

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

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

我怎么能做到这一点?

Eug*_*nov 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];
}
Run Code Online (Sandbox Code Playgroud)

我把一切都放在一个测试方法,但移动的创作mockedFetcher,并mockedTablesetUp将节省您在其他测试中的类似的代码行.


Jon*_*eid 4

编辑:请参阅 Eugen 的回答和我的评论。他使用 OCMockito 的 MKTArgumentCaptor 不仅消除了对 的需要FakeNetworkFetcher,而且还产生了反映实际流程的更好的测试流程。请参阅最后我的编辑注释.)

\n\n

您的真实代码是异步的,只是因为真实的networkFetcher. 换个假的吧 在这种情况下,我会使用手卷假货而不是 OCMockito:

\n\n
@interface FakeNetworkFetcher : NSObject\n@property (nonatomic, strong) NSArray *fakeResult;\n@property (nonatomic) BOOL fakeSuccess;\n@end\n\n@implementation FakeNetworkFetcher\n\n- (void)fetchInfo:(void (^)(NSArray *result, BOOL success))block {\n    if (block)\n        block(self.fakeResult, self.fakeSuccess);\n}\n\n@end\n
Run Code Online (Sandbox Code Playgroud)\n\n

这样,您就可以为您的测试创建辅助函数。我假设您的被测系统位于测试装置中,名为 ivar sut

\n\n
- (void)setUpFakeNetworkFetcherToSucceedWithResult:(NSArray *)fakeResult {\n    sut.networkFetcher = [[FakeNetworkFetcher alloc] init];\n    sut.networkFetcher.fakeSuccess = YES;\n    sut.networkFetcher.fakeResult = fakeResult;\n}\n\n- (void)setUpFakeNetworkFetcherToFail\n    sut.networkFetcher = [[FakeNetworkFetcher alloc] init];\n    sut.networkFetcher.fakeSuccess = NO;\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

现在,您的成功路径测试需要确保您的表视图已使用更新后的模型重新加载。这是第一次天真的尝试:

\n\n
- (void)testReloadTableViewContents_withSuccess_ShouldReloadTableWithResult {\n    // given\n    [self setUpFakeNetworkFetcherToSucceedWithResult:@[@"RESULT"]];\n    sut.tableView = mock([UITablewView class]);\n\n    // when\n    [sut reloadTableViewContents];\n\n    // then\n    assertThat(sut.model, is(@[@"RESULT"]));\n    [verify(sut.tableView) reloadData];\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

不幸的是,这并不能保证模型在reloadData消息之前更新。但无论如何,您都需要进行不同的测试,以确保获取的结果显示在表格单元格中。这可以通过保留真实的 UITableView 并允许运行循环使用此辅助方法前进来完成:

\n\n
- (void)runForShortTime {\n    [[NSRunLoop currentRunLoop] runUntilDate:[NSDate date]];\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

最后,这是一个对我来说开始看起来不错的测试:

\n\n
- (void)testReloadTableViewContents_withSuccess_ShouldShowResultInCell {\n    // given\n    [self setUpFakeNetworkFetcherToSucceedWithResult:@[@"RESULT"]];\n\n    // when\n    [sut reloadTableViewContents];\n\n    // then\n    [self runForShortTime];\n    NSIndexPath *firstRow = [NSIndexPath indexPathForRow:0 inSection:0];\n    UITableViewCell *firstCell = [sut.tableView cellForRowAtIndexPath:firstRow];\n    assertThat(firstCell.textLabel.text, is(@"RESULT"));\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

但您的真正测试将取决于您的单元格实际如何表示获取的结果。这表明这个测试是脆弱的:如果你决定改变表示,那么你必须去修复一堆测试。那么让我们提取一个辅助断言方法:

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

这样,这里的测试使用我们的各种辅助方法来表达并且非常健壮:

\n\n
- (void)testReloadTableViewContents_withSuccess_ShouldShowResultsInCells {\n    [self setUpFakeNetworkFetcherToSucceedWithResult:@[@"FOO", @"BAR"]];\n\n    [sut reloadTableViewContents];\n\n    [self runForShortTime];\n    [self assertThatCellForRow:0 showsText:@"FOO"];\n    [self assertThatCellForRow:1 showsText:@"BAR"];\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

请注意,当我开始时,我脑子里并没有这个结局。我什至一路上做了一些我没有展示的错误步骤。但这展示了我如何尝试迭代测试设计的方法。

\n\n

编辑:我现在看到,使用 FakeNetworkFetcher,该块在reloadTableViewContents\xe2\x80\x94 的中间执行,这并不能反映异步时真正发生的情况。通过转移到捕获块,然后根据 Eugen 的答案调用它,该块将在reloadTableViewContents完成后执行。这要好得多。

\n\n
- (void)testReloadTableViewContents_withSuccess_ShouldShowResultsInCells {\n    [sut reloadTableViewContents];\n    [self simulateNetworkFetcherSucceedingWithResult:@[@"FOO", @"BAR"]];\n\n    [self runForShortTime];\n    [self assertThatCellForRow:0 showsText:@"FOO"];\n    [self assertThatCellForRow:1 showsText:@"BAR"];\n}\n
Run Code Online (Sandbox Code Playgroud)\n