5

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

我想测试一下,当用户在搜索栏中键入时,带有过滤器文本的内部变量会更新,过滤后的项目列表也会更新,最后表格视图会收到“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 调用中测试这些东西?

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

或者也许我应该把我的代码放在不同的方式上,让它更容易测试?

如果您需要知道,我正在使用Jon Reid的OCMockitoOCHamcrest

提前致谢!!

4

2 回答 2

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:单元测试不会涵盖它。单个手动测试可以确认它正在发送正确的东西。

如果您真的想要对委托方法进行自动化测试,请编写一个具有自己的运行循环的验收测试。有关在完成时调用主队列的单元测试异步队列,请参阅模式。(但是将验收测试保留在单独的测试目标中。它们太慢而无法包含在单元测试中。)

于 2013-05-13T05:12:54.827 回答
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;
}

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

- (void) disableGCD
{
    dispQueue = nil;
}

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

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

您在测试设置中调用它,您的块将被直接调用。

我眼中的优势是调试。当测试用例涉及运行循环以便实际调用块时,问题是必须涉及超时。这个超时时间通常很短,因为如果测试超时,您不希望测试持续很长时间。但是超时时间很短意味着您的测试在调试时会遇到超时。

于 2015-09-01T17:53:00.113 回答