Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Writing a unit test to verify NSTimer was started

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:

  1. Actually wait for the timer to fire. (Reason I don't like it: terrible unit testing practice. It's more like a slow integration test.)
  2. Extract the timer starting code into a private method, expose that private method in a class extension file to the unit tests, and use a mock expectation to verify that the private method gets called. (Reason I don't like it: it verifies that the method gets called, but not that the method actually sets up the timer to run correctly. Also, exposing private methods is not a good practice.)
  3. Provide a mock 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.)
  4. Provide a mock 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?

like image 241
Richard Shin Avatar asked Jan 07 '14 06:01

Richard Shin


People also ask

How do you start writing a unit test?

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.

Should you write unit tests before code?

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.

What is verify in unit test?

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.


2 Answers

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:

  • The return value of a method
  • A change of state or behavior of the system (preferably available through a public interface)
  • The interaction your code has with some other code that you don't control

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:

  • When the timer fires, the test expects that a NSNotification is posted to the default NSNotificationCenter. OCMock managed not to disappoint: notification broadcast testing is this easy.
  • To actually trigger the timer to fire, you need a reference to the timer. My class under test does not expose the NSTimer in a public interface, so the way I did it was by creating a class extension that exposed the private NSTimer property to my tests, as described in this SO post.
like image 74
Richard Shin Avatar answered Oct 19 '22 22:10

Richard Shin


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];
like image 30
psobko Avatar answered Oct 19 '22 22:10

psobko