【问题标题】:Some verification involving block methods and OCMockito一些涉及块方法和 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 上被调用并且模型是什么应该是的。

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

我怎样才能做到这一点?

【问题讨论】:

    标签: ios unit-testing ocmockito ochamcrest


    【解决方案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 的作者的详细回答。
    猜你喜欢
    • 1970-01-01
    • 2023-03-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-08-23
    • 2013-03-28
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多