Saturday, May 3, 2008

The problem with JUnit

I'm a big unit test fan. However, I often feel that the goals of testing run counter to the goals of good Object Oriented design. At least in static languages like Java.

Object oriented design is based on the idea of encapsulating behavior. Testing is an attempt to reveal and examine behavior. You can't have both.

I often run into this problem when implementing algorithms inside an object. From an OO perspective, I often want to declare all the methods that perform the scary math as private. They should never be called from the outside. However, I really need to test them somewhere.

Problems also crop up when calling methods that change an object's state. The state is not always directly exposed, and sometimes it's nearly impossible to indirectly detect the change.

I don't feel the same tension when testing in dynamic languages like Ruby. Usually, these languages have a stronger reflexion or metaprogramming functionality. They let me safely encapsulate the things that should remain hidden, but still allow me to pry my objects open and root around in the guts.

I can write tests using reflection in Java, but the syntax is painful. I can get around that with a library of helper methods--but for some reason, reflection makes many Java developers uncomfortable, even when only used in testing. And, truth be told, it never feels as natural as the dynamic language tests.

Similarly, I can use a mixture of interfaces and mock objects (for example, using EasyMock) to let me examine the inner workings of a class. Often this simplifies writing the tests, but the results can be incredibly brittle. Building useful mock objects generally requires a detailed understanding of our classes inner workings. If I change the implementation, I will break my mock objects, and then break my tests--even if the new version is functionally identical to the old.

Of course, even the reflection-based testing is somewhat brittle. Reflection, by definition, looks at the implementation, not the interface. But, our interactions with the implementation tend to be more surgical and specific. So, while these tests are somewhat brittle, they tend to be more resilient than the mock-object versions.

I can try to get around all these problems by redesigning my objects. Move my algorithms to a utility class, where they are publicly exposed and easy to test, or add accessors to the internal state, even if the accessors should never be used for non-test code. While this works, it can lead to unnecessarily awkward designs, or exposing more of the implementation than is really necessary.

I think, ultimately, a mixed approach is best. Like many software engineering tasks, we must examine our design and decide which sections are likely to change, and which are likely to remain the same. Static sections can often be effectively tested using reflection or mock objects. Sections that are likely to change should be encapsulated and tested as separate objects.

The trick is then to successfully separate one from the other.

No comments: