In the previous part of this tutorial, we finished up restructuring and unit testing the on-screen animation code for our sample application. We did still have some issues outstanding with the sound, however, which we will clean up in this part. As always, the source code for this tutorial may be found on GitHub. This part’s code is in the Version7 directory.

Specifically, we have one problem and one outstanding requirement. The problem is that the first time the animation runs, the bounce sound is late in playing. We’re using the AudioServicesPlaySystemSound function to play our sound. This works fine, but it can be slow to react. The problem is that the sound file isn’t really loaded, parsed and queued up until we actually go to play the sound. As a result, there’s a delay between when we ask it to play and it actually plays. We can get around this by using an AVAudioPlayer instead.

AVAudioPlayer is part of the AVFoundation framework, so we’ll have to add that to our project. (We can remove the AudioToolbox framework, since we won’t need it.) Creating an AVAudioPlayer object is just as simple as loading a sound using the AudioToolbox routines – you simply allocate one and initialize it with the URL to the sound:

        NSString *soundFilePath = [[NSBundle bundleForClass:[self class]] pathForResource:@"bounce" ofType:@"mp3"];
        NSURL *soundFileURL = [NSURL fileURLWithPath:soundFilePath];
        NSError *error = nil;
        _bouncePlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:soundFileURL error:&error];
        NSAssert(_bouncePlayer != nil, [error localizedDescription]);
        [_bouncePlayer prepareToPlay];

The last method is the magic one. This instructs the player to do whatever magic is required behind the scenes in order to be ready to play the sound. From my experience, this call isn’t strictly synchronous, however in most applications you usually have plenty of time between when you load a sound and when you play it, so by the time you go to play it, it’s all ready to go. To play the sound, we simply call

    [_bouncePlayer play];

In addition, an AVAudioPlayer works like any other iOS object, so we don’t have to remember to clean up the sound in dealloc. (Unless, of course, your project isn’t using ARC.)

You’ll notice that if you start a bounce animation and then press the Home button, the sound no longer plays while the application is hidden the way it did in the initial version of the application. This is not a function of the AVAudioPlayer – it’s actually a side effect of our revised animation logic. In this situation, since we get notified that an animation didn’t complete, we don’t play the sound.

One of the advantages of having an object for a player, as opposed to simply a function call, is that we can more easily test that it’s being properly used by mocking it. Thus, if we add a property to SBAnimationManager+Internal.h that allows us to explicitly set the bounce player object, we can replace it with a mock and then verify that it was played at the correct time. Thus, instead of expecting or rejecting a call to playBounceSound, we can expect or reject a call to the audio player’s play method.

So, in our setUp for SBAnimationManagerTests we can add:

    _mockAudioPlayer = [OCMockObject mockForClass:[AVAudioPlayer class]];
    _objUnderTest.bouncePlayer = _mockAudioPlayer;

And then in the tests where we previously expected or rejected a call to playBounceSound on a “wrapper” (partial mock), we can replace

    [[wrapper expect] playBounceSound];

with

    [(AVAudioPlayer *)[_mockAudioPlayer expect] play];

(The cast is required to get rid of a compiler complaint by Xcode.)

The methods that expect a call to play will also need to call

    [_mockAudioPlayer verify];

before exiting.

Note that the “reject” isn’t strictly necessary in this case – normal mock objects created by OCMock will automatically reject any call that isn’t expected or stubbed. I still prefer the explicit declaration, however, since it makes things clear. It also makes the tests less fragile – there might come a time in the future where you might change the type of mock object you were creating to a “nice mock,” which will accept undefined calls without complaint.

The final thing to handle is the question as to whether or not there is a phone call in progress – remember that marketing wanted us to suppress the sounds in that case. For this, you can turn to the CTCallCenter class, which is part of the CoreTelephony framework. It has a currentCalls method that will return a NSSet of calls when there are calls in progress, and nil if there are not. Thus, all we need to do is:

- (BOOL) callInProgress
{
    return _callCenter.currentCalls != nil;
}

- (void) playBounceSound
{
    if (![self callInProgress])
    {
        [self.bouncePlayer play];
    }
}

We can then test playBounceSound by using a partial mock on the SBAnimationManager class to override the callInProgress method to have it return what we want it to:

    [[[wrapper expect] andReturnValue:OCMOCK_VALUE((BOOL){YES})] callInProgress];

and expect or reject a call to play on the mock audio player.

At this point, if we go back and check our code coverage, we’ll find that the only class that we haven’t handled is the SBAppDelegate class. Most of the methods there we don’t need, so we can just delete. There is one thing that might be slightly confusing – if you look at the coverage for SBAppDelegate, you’ll see line 24 highlighted as not covered:

AppDelegateFirstLine

This looks very odd, because there isn’t any executable content there. If you click on the “functions” link near the very top of the page, however, you’ll see this:

AppDelegateFunctions

The first line is our culprit – our SBAppDelegate class is getting created, but it’s underlying destructor is never being called. This is because the AppDelegate is getting created as the unit tests are running, but isn’t destroyed until after they’re done which means that the code coverage logic never sees the destructor being called. If you’re bound and determined to have 100% code coverage, you can simply create and destroy an instance of SBAppDelegate as a separate unit test. Otherwise, you can just ignore it. Particularly when you start dealing with blocks, there are cases in which it can be difficult to guarantee that you will actually execute all the interesting little functions that the compiler can generate behind the scenes.

This concludes this series of tutorials. I hope that you take away the fact that iOS applications can (and should) be unit tested just like any other application.