I’ve written a lot about TDD. So, it’s no surprise that when I published my last article at almost the exact minute David Heinemeier Hansson mentioned TDD during his RailsConf keynote, I got some questions:
Do you agree with DHH’s opinions on TDD? Do you still recommend TDD? If not, what should I do instead?
What DHH Said
If you missed the keynote, DHH’s essay captured the gist of it:
Maybe it was necessary to use test-first as the counterintuitive ram for breaking down the industry’s sorry lack of automated, regression testing. Maybe it was a parable that just wasn’t intended to be a literal description of the day-to-day workings of software writing. But whatever it started out as, it was soon since corrupted. Used as a hammer to beat down the nonbelievers, declare them unprofessional and unfit for writing software. A litmus test.
Enough. No more. My name is David, and I do not write software test-first. I refuse to apologize for that any more, much less hide it. I’m grateful for what TDD did to open my eyes to automated regression testing, but I’ve long since moved on from the design dogma.
Yes, test-first is dead to me. But rather than dance on its grave, I’d rather honor its contributions than linger on the travesties. It marked an important phase in our history, yet it’s time to move on.
The whole thing is worth reading, but I broke out some of the main points:
- TDD is a mind hack to encourage automated regression testing
- Angry test-first rhetoric leads to sadness and despair
- TDD leads to overly complex webs of objects and indirection
- You should prefer isolated system tests over unit tests
- But you shouldn’t turn that preference into another religion
- For those reasons, test-first isn’t a design practice you should use anymore.
In the essay, most of these points are tied together. They make sense on their own, but are not as intertwined as they’re made out to be.
If you don’t break them apart, arguing one point assumes other points that you may not actually agree with. When you combine that with twitter outrage and a 140-character limit, you end up with confusion, strawmen, and flamewars.
Instead, I’ll talk about a few of these points separately.
TDD leads to overly complex webs of objects and indirection
Recently, I’ve been seeing a preference in the Ruby testing community for “complex webs of objects and indirection.” I think this is a bad thing. (It’s one of the reasons I originally ran from Java!) But I don’t agree that TDD causes it.
I’ve TDD’d for close to a decade, and I don’t isolate my tests from the database. Controller-level functional tests and a robust integration test suite are also valuable parts of my total test suite, no matter how slowly they run.
Isolating your tests from the system is just optimization. And YAGNI says it’s probably not all that important yet. Besides, I’d rather get across-the-board speed boosts from SSD’s and app preloading instead of trying to optimize each individual test.
That said, TDD still has an effect on your system design.
TDD grows your code organically. This can be a great thing! But sometimes, a skilled software developer can write code that looks better than TDD would have designed it. It may be more tightly coupled with other code, it may not follow all of the OOD rules, but it’s clear, straightforward, and simple. (DHH Ping Pong has some good examples).
TDD can also lock you into an object API design before you really know what you’re building. It makes it harder to change the API later on. And that friction could cause you to settle on a worse design.
Test-first isn’t a design practice you should use anymore
TDD is a great tool for creating flexible, well-tested code. So I totally disagree with this point. I already talked about many benefits of TDD in an earlier article:
- You’ll find yourself with a more flexible, tested object model.
- Your system is by definition testable, making future tests less expensive to write.
- You amortize your testing costs across the development process, making your estimates more accurate.
- It keeps you in flow, because you never have to decide what to do next.
Besides those benefits, it helps you practice testing. Because you’re always writing your tests first, you’re going to test a lot more than if you tried to test after the fact. You’ll know right away on whether your tests are actually correct (because they’ll fail first). Plus, you don’t have an excuse to skip the tests “this one time.”
Angry test-first rhetoric leads to sadness and despair
Totally, 100% agree. Yes, I’m posting this again. But if someone isn’t TDDing because they don’t know about the benefits, or they haven’t quite learned how to do it yet, or they feel uncomfortable with it, the correct approach isn’t to shame them, it’s to help them. If TDD is useful to you, show them how it helps you. If they’re interested, help them get started.
If they’re not TDDing because they prefer the way the code turns out without TDD, let them.
As you build expertise, you’ll develop your own intuition, your own sense of taste, and your own preferred techniques for developing software. And no internet argument is going to teach an expert to ignore those things.
So, if you’re going to criticize someone for not following your idea of best practices, you’re either hurting their ability to learn or screaming into nothingness. Which is a waste of time in the best case, and hurting your cause in the worst.
So should you keep TDDing?
Yes! I absolutely suggest that you learn and practice it.
I credit TDD for teaching me how to test, and I still get huge benefits when I TDD. It’s not perfect, but no one technique is the key to magically producing better code.
Instead, you have to keep learning new patterns, techniques, and tools. Practice applying them until you can use the right pattern in the right place because it feels right.
As you learn, take some time to modify, refactor, and experiment with your code. Look at your old code and your new code. Get someone more experienced than you to review it. Did you make it better? That’s the way you grow.