Writing Testable Code
Posted: January 22, 2012 Filed under: Development, regular | Tags: Development, Objective-C, Pattern, Python 2 Comments »
In the last few years, several types of development have emerged: Test Driven Development, Behavior Driven Development and others. At the same time I see the benefits of what is stated by their creators, I don’t think that writing tests or specifying behavior upfront always a good thing for all kinds of projects. In fact, I suspect it isn’t even possible for some projects, due its natures.
Please beware I’m not discouraging you to use those methodologies in any way. My objective with this essay is to encourage you as developer to write as much testable code as possible.
What Testable Code Is?
We should start with the modern definition of code, according to Wikipedia:
In computer science, source code is a text written using the format and syntax of the programming language, or a computer language of another purpose, that it is being written in.
Basically, it is the text written in some language, defining a set of instructions, that will be transformed at some point into computer commands and executed by the computer.
For the computer, it really doesn’t matter how we write the code, as long it makes sense at the time it executes. Simple like that.
Several programing paradigms were created since the beginning of computing: procedural, functional, object oriented programming, amongst other more obscure. As I mentioned before, the net result of each one of these programming paradigms is to produce computer executable code, and it is possible that programs written in languages implementing different paradigms produce the exact same computer executable code.
What I call testable code is the code you, as developer, write and have enough hooks to enable it to be tested when necessary. For instance, take in account the following piece of Python code:
def read_input_file():
file('/tmp/input_file.txt').readlines()
This code will be interpreted and transformed correctly and does what we want: it opens the right input file, and read its contents. Now, what if we want to test the behavior of our application with a different input file? One could, quickly, change the code:
def read_input_file():
file('/tmp/test_input_file.txt').readlines()
This represents a chunk of code that isn’t testable. By doing a simple modification to our function, we can turn it into a piece of testable code:
def read_input_file(input_file_path="/tmp/input_file.txt"):
file(input_file_path).readlines()
Writing Testable Code
Of course, the example above is quite simple and doesn’t represent the whole class of cases we can find. At work once I found a class method that returned, aided by several heuristics, a database table name. This class method was quite important, since it was used by several other methods within that class to perform all sorts of operations. The problem was that there were no easy way to override the heuristics to use a test database table. The solution was to use brute force: overwrite the method to return the table name I wanted. If I had to write a Python version of the original class, it’d be something like the following:
class MyClass:
# ...
def table_name(self):
table_name = ... # use some heuristics to calculate the table name.
return table_name
To transform it to a testable piece of code, I’d do like the following:
class MyClass:
_table_name = None
# ...
def table_name(self):
table_name = self._table_name
if not table_name:
table_name = ... # use some heuristics to calculate the table name.
return table_name
We could test the code like this:
>>> obj = MyClass()
>>> obj._table_name = 'Bar'
>>> obj.table_name()
'Bar'
>>> obj = MyClass()
>>> obj.table_name()
'Foo'
Some other programming languages are more structured, with practices that allow code to be tested more easily. One example is the Cocoa framework, which makes extensive use of the Delegation Pattern, where a piece of code delegates the responsibility of certain aspects of the code path to a delegate object. This approach makes the code more testable.
Below you can see a delegate protocol definition:
@protocol MyClassDelegate
- (void)myClassDidFinishLoading:(id)obj;
- (void)myClass:(id)obj didFailWithError:(NSError *)error;
@end
And now, the implementation of a class that uses the MyClassDelegate shown above:
@interface MyClass
@property (nonatomic, weak) id<MyClassDelegate> delegate;
- (void)doSomething;
@end
@implementation MyClass
@synthesize delegate;
- (void)doSomething;
{
BOOL success = NO;
NSError *error = nil;
// Do something important here, and update the ‘success’ variable. If an error
// occurs, the error description will be stored in the ‘error’ variable.
if (success)
[self.delegate myClassDidFinishLoading:self];
else
[self.delegate myClass:self didFailWithError:error];
}
@end
We know that when the -doSomething message is sent to an MyClass instance, it’ll send either the -myClassDidFinishLoading: or -myClass:didFailWithError: messages to the delegate instance. If we want to test the code written above, it’d be as simple as this:
@interface MyTestDelegate : NSObject <MyClassDelegate>
@end
@implementation MyTestDelegate
- (void)myClassDidFinishLoading:(id)obj;
{
NSLog(@"SUCCESS");
}
- (void)myClass:(id)obj didFailWithError:(NSError *)error;
{
NSLog(@"FAIL: %@", error);
}
@end
And somewhere in your test case:
// Create a MyClass class instance.
MyClass *obj = [[MyClass alloc] init];
// Create our test delegate object.
MyTestDelegate *testDelegate = [[MyTestDelegate alloc] init];
// Assign our MyClass instance’s delegate to our test delegate object.
obj.delegate = testDelegate;
// Send a message to obj and expect to read either “SUCCESS” or “FAIL” in
// the application’s log.
[obj doSomething];
Conclusion
Some programming languages have this testability aspect built in its core, making it easier to write code that’s easy to test when required, but the majority of the programming languages require a good deal of discipline and organization from the developer’s side.
Although the Delegation Pattern is one of the signature patterns in Objective-C, nothing stops you to design software using this pattern, or even the Inversion of Control Pattern in projects using Python, Ruby or even Perl.