It was about 19 years ago now that a colleague of mine lent me a copy of a little white book. That book changed the way I looked at programming more than any other book (though The Pragmatic Programmer gave it a run for its money). One of the things, in particular, was Test Driven Development, or TDD.
Like many developers who came across this idea, I liked TDD in no small part because it was something I could do without needing to convince a whole group of people to do it with me. Sure, it works a lot better if the whole team does it, but if you’re working in a non-Agile environment, you might well be working in your own silo anyway – so why not use TDD there? Plus, you can move on from that to doing a proper build script, followed by a build server to monitor the tests – and then spam people who break your tests. (This was also about the same time that I came across the Apache Ant project, and about 2 years later I got involved with the CruiseControl project as well). While there are a lot of XP practices I’m willing to work without in favour of simply being able to work, there are ones I won’t compromise. TDD is at the top of that list – because it’s the one I get to choose. It’s about how I work.
I’m not a 100% dogmatic purist about TDD. Very few people are, not even Kent Beck. I recognise that 100% test code coverage is neither sufficient or necessary – not all code needs tests (particularly generated code, like getters and setters), and 100% code coverage doesn’t find the bugs due to code that isn’t there. Sometimes I write the tests post-hoc. Sometimes I don’t write the tests at all, especially if the code I’m working on is temporary placeholder code. I never write tests for look-and-feel (though I will write tests that display the UI component so I can see it).
The essence of TDD, to me, is:
- determine what you want to achieve
- determine how you will know you have achieved it
- write a test to verify that you’ve achieved it (which should fail, initially)
- write code to achieve what you wanted
- later, rinse and repeat until done
- periodically, take time out to clean up and refactor, improving the design and implementation without breaking your tests
There are feedback loops between “write the tests” and “write the code”. Often, especially when starting a new feature, you won’t really know how to write the test to verify that you achieved your goal. As your design gets clearer, your tests will move towards being more specific.
The early stages of a new feature may be ridiculously simple. For example, if I was writing a service to, say, accept an order in a shopping cart, my first test would be along the lines of “Add item to cart, then check it is there”, and the code for “Add item” would do nothing, while “check it is there” returned true. As that feature evolved, the shopping cart would remain in memory. Depending on requirements, it may never leave memory storage – say, if carts were only valid for the current user session. It certainly wouldn’t get a test pushing it to persistent storage early – and even then, most of the tests would probably use the cart without persistent storage, because real unit tests should never write to disk (or call the network, for that matter).
The observant reader would have noted that the last sentence was the first time I specified “unit tests”, instead of just tests. That’s because TDD, to me, is more than “just” unit testing. I take TDD to embrace any test specified by the developer writing the code that makes the test pass, in a feedback loop. You should see a failing test, then write code, then see it passing. I’ll accept it if you write the tests afterwards, if you go back and remove or comment out the corresponding code, see the failure, and then put the code back in. The important part here is that you do this all in the same temporal context.
(Other forms of tests – especially proper BDD, where the Goal Donor specifies behaviour – are important and/or valuable, but they’re not TDD. Except manual regression testing. That’s just a waste of time, and an indicator that you don’t have enough good automated regression testing. Save manual testing for things you can’t automate)
I typically write unit tests by doing some pseudocode in comments. Things like:
// should start with an empty cart // should be able to add an item to a cart // should be able to see items I've previously added to a cart // should be able to remove items from the cart
This list will grow to become my tests. I usually, but not always, work on one test at a time. I will run just the test I’m looking at until I’m happy with the solution for that test. I’ll then run the tests for that class, followed by the tests for that project. Usually, these will be passing okay. Sometimes they’ll have broken – for example, I may have introduced a dependency that wasn’t there before, or changed a method or constructor signature. My typical cycle time for this is a few minutes, so it’s easy to decide to either revert the change and try again, or to commit to it and update the broken code.
(Sidebar: One of the biggest reasons I use Eclipse for my Java work, instead of IntelliJ, is the ability to work with code that has compile errors. Not being able to run tests over a class just because a method that is only called in a couple of tests won’t compile just feels weird – let alone compile errors in code that isn’t even touched by those tests!)
I try to test only one logic path in a given test case. This occasionally results in a few assertions, and I’m okay with that.
When I notice that I’ve got a bunch of similar looking tests, that’s usually a sign that it’s time to refactor the code-under-test to break out another class, so I can make the tests more focussed. A good example, for the shopping cart, might be rules for calculating discounts, or shipping, or if a minimum spend has been reached.
One big time I’ll work on a bunch of related tests would be when I’ve got a test that’s a specification. A good ‘go-to’ example I’ve used in the past is a Roman Numeral convertor. You can easily specify several tests at once:
- 1 = I
- 2 = II
- 3 = III
- 4 = IV
- 5 = V
- … and so on
With tests like these, I’ll often run them all at once, but I’ll still only focus on making one pass at a time.
A common time I’ll specify a bunch of tests together is when I’ve completed the “happy case” for a feature, and need to add various edge conditions. This is often easy to do at a time, because they share the code for checking if the test is complete. This is often “something didn’t happen” (e.g. when writing validation code), or “the usual thing happened, and I’m just checking a different detail”).
Every so often, I make a test pass, and other tests – that I wasn’t focusing on yet – start working. That’s usually okay. Sometimes, it just means I haven’t got all the details in yet.
When writing UI code, I usually have tests that specify UI elements are present. These help when/if I end up writing UI-driven system-level tests. I won’t write tests that look to see, say, if a button is red – but I will write a test that checks the button has the right CSS style (with a semantically relevant name, such as “cancel”, or “important”, or “secondary”) that will make it look red. When I write these tests, I usually make it so they leave the UI component visible so I can check the look-and-feel by hand.
That’s what TDD means to me, and how I do it.