Unit Testing Tricks With Method Replacement
| February 5th, 2010Automated unit tests can be incredibly awesome and incredibly frustrating. Sometimes, in order to write a unit test you’re forced to design your code in such a way as to make the test feasible to write. Often this is a good thing (even though it’s annoying at the time you realize you need to refactor), but other times its just not practical. In these cases, method replacement might do the trick.I don’t at all think that method replacement is a substitute for well-factored code, but often there’s just that one not-very-unit-test-friendly thing that your class under test needs to do: read a file, access the network, interact with the UI, or something similar. Sure, you could factor out that one piece into it’s own class, then provide a way to use a mock instance in your class under test. If theres a lot going on, then maybe that’s the way to go, but for small cases it’ll often lead to messier, less understandable code, not cleaner code. In these cases, method replacement works great.
In an earlier post, I talked about extending a class at runtime using (what Ruby calls) modules and I’ve added a github repository for it. It covers most of the functionality we need with the exception of undoing the changes it made. The implementation for this (along with absolutely no error checking) is pretty short:
+(void)revertMethod:(SEL)methodName onClass:(Class)cls
{
NSString* savedSelectorName = [[NSString alloc] initWithFormat:
@"%s_%s", class_getName(cls), sel_getName(methodName)];
SEL savedSelector = NSSelectorFromString(savedSelectorName);
Method savedMethod = class_getInstanceMethod(cls, savedSelector);
IMP savedImp = method_getImplementation(savedMethod);
Method existingMethod = class_getInstanceMethod(cls, methodName);
method_setImplementation(existingMethod, savedImp);
}
If you read the other post, you’ll notice that we saved the original implementation of a method in a new method: <class name>_<method name>. This way, when revertMethod:onClass: is called, we can just reset the implementation. The one thing we can’t do is get rid of our backup method since this capability was removed from Objective-C 2.0 apparently…oh well.
Now that we have a way to reset our method back, using it is a piece of cake. Pretend our class under test, Foo, has a method readDataFromFile that returns a string from some configuration file. We don’t want to be bothered with this during a test, or we want to supply specific values for it while testing. So we might create our fixture as follows:
@implementation FooTest
-(void)setup
{
[BRClassExtender
extendClass:[Foo class]
withModule:[FooTest class]
override:YES
method:@selector(readDataFromFile)];
}
-(void)tearDown
{
[BRClassExtender
revertMethod:@selector(readDataFromFile)
onClass:[Foo class]];
}
-(void)testConfiguration
{
// do something with foo
}
// this is our replacement implementation
-(NSString*)readDataFromFile
{
return @"my configuration";
}
@end
Now rather than opening some file and reading it, we simply return the string specified. Note that the method can be private since the test case doesn’t care if it’s in the interface of Foo or not.
Using this kind of testing technique can be a slippery slope. Obviously, if we do this, we’re no longer testing the implementation of the method we overrode. I prefer only using it if I can keep the overridden methods really small and it’s easy for me to verify they work by some other means (like running the actual application).