一些涉及块方法和 OCMockito 的验证

Posted

技术标签:

【中文标题】一些涉及块方法和 OCMockito 的验证【英文标题】:Some verification involving block methods and OCMockito 【发布时间】:2014-05-25 21:56:16 【问题描述】:

我正在使用 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】:

这很简单:

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

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

【讨论】:

MKTArgumentCaptor 的好例子。使用这种方法,我要提取的辅助方法 调用 reloadTableViewContents 之后而不是之前,所以我可能会调用它而不是 setUpFakeNetworkFetcherToSucceedWithResult: simulateNetworkFetcherSucceedingWithResult:【参考方案2】:

(编辑:请参阅 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"];

【讨论】:

+1 来自 OCMockito 的作者的详细回答。

以上是关于一些涉及块方法和 OCMockito 的验证的主要内容,如果未能解决你的问题,请参考以下文章

OCMockito / OCHamcrest 验证数组包含对象属性

使用 OCMockito 模拟无效的方法

使用 OCMockito 测试发送到模拟协议的参数

涉及 moc 文件的 Qt 错误未通过清理项目修复

由于验证规则上下文导致的 iOS 核心数据保存 MOC 问题

用于 PC Mockito 模拟验证的自定义 Hamcrest 匹配器