I'm using XCTest
and OCMock
to write unit tests for an iOS app, and I need direction on how to best design a unit test that verifies that a method results in an NSTimer
being started.
Code under test:
- (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];
...
}
What I want to test is that the timer is created with the right arguments, and that the timer is scheduled to run on the run loop.
I've thought of the following options that I am not happy with:
NSTimer
to the code under test. (Reason I don't like it: can't verify that it actually gets scheduled to run because timers are started via the run loop and not from some NSTimer
start method.)NSRunLoop
and verify that addTimer:forMode
: gets called. (Reason I don't like it: I'd have to provide an interface into the run loop? That seems wacky.)Can someone provide some unit testing coaching?
Follow Arrange, Act, Assert The AAA is a general approach to writing more readable unit tests. In the first step, you arrange things up for testing. It's where you set variables, instantiate objects, and do the rest of the required setup for the test to run. During this step, you also define the expected result.
It often makes sense to write the test first and then write as much code as needed to allow the test to pass. Doing this moves towards a practice known as Test-Driven Development (TDD). Bluefruit uses a lot of TDD because it helps us to build the right product without waste and redundancies.
Asserts are used to validate that properties of your system under test have been set correctly, whereas Verify is used to ensure that any dependencies that your system under test takes in have been called correctly.
Okay! It took a while to figure out how to do this. I'll explain my thought process in entirety. Sorry for the long-windedness.
First, I had to figure out exactly what I was testing. There are two things my code does: it starts a repeating timer, and then the timer has a callback that makes my code do something else. Those are two separate behaviors, which means two different unit tests.
So how do you write a unit test to verify that your code starts a repeating timer correctly? There are three things you can test for in a unit test:
With NSTimer
and NSRunLoop
, I had to test for interaction because there's no way to externally verify that the timer was configured correctly. Seriously, there's no repeats
property. You have to intercept the method call that creates the timer itself.
Next, I realized that I wouldn't have to touch NSRunLoop
at all if I created the timer with +scheduledTimerWithTimeInterval:target:selector:userInfo:repeats
, which automatically starts the timer. That's one less interaction I have to test for.
Finally, to create an expectation that +scheduledTimerWithTimeInterval:target:selector:userInfo:repeats
is called, you have to mock the NSTimer class, which thankfully OCMock can do now. Here's what the test looked like:
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];
Looking at this test, I thought, "Wait, how can you actually test that the timer is configured with the correct target and selector?" Well, I finally realized that I shouldn't actually care that it's configured with a particular target and selector, I should only care that when the timer fires, it does what I need it to do. And that's a really important point for writing good, future-proof unit tests: really try hard not to rely on the private interface or implementation details because those things change. Instead, test your code's behavior that won't change, and do it through the public interface.
That brings us to the second unit test: does the timer do what I need it to do? To test this, thankfully NSTimer
has -fire
, which causes the timer to perform the selector on the target. Thus, you don't even need to create a fake NSTimer
, or do an extract & override to create a custom mock timer, all you have to do is let it rip:
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];
A few comments about this test:
NSNotification
is posted to the default NSNotificationCenter
. OCMock
managed not to disappoint: notification broadcast testing is this easy.I really liked Richard's first approach, I expanded the code a little to use block invocations to avoid having to reference the private NSTimer
property.
[[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];
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With