How to Test Calls to Super

It is quite common in iOS object methods to include a call to the parent object using a construct like [super …]. This call is important, albeit simple. So how do we verify in our unit tests that this call is made?

For example, let’s look at testing the default implementation of UIViewController didReceiveMemoryWarning;

@interface ViewController : UIViewController
...
@end
@implementation ViewController
...
- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Release any cached data, images, etc that aren't in use.
    ...
}

When I first started looking at this problem, there appeared to be 3 obvious ways to test this:

  1. Using a Category to Intercept Calls to Super
  2. Using a Category on a Subclass of the Parent to Intercept Calls to Super
  3. Swizzling

1: Using a Category to Intercept Calls to Super

The first approach is to use a category on the parent class to intercept the call to super’s method. It could then set a global flag to be inspected by the unit test.

#import "ViewController.h"

BOOL superWasCalled = NO;

@interface UIViewController (OverrideSuperForTesting)
- (void)didReceiveMemoryWarning;
@end
@implementation UIViewController (OverrideSuperForTesting)
- (void)didReceiveMemoryWarning {    
    superWasCalled = YES;    
}
@end

@interface ViewControllerTests : SenTestCase
@property (strong, nonatomic) ViewController *vc;
@end

@implementation ViewControllerTests
- (void)setUp {
    self.vc = [[ViewController alloc]init];
}
- (void)tearDown {
    self.vc = nil;
}

- (void)testDidReceiveMemoryWarningCallsSuper {
    [self.vc didReceiveMemoryWarning];
    STAssertTrue(superWasCalled, @"super not called");
}
@end

There are two issues with doing it this way:

  1. This code does not actually call super’s implementation of the method, which is probably ok for unit testing. The category’s method replaces the original implementation, which is simply lost.
  2. It does not work if the parent method being called is implemented as a category. Only one category will be called, and which one is unpredictable.

2: Using a Category on a Subclass of the Parent to Intercept Calls to Super

This second approach violates one of my testing rules: don’t modify product code just for testing. But this approach works, and is fairly clean. The only difference between this approach and the first one is that an empty subclass is created between the class and its parent. This allows creating a category on the empty class, which can then still call its super.

// Create an empty subclass
@interface ViewControllerSubclass : UIViewController
@end

@implementation ViewControllerSubclass
@end

// Change ViewController to use the subclass instead of UIViewController
@interface ViewController : ViewControllerSubclass
...
@end
#import "ViewController.h"

BOOL superWasCalled = NO;

@interface ViewControllerSubclass (OverrideSuperForTesting)
- (void)didReceiveMemoryWarning;
@end
@implementation ViewControllerSubclass (OverrideSuperForTesting)
- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    superWasCalled = YES;    
}
@end

@interface ViewControllerTests : SenTestCase
@property (strong, nonatomic) ViewControllerSubclass *vc;
@end

@implementation ViewControllerTests
- (void)setUp {
    self.vc = [[ViewControllerSubclass alloc]init];
}
- (void)tearDown {
    self.vc = nil;
}

- (void)testDidReceiveMemoryWarningCallsSuper {
    [self.vc didReceiveMemoryWarning];
    STAssertTrue(superWasCalled, @"super not called");
}
@end

I am assuming that the only change to the product code is the inclusion of an empty subclass between ViewController and UIViewController. During testing, a category is used to change the functionality of the subclass.

It might be tempting to put the code to set the test flag directly into the subclassed method instead of using a category, but then this code would be executed during normal product runtime. I recommend against littering up product code with things that are there solely for testing. Separating test code into test files keeps the product code more readable.

3: Using Swizzling to Intercept Calls to Super

The third approach is to swizzle the method on the parent class. Swizzling is a fairly common practice, but is generally considered to be a hack. Using swizzling, we can redirect the call to the super’s method to any other method we want. In this case, a method that will set a flag to signal that it was called.

Perhaps the easiest way to do this is to use the open source JRSwizzle library. This simplifies the swizzle operations, and makes the code more readable. The down side of using JRSwizzle is that as of this writing, it isn’t unit tested, so code coverage metrics for your project will suffer some. But who knows, maybe somebody in the near future will add unit tests to it.

Like in method 2 above, we’ll subclass the parent, and intercept/swizzle calls to super there. We’ll use a category, so that the code doesn’t get included in product builds. This is optional though. The code could be put into the parent subclass, and just wouldn’t ever be called in product builds.

#import "JRSwizzle.h"

BOOL didReceiveMemoryWarningWasCalled = NO;

@interface ViewControllerTestable (ForTesting)
- (void)didReceiveMemoryWarningOverride;
@end

@implementation ViewControllerTestable (ForTesting)
- (void)didReceiveMemoryWarningOverride {
    // Call original. Swizzling will redirect this.
    [self didReceiveMemoryWarningOverride];
    didReceiveMemoryWarningWasCalled = YES;
}
@end

@implementation ViewControllerTests
...
- (void)testDidReceiveMemoryWarningCallsSuper {

    // Swizzle super's methods
    NSError *error;
    [ViewControllerTestable 
     jr_swizzleMethod:@selector(didReceiveMemoryWarning)
           withMethod:@selector(didReceiveMemoryWarningOverride)
                error:&error];
    [self.vc didReceiveMemoryWarning];
    STAssertTrue(didReceiveMemoryWarningWasCalled, 
                 @"didReceiveMemoryWarning did not call super");

    // Swizzle back
    [ViewControllerTestable 
      jr_swizzleMethod:@selector(didReceiveMemoryWarning)
            withMethod:@selector(didReceiveMemoryWarningOverride)
                 error:&error];
}
...

In this code, when our method under test calls [super didReceiveMemoryWarning], the call will actually be made to didReceiveMemoryWarningOverride. This in turn will set a flag, and call the original didReceiveMemoryWarning (using the swizzled didReceiveMemoryWarningOverride which now points to didReceiveMemoryWarning).

So what’s the best approach? That will depend on your specific test situation. The second approach seems the cleanest and safest. It does, however, complicate the product code, but only slightly.

2 thoughts on “How to Test Calls to Super

  1. Hi,
    I ran into this problem today as well. I read your article and after that I did some poking around I found that I could swizzle the parent class method directly. Could this be a better alternative?
    – (void)swizzleParentMethod:(Class)targetClass selector:(SEL)selector swizzleClass:(Class)swizzleClass
    {
    originalMethod = class_getInstanceMethod([targetClass superclass], selector);
    swizzleMethod = class_getInstanceMethod(swizzleClass, selector);
    method_exchangeImplementations(originalMethod, swizzleMethod);
    }

Leave a Reply