When I taught myself to code, testing wasn’t mentioned. Things were so simple, with a limited set of inputs, that it was usually obvious when it gave the expected result or not. If it didn’t, out would come the print statements and debugging by trial and error.
During formal lessons on programming, testing became part of the process and rightly so. As I wrote more and more complex code, it became obvious that manual testing could rapidly become impossible if you wanted to test the range of possible inputs and outputs. Formal proofs were offered to ease the situation, but to paraphrase Donald Knuth: the code has not been tested, it’s only been proven correct. We’d manually perform tests to ensure the code worked. It sucked. Automating the process was possible, but it was time-consuming to write our own test harnesses. And we wrote the tests after the code – that sounded right, right?
When I started using XP-style agile development in 2000, I was introduced to the concept of Test-Driven Development or Test-Driven Design (depending on who you speak to). TDD practises writing of tests first. It sounds weird: how do you test code that hasn’t been written yet? Worse, you write tests that will fail first and then write (or fix) the code to make the tests pass. It took a while to get used to the idea, but now it’s a comfortable part of my software development toolkit.
I should make it clear that I’m not one of those people who insist all code should be written test-first. If you’re writing throw-away code, for example, you shouldn’t feel bad if you don’t write tests first. However, for production code it makes a lot of sense. The initial time taken to write those tests pays off later when you need to refactor code or perform maintenance days, weeks, even years later. If you’re suddenly thrust into a legacy project cold, you’ll appreciate the fact your predecessor supplied a comprehensive test suite so you can make changes with confidence. Think of test suites as part specification, part bug tracker and part developer documentation, as well as an equal member of the code base.
Writing tests first also helps with design and implementation. We need to think more carefully about inputs and outputs – not just what is expected, but also the unexpected (what can go wrong). We have to write code that makes testing easy, so we are encouraged and rewarded for writing code that is simpler and more loosely coupled. It also keeps you focused: you write the minimum code that makes the tests pass. If you want to add more functionality, you write more tests to express that functionality. Far too often we as programmers get side-tracked, adding features we might need – all that extra code might not end up being used, but every extra line is a potential hiding place for a bug or a future maintenance problem.
One thing that doesn’t get mentioned often is that you can only have confidence in code covered by the tests. I know some who have supreme confidence in their tests, believing that everything is covered fully. It isn’t. The old mantra of “there’s always one more bug” can be re-applied for the test-drive generation: “there’s always one more test”. Keep adding more tests to cover existing code. If you’re working on something and notice a potential problem: write a test. If something might be a source of trouble, write a test to check if your hypothesis is correct. If you get bug reports, write tests that reproduce the bug before you start coding a fix. Take that test and see if there are related cases that might trigger another bug, write tests for those too. Use code coverage tools to verify how well you’re doing with test writing: programmers are humans, and humans make mistakes – but we, hopefully, learn from mistakes.
Test suites are living and breathing parts of the code base. They need refactoring, they need reorganising, they need to be added to and maintained in order to stay fresh, useful and provide a source of confidence in the code they support. Like stale documentation or comments, stale test suites can be just as much a source of code smells as the code they claim to test.
As with other techniques for improving code craftsmanship, TDD isn’t a cure-all on its own. What it does provide is a powerful addition to your programming toolbox.