Before we were so rudely interrupted at the end of Part 6 of this tutorial, we were most of the way through unit testing our SBAnimationManager class. Most of what remains is to test what happens when one of our animations ends. The source code for this tutorial may be found on GitHub. This part’s code is in the Version6 directory.

currentAnimationSegmentEnded: needs to handle the following cases:

  1. The animation we were just running may not have completed, because the user may have switched to a different screen, the device may have gone to sleep or any of a variety of other reasons. In this case, we’re supposed to abort, notify our delegate and reset things.
  2. If the animation completed successfully:
    1. The current animation segment may or may not be ending with a sound playing.
    2. If this is the last segment, we need to notify our delegate.
    3. If this is not the last segment, we need to start the next one.

Cases 2a and 2b/2c are technically independent of one another, so that makes four possibilities, five when you include case 1. Let’s start with the first situation.

If currentAnimationSegmentEnded: is called with a parameter of NO, we want the following:

  • The view being animated should be sent back home, where it started.
  • The delegate method should be called so that the view controller knows we’re done animating.
  • If we had segments left, they should not be processed. Thus, beginCurrentAnimationSegment should not be called.
  • If the current segment was supposed to play the sound at the end, we do not want that to happen.

Handling the first two is straightforward – we can check the view’s center after the method is called, and we can expect the call on the delegate. But how do we ensure that a method is not called. The answer is another feature of OCMock. The opposite of expect is reject.

- (void) test_currentAnimationSegmentEnded_resetsAndDoesntPlayIfNotSuccessful
{
    CGPoint originalViewCenter = _viewBeingAnimated.center;

    SBAnimationSegment *seg1 = [[SBAnimationSegment alloc] initWithPoint:CGPointMake(100,100) playSoundAtEnd:YES];
        // so we can make sure it doesn't play
    
    _viewBeingAnimated.center = CGPointMake(99,99); // so we can see if it's reset
    _objUnderTest.viewBeingAnimated = _viewBeingAnimated;
    _objUnderTest.animationSegments = @[seg1];
    
    id wrapper = [OCMockObject partialMockForObject:_objUnderTest];
    [[wrapper reject] beginCurrentAnimationSegment];
    [[wrapper reject] playBounceSound];
    
    [[_mockDelegate expect] animationComplete];
    
    [wrapper currentAnimationSegmentEnded:NO];
    
    [_mockDelegate verify];
    [wrapper verify];
    STAssertEquals(_viewBeingAnimated.center.x, originalViewCenter.x, nil);
    STAssertEquals(_viewBeingAnimated.center.y, originalViewCenter.y, nil);
}

Thus, lines 13 and 14 instruct the test to fail if either of those methods are called. With this technique, we can test the other cases:

If we successfully complete a segment that wants a sound, expect a call to playBounceSound and beginCurrentAnimationSegment with the currentSegmentIndex property having been incremented:

- (void) test_currentAnimationSegmentEnded_startsNextSegmentWithPlayIfNotLast
{
    SBAnimationSegment *seg1 = [[SBAnimationSegment alloc] initWithPoint:CGPointMake(100,100) playSoundAtEnd:YES];
    SBAnimationSegment *seg2 = [[SBAnimationSegment alloc] initWithPoint:CGPointMake(0,0) playSoundAtEnd:YES];
    SBAnimationSegment *seg3 = [[SBAnimationSegment alloc] initWithPoint:CGPointMake(5,5) playSoundAtEnd:NO];
    
    _objUnderTest.viewBeingAnimated = _viewBeingAnimated;
    _objUnderTest.animationSegments = @[seg1,seg2,seg3];
    _objUnderTest.currentSegmentIndex = 1;
    
    id wrapper = [OCMockObject partialMockForObject:_objUnderTest];
    [[wrapper expect] beginCurrentAnimationSegment];
    [[wrapper expect] playBounceSound];
    
    [wrapper currentAnimationSegmentEnded:YES];
    
    [wrapper verify];
    STAssertEquals([wrapper currentSegmentIndex], (unsigned int) 2, nil);
}

If we successfully complete a segment that doesn’t want a sound, reject a call to playBounceSound, expect one to beginCurrentAnimationSegment with the currentSegmentIndex property having been incremented:

