Testing in isolation
published: Fri, 28-Jan-2005 | updated: Thu, 27-Oct-2005
One of my current projects is writing unit tests for a body of code. It isn't germane to this discussion why these unit tests weren't written from the get-go; let's just accept that the code 'works' but that it's getting to the stage where it's becoming brittle. The client just wants proof that the code works, that input A over here produces output B over there for many combinations or kinds of A and B.
Retro-fitting unit tests is a hard job. Why? Because if unit tests aren't written at the time the code is written, flexibility and good design disappear out the window as class coupling and code duplication enters the door. Business logic gets bound up in the presentation layer. SQL statements get stitched together on the fly. You find that, to test a single class generally means building the entire application and testing the class through that. Eeek, to put it mildly.
Here's a little game for you. Pick a class from the application you're working on, any class. How quickly could you write an xUnit test suite to test that class? How much other code do you have to pull in, in order to do so? Even more basic, can this class work outside the application at all?
I played the same game with some of my early code. (These days I tend to write TDD, so my classes are already at a stage where they're easily testable in isolation. Note that interface-based design helps immeasurably in this.) Boy, was this an eye-opener.
Now it must be admitted that a lot of my code over the past ten years or so has been tightly encapsulated container type code for which it is easy to write tests. But some of it wasn't. For example, I wrote a Huffman compressor and decompressor back in April 1999. The code to write or read a stream of bits was tightly coupled into the main (de)compression code. I couldn't extract it out at all. The Huffman tree itself wasn't even a class, it was a C-style structure passed around as a pointer. The only way to test all this (which was how I originally did it) is to compress a stream, then decompress it, and then check to see whether the result was the same as the original.
Frankly this is a dreadful situation to be in. If this were in production, I would be fairly happy that it would work, but what if I just hadn't tried some particular data pattern? Without writing and running tests on the underlying smaller classes, I couldn't be utterly confident.
This is what TDD, whether the pure version or some variant, gives you. Peace of mind. Being able to test classes in isolation gives you confidence that they will work (or that they will produce error conditions that you can check) when they're used together.
Being able to only test a class or set of classes when they're built into the application itself is counter-productive. The test, debug, fix cycle is going to be a lot longer if you must build and launch the entire application every time.
(I remember doing this once with an application that required a login. Every test of the class required me to login, navigate to the part of the app that contained the class, and then test it. After a couple of round-trips like that you yearn for automated tests. However, I know lots of people who do exactly that every time they make a change. And guess what? Testing gets skipped.)
Do yourself a favor. Make sure your classes are as decoupled and as self-contained as you know how. Test them in isolation not in the application.