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

Ric*_*hin 10 unit-testing objective-c nstimer ios

我正在使用XCTestOCMock编写iOS应用程序的单元测试,我需要指导如何最好地设计单元测试,以验证方法是否导致NSTimer启动.

被测代码:

- (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];
    ...
}
Run Code Online (Sandbox Code Playgroud)

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

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

  1. 实际上等待计时器开火.(原因我不喜欢它:可怕的单元测试练习.它更像是一个缓慢的集成测试.)
  2. 将计时器启动代码提取到私有方法中,将类扩展文件中的私有方法暴露给单元测试,并使用模拟期望来验证私有方法是否被调用.(原因我不喜欢它:它验证方法被调用,但不是该方法实际上设置定时器才能正确运行.另外,暴露私有方法不是一个好习惯.)
  3. NSTimer被测代码提供模拟.(原因我不喜欢它:无法验证它实际上是否已安排运行,因为定时器是通过运行循环NSTimer启动的,而不是来自某些启动方法.)
  4. 提供模拟NSRunLoop并验证addTimer:forMode:被调用.(原因我不喜欢它:我必须在运行循环中提供一个接口?这看起来很古怪.)

有人可以提供一些单元测试指导吗?

Ric*_*hin 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];
Run Code Online (Sandbox Code Playgroud)

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

这让我们进行了第二次单元测试:定时器是否按照我的需要做了什么?为了测试这一点,幸运的是NSTimer具有-fire,这使得定时器在目标执行选择器.因此,你甚至不需要创建一个假的NSTimer,或者做一个提取和覆盖来创建一个自定义模拟计时器,所有你要做的就是让它翻录:

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];
Run Code Online (Sandbox Code Playgroud)

关于这个测试的一些评论:

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