我正在使用 XCTest
和 OCMock
为 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];
...
}
我想测试的是计时器是用正确的参数创建的,并且计时器被安排在运行循环中运行。
我想到了以下我不满意的选项:
- 实际等待计时器触发。 (我不喜欢它的原因:糟糕的单元测试实践。它更像是一个缓慢的集成测试。)
- 将计时器启动代码提取到私有(private)方法中,将类扩展文件中的私有(private)方法公开给单元测试,并使用模拟期望来验证私有(private)方法是否被调用。 (我不喜欢它的原因:它验证该方法是否被调用,但不是该方法实际设置计时器以正确运行。另外,公开私有(private)方法不是一个好的做法。)
- 为被测代码提供一个模拟的
NSTimer
。 (我不喜欢它的原因:无法验证它是否真的被安排运行,因为计时器是通过运行循环启动的,而不是从某些NSTimer
启动方法启动的。) - 提供模拟
NSRunLoop
并验证是否调用了addTimer:forMode
:。 (我不喜欢它的原因:我必须为运行循环提供一个接口(interface)?这看起来很古怪。)
有人可以提供一些单元测试指导吗?
最佳答案
好的!花了一段时间才弄清楚如何做到这一点。我会完整地解释我的思考过程。抱歉啰嗦。
首先,我必须弄清楚我在测试什么。我的代码做了两件事:它启动一个重复计时器,然后计时器有一个回调让我的代码做其他事情。这是两个独立的行为,这意味着两个不同的单元测试。
那么您如何编写单元测试来验证您的代码是否正确启动了一个重复计时器?您可以在单元测试中测试三件事:
- 方法的返回值
- 系统状态或行为的改变(最好通过公共(public)接口(interface)获得)
- 您的代码与您无法控制的其他代码的交互
对于 NSTimer
和 NSRunLoop
,我必须测试交互,因为没有办法从外部验证计时器是否配置正确。说真的,没有 repeats
属性。您必须拦截创建计时器本身的方法调用。
接下来,我意识到,如果我使用 +scheduledTimerWithTimeInterval:target:selector:userInfo:repeats
创建计时器,我根本不需要触及 NSRunLoop
,这自动启动定时器。这是我必须测试的少一种互动。
最后,要创建调用 +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];
看着这个测试,我想,“等等,你怎么能真正测试定时器是否配置了正确的目标和选择器?”好吧,我终于意识到我实际上不应该关心它配置了特定的目标和选择器,我应该只关心当计时器触发时,它会做我需要它做的事情。这对于编写良好的、面向 future 的单元测试来说非常重要:真正努力不要依赖私有(private)接口(interface)或实现细节,因为这些东西会改变。相反,测试不会更改的代码行为,并通过公共(public)接口(interface)进行。
这将我们带到了第二个单元测试:计时器是否执行了我需要它执行的操作?为了对此进行测试,幸运的是 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];
关于这个测试的一些评论:
- 当计时器触发时,测试期望将
NSNotification
发布到默认的NSNotificationCenter
。OCMock
没有让人失望:通知广播测试就是这么简单。 - 要实际触发计时器,您需要引用计时器。我的测试类没有在公共(public)接口(interface)中公开 NSTimer,所以我这样做的方法是创建一个类扩展,将私有(private) NSTimer 属性公开给我的测试,as described in this SO post .
关于ios - 编写单元测试以验证 NSTimer 是否已启动,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/20965117/