- (void) test_currentAnimationSegmentEnded_startsNextSegmentWithoutPlayIfNotLast
{
    SBAnimationSegment *seg1 = [[SBAnimationSegment alloc] initWithPoint:CGPointMake(100,100) playSoundAtEnd:YES];
    SBAnimationSegment *seg2 = [[SBAnimationSegment alloc] initWithPoint:CGPointMake(0,0) playSoundAtEnd:NO];
    SBAnimationSegment *seg3 = [[SBAnimationSegment alloc] initWithPoint:CGPointMake(5,5) playSoundAtEnd:NO];
    
    _objUnderTest.viewBeingAnimated = _viewBeingAnimated;
    _objUnderTest.animationSegments = @[seg1,seg2,seg3];
    _objUnderTest.currentSegmentIndex = 1;
    
    id wrapper = [OCMockObject partialMockForObject:_objUnderTest];
    [[wrapper expect] beginCurrentAnimationSegment];
    [[wrapper reject] playBounceSound];
    
    [wrapper currentAnimationSegmentEnded:YES];
    
    [wrapper verify];
    STAssertEquals([wrapper currentSegmentIndex], (unsigned int) 2, nil);
}

If we successfully complete a final segment that wants a sound, expect a call to playBounceSound and the delegate and reject one to beginCurrentAnimationSegment:

- (void) test_currentAnimationSegmentEnded_handlesLastSegmentWithPlay
{
    SBAnimationSegment *seg1 = [[SBAnimationSegment alloc] initWithPoint:CGPointMake(100,100) playSoundAtEnd:YES];
    SBAnimationSegment *seg2 = [[SBAnimationSegment alloc] initWithPoint:CGPointMake(0,0) playSoundAtEnd:YES];
    
    _objUnderTest.viewBeingAnimated = _viewBeingAnimated;
    _objUnderTest.animationSegments = @[seg1,seg2];
    _objUnderTest.currentSegmentIndex = 1;
    
    id wrapper = [OCMockObject partialMockForObject:_objUnderTest];
    [[wrapper reject] beginCurrentAnimationSegment];
    [[wrapper expect] playBounceSound];
    
    [[_mockDelegate expect] animationComplete];
    
    [wrapper currentAnimationSegmentEnded:YES];
    
    [wrapper verify];
}

If we successfully complete a final segment that doesn’t want a sound, expect a call to the delegate and reject one to beginCurrentAnimationSegment or playBounceSound and :

- (void) test_currentAnimationSegmentEnded_handlesLastSegmentWithoutPlay
{
    SBAnimationSegment *seg1 = [[SBAnimationSegment alloc] initWithPoint:CGPointMake(100,100) playSoundAtEnd:YES];
    SBAnimationSegment *seg2 = [[SBAnimationSegment alloc] initWithPoint:CGPointMake(0,0) playSoundAtEnd:NO];
    
    _objUnderTest.viewBeingAnimated = _viewBeingAnimated;
    _objUnderTest.animationSegments = @[seg1,seg2];
    _objUnderTest.currentSegmentIndex = 1;
    
    id wrapper = [OCMockObject partialMockForObject:_objUnderTest];
    [[wrapper reject] beginCurrentAnimationSegment];
    [[wrapper reject] playBounceSound];
    
    [[_mockDelegate expect] animationComplete];
    
    [wrapper currentAnimationSegmentEnded:YES];
    
    [wrapper verify];
}

If we go back and check our code coverage, we’ll find that we have, indeed, handled all the cases we coded. The only method that isn’t covered is the playBounceSound one. We’re going to make an executive decision not to test that method. It’s so dirt simple that it really shouldn’t fail (famous last words), but if the sounds don’t play, we’re sure that our testers will pick it up. In order to keep our code coverage stats from continuing to complain about it, we can “exclude” it from lcov‘s processing by surrounding it with comments:

//LCOV_EXCL_START
- (void) playBounceSound
{
    AudioServicesPlaySystemSound(_soundID);
}
//LCOV_EXCL_STOP

Obviously, we should’t be too cavalier about excluding areas of our code from testing, but in this case we can justify it.

In the final part of this tutorial we’ll go back and address some of the issues we were having with the sounds.