As of Part 4 of this tutorial, we have tested our view controller, and added support for measuring unit test code coverage to our project. At this point, we’re going to test the SBAnimationManager class that is driving the on-screen animations.

The first thing we will observe is that we don’t really want to do our animation tests at the same speed as which the animations will occur when the app is running. Unit tests are supposed to be fast, so that we won’t hesitate to run them. The on-screen animations are taking 2 seconds each, or 4 seconds total. We don’t want to sit around for that long – ideally, all the unit tests would be complete, soup to nuts, in about that time.

So the first thing we will do is to add a duration to SBAnimationManager. When run in production, we’ll allow this to just use the default two seconds, but during our unit tests we’ll override it with a smaller value so that things go faster.

There are a few schools of thought on how we should go about providing our unit tests with access to this value, since it’s not nominally (at least at present) part of the public interface of the SBAnimationManager class.

  1. We could just bite the bullet and add this as part of the public interface. There’s no good way, however, to say “Hands off!” to users who find this method in the public interface, so doing this tends to make the additional property part of the permanent contract for the object.
  2. We can declare the appropriate methods in a category within the unit test class itself. This works, but has the disadvantages that:
    1. The declaration is well separated from the implementation.
    2. Xcode (at least at present) won’t “notice” these definitions if we refactor them, leaving them orphaned.
  3. We can set up a separate .h file in the main project that defines “non-public” methods which must still be accessible via a category.

I tend to prefer the third approach – I usually name the file with Internal or UnitTest in its name.

An additional quirk is that we cannot declare properties that will add members to our classes in standard categories. For example, if we declared SBAnimationManager+Internal.h as follows:

@interface SBAnimationManager(Internal)
@property (assign, nonatomic) NSTimeInterval duration;
@end

and then tried to @synthesize duration inside SBAnimationManager.m, we would get a compiler error that says Property declared in category 'Internal' cannot be implemented in class implementation. We can get around this, however, by being sneaky, and using an “anonymous” category:

@interface SBAnimationManager()
@property (assign, nonatomic) NSTimeInterval duration;
@end

Note the omission of a category name on the first line. An “anonymous category” has special privileges in Objective C. Frequently, they are included at the top of a .m file (Xcode includes one by default when building a new class file) and are used to define “forward references” or “internal only properties.” Here, we want a “semi-internal” property, so this is just the ticket.

With this, we can set up the skeleton of our unit test file for the animation manager:

#define ANIMATION_DURATION  0.1

@interface SBAnimationManagerTests : SenTestCase
@end

@implementation SBAnimationManagerTests
{
    SBAnimationManager *    _objUnderTest;
    id                      _mockDelegate;
    UIView *                _parentView;
    UIView *                _viewBeingAnimated;
}

- (void) setUp
{
    _objUnderTest = [[SBAnimationManager alloc] init];
    _objUnderTest.duration = ANIMATION_DURATION;
    _mockDelegate = [OCMockObject mockForProtocol:@protocol(SBAnimationManagerDelegate)];
    _objUnderTest.delegate = _mockDelegate;
    _parentView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
    _viewBeingAnimated = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 10, 10)];
    [_parentView addSubview:_viewBeingAnimated];
}

- (void) tearDown
{
    _objUnderTest = nil;
    _parentView = nil;
    _viewBeingAnimated = nil;
    _mockDelegate = nil;
}

Before each test is run, we create an SBAnimationManager object, override the default duration to set it to 0.1 seconds, and provide a mock implementation of a delegate so that we can check to see that it makes the appropriate delegate calls. In addition, we create a 100×100 “parent” view containing a 10×10 “child” view at its upper left corner. These will be used in place of the real views in the application – it turns out that, with a few exceptions, views don’t have to be “live” in order to be animated, so we can operate perfectly well with our detached views.

So let’s run our first test:

- (void) test_viewReturnsHomeAfterAnimationsComplete
{
    CGPoint originalViewCenter = _viewBeingAnimated.center;
    
    [[_mockDelegate expect] animationComplete];

    [_objUnderTest bounceView:_viewBeingAnimated to:CGPointMake(5, 95)];
    
    [NSThread sleepForTimeInterval:ANIMATION_DURATION * 2 + 0.1];
    
    CGPoint viewCenter = _viewBeingAnimated.center;
    
    STAssertEquals(viewCenter.x, originalViewCenter.x, nil);
    STAssertEquals(viewCenter.y, originalViewCenter.y, nil);
    [_mockDelegate verify];
}

This test records where the view being animated starts, and bounces it elsewhere. We expect that SBAnimationManager will call the delegate’s animationComplete, we sleep for long enough to allow the two animations to complete (plus a little for good measure) and then check that the view returned back home and that the delegate call was made.

Unfortunately, if we run this, we find out that the view’s center is reported to be at (5.0,95.0), and that the delegate call isn’t made. We know that the code works, because we can see it running on the emulator – what’s going on?

