我正在使用XCTest
和OCMock
为iOS应用编写单元测试,我需要指导如何最好地设计单元测试,以验证方法是否导致{{1正在开始。
受测试代码:
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];
...
}
。 (原因我不喜欢它:无法验证它实际上是否已计划运行,因为计时器是通过运行循环启动的,而不是来自某些NSTimer
启动方法。)NSTimer
并验证NSRunLoop
:是否被调用。 (原因我不喜欢它:我必须在运行循环中提供一个接口?这看起来很古怪。)有人可以提供一些单元测试指导吗?
答案 0 :(得分:6)
好!花了一段时间才弄清楚如何做到这一点。我将完整地解释我的思考过程。抱歉长篇大论。
首先,我必须弄清楚我在测试的确切内容。我的代码有两件事:它启动一个重复计时器,然后计时器有一个回调,使我的代码做其他事情。这是两个不同的行为,这意味着两个不同的单元测试。
那么如何编写单元测试来验证代码是否正确启动了重复计时器?您可以在单元测试中测试三件事:
使用NSTimer
和NSRunLoop
,我必须测试互动,因为无法从外部验证计时器是否已正确配置。说真的,没有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
发布到默认NSNotificationCenter
。 OCMock
设法不让人失望:通知广播测试很容易。答案 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];