5

继TDD之后,我正在开发一款iPad应用程序,用于从互联网下载一些信息并将其显示在列表中,允许用户使用搜索栏过滤该列表。带有dispatch_async调用的测试代码

我想测试一下,当用户在搜索栏中输入内容时,带有过滤文本的内部变量被更新,项目的过滤列表被更新,最后表格视图接收到一个“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”属性触发,现在实际的过滤过程,这在其他测试中进行了测试。

我的搜索栏委托写的代码为这个工程确定如下:

- (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调用中测试这些东西?

有没有优雅除了基于时间的解决方案以外的方式吗? (就像等待一段时间,并期望这些任务已经完成,不是非常确定的)

或者我应该把我的代码放在不同的方式来使其更具可测性?

如果您需要知道,我使用OCMockitoOCHamcrest通过Jon Reid

在此先感谢!

+0

使用brakpoints和NSLogs可能对您有帮助吗? – 2013-05-12 18:48:50

+0

为了什么目的你有前两种方法。 – 2013-05-12 18:49:26

+0

Hi @ArpitParekh!这个想法是使用[单元测试](https://en.wikipedia.org/wiki/Unit_testing)自动测试我的代码。这不是关于找到一个错误,而是为了确保此代码从现在开始正确运行。前两种方法是来自我的测试套件的测试。检查链接关于单元测试的更多信息:) – sergiou87 2013-05-12 20:46:44

回答

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。 (但是要将验收测试保存在一个单独的测试目标中,它们太慢而不能包含单元测试)

+0

谢谢乔恩!现在我只是写单元测试,对于不采用某些方法的决定,我觉得很困难,但是我想这就是在这种情况下,验收测试来解决问题的时候。 – sergiou87 2013-05-13 09:30:12

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 

您在您的测试设置调用这个和你的块将被直接调用。

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

+0

感谢您的回答!我正在选择其他解决方案:将异步代码隐藏在另一个类中,并在测试期间模拟该类。通过间谍我捕获完成块,模拟器立即执行完成块。 我的测试中没有异步代码了:) – sergiou87 2015-09-07 09:41:58