I’ve been thinking about what makes TDD fail, and there are obviously few reasons that were discussed to death (Get it? Dead? TDD? Ok, let’s move on).
There’s a pattern I see when working with beginners. I’m talking about TDD beginners, because these people may have years of experience on them. They follow the red-green steps religiously, and do minimal refactoring on the way.
Usually, the refactoring looks like that:
- Renaming, which is very important, of course.
- Extracting code into private methods.
- Switching control flows as part of the incremental design additions. For example, adding loops, replacing if-else’s with switch-cases.
- Small modifications to data structures , although not many.
What this gets you is a big, convoluted class that has tests, and some small internal, simple data classes.
Tests are good. But is this the magical emergent design those TDD fanatics always talk about?
Out of the five SOLID principles, the Single Responsibility Principle has the biggest impact on TDD. While SOLID principles are universal (I hear they are BIG on Alpha Centauri) TDD’s outside-in method separates the S from the others.
The OLID principles are mostly about external interfaces.
- Open/Closed principle: Usually translates in OO to working against interfaces/base classes
- The Liskov’s Substitution principle is built into OO languages. We think about base and subclasses as similar behavior through interfaces, where it’s most powerful.
- Interface Segregation is about, well, interfaces.
- Dependency injection is about testability, and about, well, interfaces.
Liskov’s Substitution principle, is a bit different from the rest, because the substitution is not just about interface, but also about behavior. Behavior means internal code.
While the L principle has small internal implementation impact, SRP is mostly about it. SRP is about answering the important design question:
Where Should This Code Go?
Should this code be in a private method?
Should it be extracted to another class?
Should it be split into base and derived classes?
Should it be called as a virtual method, overriding the base method?
Should it be tested separately?
Where should this code go?
A big part of refactoring is tediously asking this question. When we do, simpler design appears. Code moves into smaller classes and components, into smaller methods , and private methods are extracted to other classes, that make more sense (and better named).
The resulting code is staggeringly different than the one I described above. And that can make or break TDD implementations.
TDD Is Misleading
It looks like just following red-green-refactor will get you the right design.
That’s a lie.
If you believe it, you’ll get into the “TDD is dead” crowd.
The “Emergent Design” part of TDD does not come magically from the keyboard. You and I drive the design.
The best way to create the readable, understandable and maintainable design is about asking “where should this code go”.
And that’s the power of SRP. If you get into TDD without understanding it, you’re most likely to fail.