The problem is our call to [NSThread sleepForTimeInterval]. Our animation manager uses blocks in order to chain together its animations. As written, we are simply sleeping the UI thread. For blocks that run on the UI thread to work, however, we have to implement a run loop. We talked about this in another context in my post on UIDocument coding. Thus, instead of sleeping, we need to cycle the run loop. We can do this via the following code:

- (void) waitForAnimations
{
    NSTimeInterval timeout = ANIMATION_DURATION * 2 + 0.1;
    NSDate *loopUntil = [NSDate dateWithTimeIntervalSinceNow:timeout];
    while ([loopUntil timeIntervalSinceNow] > 0)
    {
        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode
                                 beforeDate:loopUntil];
    }
}

If we replace the call to [NSThread sleepForTimeInterval] with a call to [self waitForAnimations], we now find that our unit test succeeds. In addition, the “bounce” sound plays, further indicating that the animation manager went through its paces. I don’t know about you, but I don’t want to listen to that sound for every single test we run. Right now, the call is embedded right within the nested blocks. We could always set up the animation manager so that this was overrideable, the way we did with the duration, but we are going to want to verify that the sound is played (or not played) at the appropriate times. Thus, let’s take a different approach. Let’s separate it out into a separate method:

- (void) bounceView:(UIView *)view to:(CGPoint)dest
{
    _view = view;
    _viewHome = view.center;
    
    [UIView animateWithDuration:_duration
                     animations:^(void)
                     {
                         _view.center = dest;
                     }
                     completion:^(BOOL finished)
                     {
                         [self playBounceSound];
                         
                         [UIView animateWithDuration:_duration
                                          animations:^(void)
                                          {
                                              _view.center = _viewHome;
                                          }
                                          completion:^(BOOL finished)
                                          {
                                              [self.delegate animationComplete];
                                          }];
                     }];
}

- (void) playBounceSound
{
    AudioServicesPlaySystemSound(_soundID);
}

We can also add the definition of playBounceSound to SBAnimationManager+Internal.h, since we’re going to need it in just a couple of paragraphs.

Now what we would like to do is to verify that this method gets called when the animation is performed, but not really execute the real implementation. One technique would be to create a class derived from SBAnimationManager which overrides this method, sets a flag to indicate it’s been called, and then use that class instead of SBAnimationManager in the unit test. Fortunately, however, there’s a way that’s a lot less work.

Any time we want to verify that something has happened, we should (I hope) be thinking about our old friend the Mock Object and its expect and verify methods. We’ve already used OCMock to synthesize an object for our delegate. That, however, was a “completely fictitious” object. Here, we kind of want a hybrid – we want most of our SBAnimationManager to be its normal self, but we want to expect (and intercept) the call to just one method. Fortunately, OCMock provides just such an implementation – a “partial” mock.

Basically, a “partial mock” is a wrapper around an existing object. One manipulates the wrapper object exactly as one would the “real” object. What the partial mock allows us to do, however, is to expect or stub individual methods, but allow other methods to “pass through” to the original implementation. Thus, we can modify our unit test as follows:

- (void) test_viewReturnsHomeAfterAnimationsComplete
{
    CGPoint originalViewCenter = _viewBeingAnimated.center;
    
    [[_mockDelegate expect] animationComplete];
    
    id wrapper = [OCMockObject partialMockForObject:_objUnderTest];
    [[wrapper expect] playBounceSound];
    
    [wrapper bounceView:_viewBeingAnimated to:CGPointMake(5, 95)];
    
    [self waitForAnimations];
    
    CGPoint viewCenter = _viewBeingAnimated.center;
    
    STAssertEquals(viewCenter.x, originalViewCenter.x, nil);
    STAssertEquals(viewCenter.y, originalViewCenter.y, nil);
    [_mockDelegate verify];
    [wrapper verify];
}

The expect on the wrapper prevents the actual implementation of playBounceSound from being executed, but, in combination with the verify at the end, ensures that the code did, in fact, call that method. If we run our unit tests again, we find that it passes, and we don’t hear the bounce sound. This is good – we’re going to be adding more tests, and I don’t want it to sound like a basketball convention every time I run the tests.

If we checked our code coverage right now, we’d find that we’ve covered everything in SBAnimationManager, except the playBounceSound method. So are we done? Well, if our goal was just to satisfy our boss our tests achieve a particular coverage metric on the source, I guess yes. In reality, however, our tests are really insufficient right now. How do we know that our animation actually did what we expected? We know that the view being animated ended up where it started when things are done, but for all we know, it may not have moved at all – we could have ripped out most of the implementation of bounceView:to: and still gotten the same result. (If we were TDD’ing, of course, that’s exactly how we would have started out.)

Thus, we really need to test that the view goes where it should. Plus, we still have the code modifications to make to support things like shutting down if the app is exited. The current nested block approach, however, doesn’t let us stick our nose into the middle of the class’s operation – it just isn’t designed to be tested. In the next installment, we’ll rip it apart and put it back together in a more testable fashion.