Using Categories in Unit Tests

Categories are a wonderful feature of the Objective-C runtime. They allow adding or overriding (* see note below) methods on any class. This has profound implications for unit testing. For example, this morning I was enhancing the unit tests for an object that I created to access weather information from the Weather Underground. This service is free for developers, limited to 10 calls per minute, 500 calls per day. So of course I’m monitoring the number of calls to the API that my code is making, and I realize that I’m making about 10 calls to the service when running my unit tests. Yikes, if I add any more tests that call the service, I’m going to exceed my limit every time I run tests. As it is, I’ll have to be careful to not run the tests more than once per minute.

So I started thinking about how to prevent actually calling the weather API during testing without adding flags or #defines or other messiness to the product code. The tests I’m running here unit tests. They aren’t API or integration tests, so I’m not even waiting for, or examining any return data from the weather API. So the obvious thing to do is to simply disable making the call to the weather API. But how to do this without cluttering up the product code?

Categories to the rescue. By simply extracting the call to AFJSONRequestOperation start into a separate method, I can then create a category in my test file that overrides that method. Here’s a simplified example of the code after refactoring it to work this way:

@implementation WeatherService
...
- (void)sendWeatherServiceRequest {
   NSString *requestString = [@"<path and key for weather API>"];
   NSURL *url = [NSURL URLWithString:requestString];
   NSURLRequest *request = [NSURLRequest requestWithURL:url];
   AFJSONRequestOperation *operation = [AFJSONRequestOperation ...];

// [self startWeatherRequestOperation:operation];
}

- (void)startWeatherRequestOperation:(AFJSONRequestOperation *)operation {
   [operation start];
}
...

Now in the unit test case file, which is only included in the unit test target, I create a category with a method to override the start operation:

@interface WeatherService (ForTesting)
- (void)startWeatherRequestOperation:(AFJSONRequestOperation *)operation;
@end
BOOL weatherServiceWasCalled; // Global for explanation purposes only
@implementation WeatherService (ForTesting)
- (void)startWeatherRequestOperation:(AFJSONRequestOperation *)operation {
   //Note that start is not actually called during unit tests.
   NSLog(@"startWeatherRequestOperation prevented for testing");
   //Tell the tests that this happened
   weatherServiceWasCalled = YES;
}
@end

So now when running unit tests, instead of actually kicking off a request to the weather service, it displays an NSLog message.

Alternatively, I could have added a disable flag to the code that would be set somehow during testing, but this violates my rule of not adding code to the product target solely for testing, and exposes a risk of forgetting to turn this flag off when releasing the code.

As a side note, this small refactoring also makes the code easier to unit test. For example, we can set a flag in the category’s startWeatherRequestOperation that will signal when it is called. This can be done using a global as shown here, or better using a class method in the test case class. Again, the category won’t be included in product code, only in the test target.

- (void)testStartWeatherRequestOperation {
    weatherServiceWasCalled = NO;
    [self.weatherService startWeatherRequestOperation:nil];
    STAssertTrue(weatherServiceWasCalled,@"startWeatherService was not called");
}

Using a category that is located in the unit test file provides a very simple way of modifying product code behavior when subclassing won’t work. Read on.

Follow-up 1/4/13

After playing with this for a few more days, and doing some more researching, I’ve changed my mind about using categories to override methods. From what I have been reading, categories are not designed to override methods. It may work, but is probably fragile, and indeed generates compiler and linker errors (as of 4.5.2 anyways). The correct way to override methods is to subclass. In the example code above, there is no reason not to subclass.

I can image scenarios, perhaps involving legacy code, where a subclass won’t work. For example, if the code under test is explicitly creating instances of the specific class being tested. But even then, the correct test solution is probably to refactor that smelly code.

So redoing the above code to use subclassing instead:

@interface WeatherServiceForTesting : WeatherService
@property BOOL weatherServiceWasCalled;
- (void)startWeatherRequestOperation:(AFJSONRequestOperation *)operation;
@end

@implementation WeatherServiceForTesting
- (void)startWeatherRequestOperation:(AFJSONRequestOperation *)operation {
   //Note that start is not actually called during unit tests.
   NSLog(@"startWeatherRequestOperation prevented for testing");
   //Tell the tests that this happened
   self.weatherServiceWasCalled = YES;
}
@end
- (void)testStartWeatherRequestOperation {
    [self.weatherService startWeatherRequestOperation:nil];
    STAssertTrue(self.weatherService.weatherServiceWasCalled,@"startWeatherService was not called");
}

3 thoughts on “Using Categories in Unit Tests

  1. The problem with subclassing is you have to have your test use the subclass, which becomes untenable as soon as there are even a few collaborators. At that point, might as well use Mockito or OCMock.

    1. If a mock works and is easier to use for a given situation, then by all means use it. Sometimes using a mock doesn’t work, in which case subclass-and-override often will. It’s just another tool in your toolbox. Use the one that works best for the given situation.

Leave a Reply