继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使用OCHamcrest和Jon Reid。
提前致谢!!
答案 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以便实际调用块时,问题是必须存在超时。这种超时通常很短,因为如果它们遇到超时,你不想让测试持续很长时间。但是短暂超时意味着您的测试在调试时会进入超时状态。