Using a hammer to drive in a screw. I mean, it works, kind of. But if you use a tool in a way other than its intended purpose, you’ll be missing its most important benefits. It’s kind of like that with test-driven development (TDD).
Is TDD about preventing bugs? That’s more of a side effect than a direct goal.
Is it about making a test suite? Well, kind of. But… no. Not really.
What is the test suite for? Until you know the intended purpose of test-driven development, you may apply it incorrectly. TDD may seem like wasted work. And without some guiding principles, you won’t know how to optimize your TDD to get the most out of it.
So let me just lay this card on the table: TDD is about feedback. Fast feedback.
Let’s look at how it works in the three steps of the “TDD Waltz”:
Fast feedback step 1: Write a failing test
TDD operates in a tight, 3-step loop. The first step is to write a failing test that clearly states the expectations.
Doing this for the first time on any feature is the hardest part of TDD. We’re used to thinking about how to code something without TDD, then asking, “How would I write a test for that?” But that’s just test-after coding, hidden in your head.
In test-driven development, a test ideally specifies one small piece of the desired outcome, without caring about the implementation.
This first step provides fast feedback of your “inner API.” Mark Seemann puts it this way: “If a test is painful to write, make the System Under Test easier to use.”
TDD is about feedback; if a test is painful to write, make the SUT easier to use.
Unfortunately, some people tolerate pain too well.
— Mark Seemann (@ploeh) December 16, 2014
Pay attention to the ease or difficulty of writing a test. This fast feedback guides API design.
This feedback isn’t automated, and isn’t complete: You still have to think, apply good API principles, and apply good testing principles. But it sure helps, and if you listen to it, it’s fast.
Fast feedback step 2: Make it work
The second step of TDD is to make the failing test pass, in the simplest way you can.
The important thing here is the transition from failing to passing. If a change in the production code affects the outcome of a test, we can have confidence that the test is hooked up correctly. It’s like flipping a switch on and off, and watching the light change.
This step provides fast feedback about the communication between your production code and your test code.
This feedback is automated, but it isn’t complete: You still have to think, and figure out the simplest thing that works.
Fast feedback step 3: Make it right
The third step of TDD is to refactor the code, both the production code and the test code.
The cardinal rule of refactoring is to stay green: start with passing tests and never trigger failure. This is true refactoring by the book, making repeated small changes to nudge the code toward clean design.
(Test code should be so simple that any refactoring there is also simple, like “rename variable” or “extract method.” Anything more complicated deserves its own layer with its own tests. But if you ever get nervous, just deliberately break the production code to watch your test fail, then undo.)
This step provides fast feedback about the correctness of your refactoring.
This feedback is automated, but it isn’t complete: You still have to think, apply good refactoring principles, and understand clean code.
Keep the feedback fast
These three steps work in a tight loop: make a failing test, make the code work, then make it right. Here’s the magic: Each iteration is a small piece of the goal, so we’re already breaking the big problem into smaller problems. But each iteration has three steps, each with a different mindset focus. So we’re breaking the smaller problem into even smaller steps.
At each step, you get feedback. If you make a misstep, just undo. If the direction of the iteration turns out to be wrong, just revert.
The key is to get that feedback often, and as fast as you can. Focused thinking in micro-steps, with fast feedback, keeps me from having to backtrack very far. Instead of trying to fix problems I just created, I simply revert to the last clean state! This makes it possible to get “in the zone” of each mindset focus, without being distracted by the mechanics. I want TDD to give me feedback so fast, that it stays out of the way.
So in upcoming posts, let’s explore practical ways to work on feedback speed:
- How to Easily Switch Your iOS App Delegate for Testing
- Are Slow Tests Killing Your Feedback Loop?
- 3 Most Important Parts of the Best Unit Test Names
Your experience of TDD so far — what has it been like? Have you ever made it past the mechanics to achieve “flow”? Leave a comment by clicking here.