编写单元测试以验证NSTimer已启动

时间:2014-01-07 06:18:12

标签: ios objective-c unit-testing nstimer

我正在使用XCTestOCMock为iOS应用编写单元测试,我需要指导如何最好地设计单元测试,以验证方法是否导致{{1正在开始。

受测试代码:

NSTimer

我想测试的是使用正确的参数创建计时器,并计划在运行循环上运行计时器。

我想到了以下我不满意的选项:

  1. 实际上等待计时器开火。 (理由我不喜欢它:可怕的单元测试练习。它更像是一个缓慢的集成测试。)
  2. 将计时器启动代码解压缩为私有方法,将类扩展文件中的私有方法暴露给单元测试,并使用模拟期望来验证私有方法是否被调用。 (原因我不喜欢它:它验证方法是否被调用,但不是该方法实际上设置定时器才能正确运行。另外,暴露私有方法不是一个好习惯。)
  3. 为正在测试的代码提供模拟- (void)start { ... self.timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(tick:) userInfo:nil repeats:YES]; NSRunLoop *runLoop = [NSRunLoop currentRunLoop]; [runLoop addTimer:self.timer forMode:NSDefaultRunLoopMode]; ... } 。 (原因我不喜欢它:无法验证它实际上是否已计划运行,因为计时器是通过运行循环启动的,而不是来自某些NSTimer启动方法。)
  4. 提供模拟NSTimer并验证NSRunLoop:是否被调用。 (原因我不喜欢它:我必须在运行循环中提供一个接口?这看起来很古怪。)
  5. 有人可以提供一些单元测试指导吗?

2 个答案:

答案 0 :(得分:6)

好!花了一段时间才弄清楚如何做到这一点。我将完整地解释我的思考过程。抱歉长篇大论。

首先,我必须弄清楚我在测试的确切内容。我的代码有两件事:它启动一个重复计时器,然后计时器有一个回调,使我的代码做其他事情。这是两个不同的行为,这意味着两个不同的单元测试。

那么如何编写单元测试来验证代码是否正确启动了重复计时器?您可以在单元测试中测试三件事:

  • 方法的返回值
  • 系统状态或行为的变化(最好通过公共接口提供)
  • 您的代码与其他一些您无法控制的代码的交互

使用NSTimerNSRunLoop,我必须测试互动,因为无法从外部验证计时器是否已正确配置。说真的,没有repeats属性。您必须拦截创建计时器本身的方法调用。

接下来,我意识到如果我用NSRunLoop创建了一个自动启动计时器的计时器,我根本不需要触摸+scheduledTimerWithTimeInterval:target:selector:userInfo:repeats。这是我必须测试的一个较少的互动。

最后,为了创建一个期望+scheduledTimerWithTimeInterval:target:selector:userInfo:repeats被调用,你必须模拟NSTimer类,幸好OCMock现在可以做。这是测试的样子:

id mockTimer = [OCMockObject mockForClass:[NSTimer class]];
[[mockTimer expect] scheduledTimerWithTimeInterval:1.0
                                            target:[OCMArg any]
                                          selector:[OCMArg anySelector]
                                          userInfo:[OCMArg any]
                                           repeats:YES];

<your code that should create/schedule NSTimer>

[mockTimer verify];

看看这个测试,我想,“等等,你怎么能真正测试计时器配置了正确的目标和选择器?”好吧,我终于意识到我不应该真的关心它配置了特定的目标和选择器,我应该只关心当计时器触发时,它完成了我需要它做的事情。这对于编写良好的,面向未来的单元测试来说非常重要:真的很努力不依赖于私有接口或实现细节,因为这些都会发生变化。相反,测试代码不会改变的行为,并通过公共接口进行。

这让我们进行了第二次单元测试:计时器是否按照我的需要做了什么?为了测试这一点,谢天谢地NSTimer-fire,这导致计时器在目标上执行选择器。因此,您甚至不需要创建假的NSTimer,或者做一个提取&amp;覆盖以创建自定义模拟计时器,您所要做的就是让它翻录:

id mockObserver = [OCMockObject observerMock];
[[NSNotificationCenter defaultCenter] addMockObserver:mockObserver
                                                 name:@"SomeNotificationName"
                                               object:nil];
[[mockObserver expect] notificationWithName:@"SomeNotificationName"
                                     object:[OCMArg any]];
[myCode startTimer];

[myCode.timer fire];

[mockObserver verify];
[[NSNotificationCenter defaultCenter] removeObserver:mockObserver];

关于此测试的一些评论:

  • 当计时器触发时,测试期望NSNotification发布到默认NSNotificationCenterOCMock设法不让人失望:通知广播测试很容易。
  • 要实际触发定时器触发,您需要对定时器的引用。我的测试类没有在公共接口中公开NSTimer,所以我这样做的方法是创建一个类扩展,将私有NSTimer属性暴露给我的测试as described in this SO post

答案 1 :(得分:1)

我真的很喜欢Richard的第一种方法,我稍微扩展了代码以使用块调用来避免引用私有NSTimer属性。

[[mockAPIClient expect] someMethodSuccess:[OCMIsNotNilConstraint constraint]
                                  failure:[OCMIsNotNilConstraint constraint];

id mockTimer = [OCMockObject mockForClass:[NSTimer class]];
[[[mockTimer expect] andDo:^(NSInvocation *invocation) {
    SEL selector = nil;
    [invocation getArgument:&selector atIndex:4];
    [testSubject performSelector:selector];
}] scheduledTimerWithTimeInterval:10.0
                           target:testSubject
                         selector:[OCMArg anySelector]
                         userInfo:nil
                          repeats:YES];

[testSubject viewWillAppear:YES];

[mockTimer verify];
[mockAPIClient verify];