Recently my team and I had a discussion panel about TDD. That made me reconsider a few things I was certain about. Here’s a little confession that summarizes the whole thing – 5 things that I no longer consider true about TDD:
1. You have to take small steps
Before I had been under impression that TDD cycle (red -> green -> refactor) needs to last 10 minutes at most. Otherwise, it’s not TDD. It may be something close, but nope. Sounds hard? Keep practicing, you’ll eventually get there.
Here is why I got that belief: I had seen a screencast (Bowling Kata) of Uncle Bob where he did that (I can’t find now). Every step took him less than 5 minutes. And that was including explanation, so it’s easily improvable in work. And that was TDD to me the whole time.
And yes, I managed to work this way a few times, but usually, I wasn’t even close. Writing tests in legacy environments was too hard to make it quick.
Then I read TDD by Example by Kent Beck (the book will be cited a few times in this post), where he highlighted multiple times – it doesn’t have to be that way.
Best quote I can find (emphasis mine):
TDD is not about taking teensy tiny steps, it’s about being able to take teensy tiny steps.
There’s even a whole chapter called “Can you drive development with application-level tests?”. The short answer is – yes.
After consideration, that makes sense to me. Consider the sole name of TDD. Do you have to make your test small, to ensure that your app is driven by tests?
Another story is that people tend to take small steps. Beck again:
Programmers practicing TDD consistently report that they take smaller and smaller steps over time.
However, let’s be polite and not revoke people the rights to work in TDD in bigger steps.
2. One can tell if steps are small enough
The previous point already debunked this claim of mine.
But let’s imagine that TDD means small steps by definition. Beck provokes this kind if thinking by describing the rhythm of TDD (emphasis mine):
- Quickly add a test
- Run all tests and see the new one fail
- Make a little change
- Run all tests and see them all succeed
- Refactor to remove duplication
“Quickly” and “little” are not technical terms. This definition doesn’t provide any number to discuss with. That’s fair. It all depends on application, problem, your experience, and other circumstances.
How would you rephrase “small steps” in more precise words? Our attempts were far from perfect:
- it’s not TDD if you write fewer than one test a day
- it could be TDD if you make smaller steps than you’d do without it
Personally, I can “feel” whether or not one makes small steps, but this isn’t a measure that you can put a ruler on. A “small” step for one could be a rather “big” step for another.
Then maybe we should completely drop the notion of “small” steps from TDD definition? What would change if we called it “fluent” or “pretty” steps?
3. TDD has a clear definition
The definition that starts the book says:
- Don’t write a line of new code unless you first have a failing automated test.
- Eliminate duplication
…and then in the book, Kent consciously goes against it a few times, which clearly means it’s more like a signpost.
Again, I’ve always thought that’s a fair approach. Ignoring (temporarily!) the main workflow instead of religious sticking to it is healthy. But this leads to unclear separation of what’s TDD and what’s not.
In the team, we agreed on the definition:
- TDD is when you work in TDD cycle (red -> green -> refactor -> repeat).
(Note that we dropped the notion of small steps)
But to be honest, I don’t like this definition. There are times when working in the TDD cycle is not TDD. Also, there are cases when your app is driven by tests, even if you don’t work in the TDD cycle.
TDD must be more like a submission to automated tests, than a few words of definition.
The bottom line is that TDD is more like an opinion. Strange thing is that despite that, we keep talking about it, and even require TDD in job offers.
4. You have to start from hardcoding output
This is how I used to imagine the implementation of addition in TDD:
1. start implementation with null example:
2. test a basic case:
expect(sum(1, 1)).to eq(2)
3. resolve this case with a hardcoded answer:
4. when there’s a reason, write another test to triangulate:
expect(sum(2, 2)).to eq(4)
5. only now you’re allowed to write the real implementation
Quite a few steps to implement sum, right? But that’s how I imagined it. Dura lex, sed lex.
Kent dispelled this impression. He provided 4 strategies of how to make a test pass:
- Hardcoded answer (“Fake it till you make it”)
- Obvious implementation
- One to many (when dealing with collections)
With a comment:
I only use triangulation when I’m really, really unsure about the correct abstraction for the calculation. Otherwise I rely on either Obvious Implementation or Fake It.
Less restricting and more human-friendly. Agreed.
The long and attentive path I used to believe in is fine for complex problems, but sometimes (most of the times?) we already know the answer. We know the implementation of sum.
5. TDD makes your design better
Again that was the impression I got from Uncle Bob. On the screencast I mentioned before, he was intentionally making the smallest decision to move to “green” phase. He even coined the premise which really fell into my memory.
The final solution was quite impressive. In my eyes, it looked like only the fact of using TDD brought him to his solution. But it didn’t.
TDD doesn’t make decisions for you. It only gives you the opportunity to better understand the problem. (Don’t get misled by the word “only”, it’s a very powerful benefit.). You’re still in charge of shaping the code.
Nobody ever said that without TDD you always write worse code. I can easily imagine NOT using TDD and achieve the same results (for example, once you’ve done a code Kata, you could likely recreate the code without a single test).
That was one of the main points of DHH in Is TDD Dead? conversation. TDD is only a tool and you’re still responsible for the outcoming code.