Test Driven Development (TDD) was introduced more than a decade ago by Kent Beck and has been widely adopted by effective, agile development teams. Still, after all this success, it is surprising how often we miss the reason for TDD. Here’s the surprising thing: Tests are not the point of Test Driven Development, they are a useful byproduct. When plants perform photosynthesis they produce oxygen as a byproduct, and we are all thankful for that. We are also thankful that TDD produces vital tests for our code, but the point of TDD, and the reason you should adopt it is something else.
TDD has 3 simple steps performed in short, tight cycles (perhaps as short as 30 seconds each when you are an expert):
- Write a small test that fails
- Write just enough code to make it pass
- Refactor the code until it is clean
In step 1, you are asking yourself, how do I communicate with this code and what is the most useful result I can expect back? Because you are writing the test first, you start by envisioning the best possible interface to the code and the simplest, most useful results. As you implement the code, reality and other constraints may nudge you off of this ideal, but it is always the starting point.
For step 2, you write just enough code to make the test pass. Often, this will be a completely fake implementation just to get the test passing as quickly as possible. Red (i.e. failing) tests will tend to make you twitch in discomfort. If the real code necessary to turn the code green isn’t right at your fingertips, implement something less real but satisfying the test’s demands. Now you have a functioning test that is protecting you from straying off the path of green.
Step 3: You have a passing test, congrats! Now with the code constrained by the need to keep the test green, you refactor the code to make it conform to the rules of good design and craftsmanship. When your implementation is satisfactory, return to Step 1 by asking yourself, “What is the next test I need to write?”
Although the TDD rules are simple, there are frequent misconceptions.
- You write all the tests first before writing any code. No. As described above, write one small test, then one bit of code to make it pass. The tests and the code are implemented together in tight iterations.
- TDD takes too long. If you haven’t been writing tests at all, then yes, more time is added to your development effort, but this is true if you write them first or last. What TDD teams realize once they start using it expertly is that overall development is faster because of reduced rework, refactoring, and bug fixing. A summary of research about TDD effectiveness can be found here.
- TDD is primarily a testing process. This is the point of this entire post, so let’s explore it in detail…
The Evolution of a Test Driven Developer
With any skill, it takes time to become an expert. Until you reach a certain level, the true benefits of that skill don’t reveal themselves. With TDD, there are 4 stages you must grow through.
Stage 1: Write Unit Tests
You have to start somewhere. Learn the unit testing framework(s) available for your development language. Leverage the shortcuts and plugins in your IDE that make unit testing as quick and easy as possible.
Stage 2: Write Good Unit Tests
What makes for good unit tests is the topic for another blog post, but in general, remember that unit tests test a unit of functionality, not a unit of code. Your tests are applying inputs and examining results for testing a small bit of functionality. How this functionality is realized in code is not their concern. Unit tests also run as independent units in isolation from one another. They can be run in any order and always get the same results. And they are fast. You’ll be running them frequently and so they must be as fast as possible.
Stage 3: Write Good Unit Tests First
You are now asking, ‘What’s the next test I need to write?’ instead of ‘What do I need to add to the code?’. When you’ve run out of ideas for tests, you are done with the code. You are always thinking first about the next bit of functionality that must be tested for, not about how it will be implemented. You will remain (happily and productively) at this stage for a long time.
Stage 4: The tests drive design decisions
It’s at this final step in your TDD evolution that you realize – it’s not about the tests. You now rely on TDD to drive good design. So finally we reach the point of this post: Test Driven Development is not a test tool, it’s a design tool.
Here are a few ways this works:
The short feedback loop of the TDD discipline gives you a quick and constant evaluation of your decisions. If tests are getting hard to write, there’s something wrong with the code, back up and take a look at it. If the tests are red for too long while you try to figure out an implementation, your design needs attention. Maybe break things into smaller, more easily understood chunks.
Pressure Against Gold-plating/Future-proofing
It’s tempting to write or extend code to handle conditions that might (but probably won’t) be needed in the future, and then, if you are doing test-last, forgetting to write a test for the currently unneeded features. When your tests are driving your development, you’re always trying to tease out the design details of your current concern – what is being asked for right now. When those concerns are met, you’re done. If design changes are requested in the future, you start from a great place with well-written tests to guide you, and clean code to work with.
Pressure Against Complexity
One of the challenges of test-last when you are trying to achieve a certain target for lines covered by tests, is reaching every dark corner of deeply nested branches and every last private method that the code under test relies on. Hallmarks of tests written after the fact for legacy code include a huge amount of setup and an alarming amount of mock-object use, all done to help the tests reach deep down into the complex code.
When a test is driving development, every branch and every method is by default easily reachable. The tests dictate their creation, and there is no need to go back and figure out how to exercise a particular branch through the code. The well-written tests also serve as documentation about how the code is used. Also, because you are keeping the tests simple and concentrating on small increments of functionality, you are less likely to create complex cyclomatic craziness.
Promoting Cohesion over Coupling
I’ve recently reached a new stage in my testing philosophy. My practice has always been (in the Java world) to create a one-to-one relationship between the test class and the code under test. That is, for a Foo.java class, I would have a FooTest.java class containing all the tests for all the methods in Foo. I am now coming around to the idea that it is better to group tests by functionality. A test class that is focusing on a specific piece of functionality will naturally bring together, or bring about the creation of, objects that cohesively work together to perform that functionality. A test that is solely mapped to one object will naturally prefer that object to perform the entire function itself (even if it shouldn’t), leading to coupling and violations of the single responsibility principle.
Test Driven Development is a guide. It will help you make good design decisions but will not guarantee it. TDD provides positive pressure – pressure than can be ignored or misinterpreted. Writing good software is an art requiring experience and discipline to maintain the correct balance between all the competing, conflicting pressures.
Your Tests are Talking To You
When you focus on quick TDD cycles and are sensitive to times when you struggle to write a test or struggle to complete the code that makes your test pass, then you will immediately be aware of design problems and will course correct. Every test you write is telling you something about your design. Every test you write first is trying to influence your design to be better.
Better design, cleaner code, and a suite of tests. That’s a pretty good deal.