使用dispatch_async调用测试代码

时间:2013-05-12 18:29:36

标签: objective-c unit-testing objective-c-blocks dispatch-async ocmockito

继TDD之后,我正在开发一款iPad应用程序,可以从互联网上下载一些信息并将其显示在列表中,允许用户使用搜索栏过滤该列表。

我想测试一下,当用户在搜索栏中输入内容时,会更新带有过滤器文本的内部变量,更新已过滤的项目列表,最后表格视图会收到“reloadData”消息。

这些是我的测试:

- (void)testSutChangesFilterTextWhenSearchBarTextChanges
{
    // given
    sut.filterText = @"previous text";

    // when
    [sut searchBar:nil textDidChange:@"new text"];

    // then
    assertThat(sut.filterText, is(equalTo(@"new text")));
}

- (void)testSutReloadsTableViewDataAfterChangeFilterTextFromSearchBar
{
    // given
    sut.tableView = mock([UITableView class]);

    // when
    [sut searchBar:nil textDidChange:@"new text"];

    // then
    [verify(sut.tableView) reloadData];
}

注意:更改“filterText”属性会立即触发实际的过滤过程,该过程已在其他测试中进行过测试。

这可行,因为我的searchBar委托代码编写如下:

- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText
{
    self.filterText = searchText;
    [self.tableView reloadData];
}

问题是过滤这些数据正变成一个繁重的过程,现在正在主线程上完成,因此在此期间UI被阻止。

因此,我想做这样的事情:

- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText
{
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSArray *filteredData = [self filteredDataWithText:searchText];

        dispatch_async(dispatch_get_main_queue(), ^{
            self.filteredData = filteredData;
            [self.tableView reloadData];
        });
    });
}

这样过滤过程就会在不同的线程中发生,当它完成时,会要求该表重新加载其数据。

问题是......如何在dispatch_async调用中测试这些内容?

除了基于时间的解决方案之外,还有其他优雅的方式吗? (比如等待一段时间,并期望那些任务已经完成,而不是非常确定)

或许我应该以不同的方式使用我的代码使其更易于测试?

如果您需要知道,我OCMockito使用OCHamcrestJon Reid

提前致谢!!

2 个答案:

答案 0 :(得分:5)

有两种基本方法。任

  • 仅在测试时使事物同步。或者,
  • 保持异步,但写一个重新同步的验收测试。

要使事物同步以进行测试,请将实际工作的代码提取到自己的方法中。您已经拥有-filteredDataWithText:。这是另一个提取:

- (void)updateTableWithFilteredData:(NSArray *)filteredData
{
    self.filteredData = filteredData;
    [self.tableView reloadData];
}

现在处理所有线程的真正方法如下:

- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText
{
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSArray *filteredData = [self filteredDataWithText:searchText];

        dispatch_async(dispatch_get_main_queue(), ^{
            [self updateTableWithFilteredData:filteredData];
        });
    });
}

请注意,在所有线程范围之下,它实际上只调用了两个方法。所以现在假装所有线程都已完成,让你的测试按顺序调用这两个方法:

NSArray *filteredData = [self filteredDataWithText:searchText];
[self updateTableWithFilteredData:filteredData];

这确实意味着单元测试不会涵盖-searchBar:textDidChange:。单个手动测试可以确认它正在发送正确的东西。

如果您真的想要对委托方法进行自动化测试,请编写一个具有自己的运行循环的验收测试。见Pattern for unit testing async queue that calls main queue on completion。 (但是将验收测试保存在单独的测试目标中。它们太慢而不能包含单元测试。)

答案 1 :(得分:3)

Albite Jons选项在大多数情况下是非常好的选项,有时它会在执行以下操作时创建较少混乱的代码。例如,如果您的API有很多使用调度队列同步的小方法。

有这样的功能(它也可以是你班级的一种方法)。

void dispatch(dispatch_queue_t queue, void (^block)())
{
    if(queue)
    {
        dispatch_async(queue, block);
    }
    else
    {
        block();
    }
}

然后使用此函数调用API方法中的块

- (void)anAPIMethod
{
    dispatch(dispQueue, ^
    {
        // dispatched code here
    });
}

您通常会在init方法中初始化队列。

@implementation MyAPI
{
    dispatch_queue_t dispQueue;
}

- (instancetype)init
{
    self = [super init];
    if (self)
    {
        dispQueue = dispatch_queue_create("myQueue", DISPATCH_QUEUE_SERIAL);
    }

    return self;
}

然后有一个像这样的私有方法,将此队列设置为nil。它不是您的界面的一部分,API消费者永远不会看到它。

- (void) disableGCD
{
    dispQueue = nil;
}

在测试目标中,您可以创建一个类别以公开GCD禁用方法:

@interface TTBLocationBasedTrackStore (Testing)
- (void) disableGCD;
@end

您可以在测试设置中调用它,并直接调用您的块。

我眼中的优点是调试。当测试用例涉及runloop以便实际调用块时,问题是必须存在超时。这种超时通常很短,因为如果它们遇到超时,你不想让测试持续很长时间。但是短暂超时意味着您的测试在调试时会进入超时状态。