I admit it. Throughout my whole career at Microsoft, even as a Dev Lead, I was not a true believer in Unit Testing. That's not to say I didn't write unit tests or require my team to write tests. But I didn't believe that the benefits reaped from unit testing were sufficiently valuable given the time it took to write them (for me, about equal to the time to implement the product code itself).
Now, post-Microsoft, I am a true believer. A zealot even. I can't imagine a world in which I write code that has a ton of unit tests covering it.
So what changed? My eyes have been opened to a development world in which real testing infrastructure
exists. In my former role, what I used was a testing framework known as Tux, which ships with Windows CE. It was enhanced for Windows Mobile and given a usable GUI. The result was something like JUnit, eg, a simple framework for defining test groups and specifying setup/teardown functions. The GUI was very much like the NUnit GUI.
So far, so good. There's nothing wrong with this setup. However, a test-running framework is necessary but not sufficient for unit testing. The missing piece was a mocking infrastructure.
One of the most frustrating things about working for Microsoft (and I'm sure the same is true of other big software firms) was that everything, and I do mean
everything, had to be developed in-house. For legal reasons we couldn't even look at solutions available in the open source community. The predictable result is that a massive amount of effort is expended to duplicate functionality that already exists elsewhere. In many cases the reality of product schedules and resource constraints mean that we simply must do without certain functionality entirely. This was the case with mocking. Developers were left to create their own mocks manually, or figure out how to write a test without using mocks. I identified the lack of a mocking infrastructure as a major problem, but failed to do anything about it.
Exeunt Gabe stage-left from Microsoft to Kikini and a world of open source.
At Kikini we use JUnit for running tests and a simply beautiful component called
Mockito for mocking. I cannot emphasize enough how wonderful Mockito is. Mockito uses Reflection to allow you to mock any class or interface with incredible simplicity:
MyClass myInstance = mock(MyClass.class);
Done. The mocked instance implements all public methods with smart return values, such as
false for booleans, empty Collections for Collections, and
null for Objects. Specifying a return value for a specific call is trivial:
when(myInstance.myMethod(eq("expected_parameter"))).thenReturn("mocked_result");
The semantics are so beautiful that I am certain that readers who have never heard of Mockito or perhaps have never even used a mocking infrastructure can understand what is happening here. When the method
myMethod() is invoked on the mock, and the parameter is "expected_parameter", then the String "mocked_result" is returned. The only thing which may not be completely obvious is the
eq(), which means that the parameter must
.equals() the given value. The default rules still apply so that if a parameter other than "expected_parameter" is given, the default
null is returned.
Verifying an interaction took place on a mock is just as trivial:
verify(myInstance).myMethod(eq("expected_parameter"));
If the method
myMethod() was not invoked with "expected_parameter", an exception is thrown and the test fails. Otherwise, it continues.
Sharp-eyed readers will note that the functionality described so far requires that
equals() be properly implemented, and when dealing with external classes this is sometimes not the case. What then? Let's suppose we have an external class
UglyExternal, it has a method
complexStuff(ComplexParameter param), and
ComplexParameter does not implement
equals(). Are we out of luck? Nope.
UglyExternal external = mock(UglyExternal.class);
MyClass myInstance = new MyClass(external);
myInstance.doStuff();
ArgumentCaptor<ComplexParameter> arg = ArgumentCaptor.forClass(ComplexParameter.class);
verify(external).complexStuff(arg.capture());
ComplexParameter actual = arg.getValue();
// perform validation on actual
This is really awesome. We're able to capture the arguments given to mocks and run whatever validation we like on the captured argument.
Now let's get even fancier. Let's say we have an external component that does work as a side-effect of a function call rather than a return value. A common example would be a callback. Let's say we're using an API like this:
public interface ItemListener {
public void itemAvailable(String item);
}
public class ExternalClass {
public void doStuff(ItemListener listener) {
// do work and call listener.itemAvailable()
}
}
Now in the course of doing its job, our class
MyClass will provide itself as a callback to
ExternalClass. How can we mock the interaction of
ExternalClass with
MyClass?
ExternalClass external = mock(ExternalClass.class);
doAnswer(new Answer() {
@Override
public Object answer(InvocationOnMock invocation) throws Throwable {
Object[] args = invocation.getArguments();
ItemListener listener = (ItemListener)args[0];
listener.itemAvailable("callbackResult1");
return null;
}
}).when(external).doStuff((ItemListener)isNotNull());
We use the concept of an Answer, which allows us to write code to mock the behavior of
ExternalClass.doStuff(). In this case we've made it so that any time
ExternalClass.doStuff() is called, it will invoke
ItemListener.itemAvailable("callbackResult1").
There is even more functionality to Mockito, but in the course of writing hundreds of tests in the past 9 months I have never had to employ any more advanced functionality. I would say that only 1% of tests require the fancy Answer mechanism, about 5% require using argument capturing, and the remainder can be done with the simple when/verify functionality.
The truly wonderful thing, and the point of my writing this blog entry, is that a mocking infrastructure like Mockito enables me to write effective unit tests very quickly. I would say that I spend 25% or less of my development time writing tests. Yet with this small time investment I have a product code to test code ratio of 1.15, which means I write almost as much test code as product code.
Even more important, the product code I write is perforce highly componentized and heavily leverages dependency injection and inversion of control, principals which are well-known to improve flexibility and maintainability. With a powerful mocking infrastructure it becomes very easy and in fact natural to write small classes with a focused purpose, as their functionality can be easily mocked (and therefore ignored) when testing higher-level classes. I have always been told that writing for testability can make your product code better, but I never really understood that until I had the right testing infrastructure to take advantage of.
Now, I'm a believer.