Mathieu Eveillard

TDD is dead. Long live testing.

TDD is dead. Long live testing.

When you’re a proponent of a particular approach, it’s important to listen to what the critics of that practice have to say.

Among the critics of Test-Driven Development (TDD), one voice stands out in particular: that of David Heinemeier Hansson. Everyone calls him DHH—it’s cooler that way.

A multifaceted figure, creator of Ruby on Rails and class winner of the 82nd edition of the 24 Hours of Le Mans, among other notable achievements. His iconoclastic ideas resonate with me, even if I don’t necessarily agree with them.

In 2014, his article “TDD is dead. Long live testing.” made history. In it, DHH expresses his weariness at feeling singled out because he is not a proponent of TDD.

The words are strong: “fundamentalism,” “religion.” You can tell he’s really fed up.

So let’s go through his arguments point by point. This is sort of my “response to DHH,” even though I doubt he’s expecting any response from me.

“TDD is used as a hammer to beat down the nonbelievers”

[TDD is] used as a hammer to beat down the nonbelievers, declare them unprofessional and unfit for writing software.”

Indeed, it makes no sense to point fingers at people. It hurts, and it’s the opposite of the humility so dear to software craftspeople. Keep in mind that I might be wrong, and that what’s good for me isn’t necessarily good for someone else.

It’s hard to argue with him, actually, because results matter above all else. If he produces “good code” without TDD, great! Glenn Gould wasn’t the most “academic” of pianists—to put it mildly—and yet… what a performer!*

However, the question of what constitutes ‘good code’ remains to be defined. Beyond the absence of bugs, this means to me having tests—and not just tests at the level of the system as a whole. I want unit tests that describe the application’s behavior exhaustively and at the lowest possible level.

These tests document the code, are co-located with it, and act as a safety net. The finer the mesh, the better we can avoid regressions.

“Rebalance the testing spectrum from unit to system”

Step two is to rebalance the testing spectrum from unit to system. […] I rarely unit test in the traditional sense of the word, where all dependencies are mocked out, and thousands of tests can run in seconds.

The practice of TDD is indeed understood in the context of unit tests. TDD says nothing about higher-level tests (integration tests, acceptance tests, end-to-end tests, etc.), and above all, TDD does not claim that there is nothing beyond TDD!

Except that tests of the system as a whole cannot replace unit tests. I’m not going to test all the cases of a function that calculates income tax using end-to-end tests, which take just as long to write as they do to run. That would make no sense.

Each type of test contributes to the whole. It’s no coincidence that we talk about a testing pyramid. Unit tests provide very rapid feedback (a few milliseconds), and this is beneficial in two ways:

It’s given birth to some truly horrendous monstrosities of architecture. A dense jungle of service objects, command patterns, and worse.

I’m not familiar with any of these complications or any of these undesirable side effects when I do functional programming. So is this a result of TDD or of Object-Oriented Programming?

On the other hand, when excessive dependency injection leads to coupling the test with the implementation and “testing the implementation,” yes, it’s time to put down the pen.

“I do not write software test-first”

Enough. No more. My name is David, and I do not write software test-first.”

One last thing, and not the least: throughout his article, DHH refers to “Test First,” even though the title promises to discuss TDD. Yet these are two completely different things:

It’s a shame this confusion has never been cleared up, because it masks an entirely different way of programming.

As always, let’s be pragmatic: TDD is a tool, one among many. It’s meant to help. If it doesn’t help, it’s because it’s not the right tool for the problem at hand and we’ve fallen victim to the Law of the Instrument.

TDD lends itself particularly well to domain code—the functional core of a bounded context. The heart of the hexagon, the heart of the reactor. A calculation engine, fine-grained management rules, edge cases galore. There, I don’t know how to do it any other way than with TDD. But in the end, that represents only a small part of the codebase—30% at most.

In the end, I agree with almost none of DHH’s conclusions, but his arguments did have the merit of prompting me to better define the parameters of my own TDD practice.

TDD isn’t a religion—it’s a tool 😊

* To try to convince you: https://www.youtube.com/watch?v=qEHsDe5Vaxg

← All posts