The beautiful theory of TDD and the reality check of practice
The difference between theory and practice... is the practice
Summary
TDD promises to make your code more flexible, with less side effect, and more reliable. You will think the shape of the problem, the boundaries, the contract, and end up with a better system.
Except...
You are not good enough to TDD.
You don't know what you are doing anyway.
You might not even want to test in the first place.
Natural selection is easier than intelligent design.
And paying upfront is not good finance management anyway.
You are technically correct, the best kind of correct
Every once in a while, you will find somebody you who tell you they figured out software. They, themself, detain the one Truth on how our industry should operate.
Of course, you'll notice it's usually young people and people that never ran a business or a very intense or big projects that come out that way.
It is, indeed, quite impossible, since:
There is not a lot of absolute truth out there, software or not.
We've been writing code for a few decades, the field is basically a baby. Even things like medicine, that we have been doing for centuries, are still trying to settle on stuff.
Production is always done in a context, full of constrains like limited resources, times and skills, with variable objectives and changing horizons. Freezing yourself up in one way of doing things just ensures that out of the sweet spot for this way, you are going to be less than effective.
One thing that has been the victim of this is Test-Driven Development.
It's a strategy to write code that invites us to write a few tests first, ensure that they fail, then write the code that will make it pass. Then write a few more, repeating the cycle until the program is working as intended.
Because it puts an emphasis on correctness and design, people that have a very strong desire to do things right or like purity will be attracted to it.
Among them, unfortunately, are a few zealots. TDD zealots are very dedicated to tell you that you are doing it all wrong.
Fortunately, there are not many of them.
In fact, there are not many TDD practitioners at all.
Why is that?
Because there are not a lot of cases in which it works well.
The benefits of TDD
TDD forces the programmer to think about the API, general design and code use case first, rather than how you are going to make it happen.
This comes with quite a few upsides:
Your code will end up being more testable, and easier to test. In fact, it will be more tested, period. That tends to make it more versatile, with fewer side effects (a very good thing), and all in all, more reliable. But also, it will just do only what it needs to do, and nothing more, because you defined the minimal case in a test.
Because you focus on what is exposed to the outside of the code first, it looks better. It also makes you think in terms of system, interactions, problems to solve, instead of
if
andwhile
blocks. You will have to define the shape of the problem and the functions that will solve it, what are the boundaries, what contract (such as typing, constraints, etc.) you wish to impose to the user and so on. All those pesky things you really don't want to think about because they are hard. But they are important.It's consistent. If everyone in the team uses the method, then suddenly you have one process to add code to the source, and while it seems underrated, it's actually quite liberating. You are not wondering how some code came to be, since you know there was first a need, then a solution, then a test, and finally the logic to implement it.
Which means less unneeded code. Because you start with the need.
Looks like a dream.
The problem is that it often doesn't work.
We are not good enough
At some point it's important to be able to self reflect, to look at yourself in the mirror, and really assess what you are capable of.
Can you write code 10 hours a day?
Can you understand the RAFT algorithm?
Can you implement OAuth from scratch without introducing a huge vulnerability?
Most people can't.
And most of us... are most people.
Just like you have to stop pretending you need FANGS level of tooling because you just don't have 10 billion users signed in your service.
Just like you have to quit thinking you can sustain this minimalist desktop setup that requires 4 context switching and 7 states holding in your head to perform actions with your keyboard that others will do with a few clicks 200 ms slower but with zero brain load.
Just like you have to forsake the illusion you can learn 17 programming languages and be productive in all of them. You can't. You can't even keep up with the ecosystem of a popular one, nowadays. Remember I write a monthly, "What's up Python?", I'm quite familiar with what that entails.
You have to realize that the people that actually are capable of doing TDD are very few.
First, TDD requires a lot of experience. You need to know how to shape an API, what are the consequence of said shapes, and how those aforementioned shapes will interact and fits together. After all, it's the opposite of an emerging design. You have to know what you are doing. This excludes juniors, and quite frankly, a lot of seniors that I know.
Second, TDD assumes you can hold it all in your head. Since you are designing the outer layer, you have to create an abstract model of the problem you are solving, the solution, the structure of it all, and how it should be settled. Then you actually don't write that, you write how an hypothetical external system is going to use that imaginary concept. That's freaking hard. That's a lot of computing power for your biological CPU.
Even if you can perform those tasks decently enough, then comes a new challenge: you have to be able to do that, again and again, for hours at a time, for days and days, until the project is done. Doing hard things for a long time, is, as you have guessed, very hard.
We don't know what we are doing
TDD comes with another hidden assumption: it's that you know the solution to your problem.
However, in practice this is not often the case.
If you create a program, this is likely because there is no good existing program to solve exactly that problem. Otherwise you would use that instead of expending the energy, time and resource to create that software in the first place.
Sure, it's not always true, there are many reasons to write software for solved problems.
Still, it's very common.
Also, there are other reasons to not know what you are doing than humanity not having the solution to a problem: you, personally, might not know the solution. Because you are a junior, because programming is not your job but you have to code somehow, because you code for a client that works in a field you know nothing about...
So you will have to either discover a solution, or learn about a solution by reading, asking for help, working with your client, etc.
This comes way before building the solution, mind you.
And how do we do that?
Yes, we do talk, we write, we draw. We use paper, pens, chats, videos, models and plenty of other tools.
But the main tool we use, is exploratory programming.
It's a fancy way to say "I'll try to code something and see what comes up".
That's a LOT of our daily work.
How many times did you start understanding something only after you struggled on the code for hours?
This code has been modified, deleted, swapped, injected with trials and errors, tortured until it talked. You don't want to write tests for this code.
You want to write the tests for this code’s gran-gran-child, the cute one freshly born out of the pain and sweat of its elders.
Not all problems are suited for testing
Being a programmer is like saying you are a doctor. It doesn't mean much until you give details. Are you a dentist or a plastic surgeon? Are you a generalist or a cardiologist? Do you work in a hospital ER or small country town private practice?
The tasks of programming are many, and some are very suitable for testings, while others, are not.
If you are exploring data, malaxing it until it rats on its fellow data structures, or if you performing analysis with graphs and extracting correlation, trying to make it pass for causation at the next marketting meeting for shits and giggles, you will not write tests for that.
If you are scripting, automating, creating batch processes, touching your pipeline, etc., you rarely want to write tests early in the project for those.
If you are deploying, well, you should test. But let's be honest, most of us don't. And the tests are not going to be a good fit for TDD anyway. You are not going to write your Chaos Monkey before you put in prod, don't bullshit me.
The thing is, a project is often a mix of all that stuff. Sometimes you don't even know if what you are working on is something that falls in one category or another, or if it will require testing, before you are quite hands deep in the blood of the process you just kill -9
.
If you were to start with testing first, that would be very unproductive.
Now, to be fair, reasonable TDD practitioners will not advocate for using it everywhere, all the time. Experienced programmers are usually pragmatic.
But they are rarely the ones writing the blog posts that will end up on your timeline telling you to TDD all the things.
These persons will conveniently avoid speaking about those cases.
Emerging designs are often better design
Intelligent design or natural selection?
I'm putting all the chances on my side: it's intelligent design, but I'm smart to make this design run on natural selection to avoid having to set every little detail in advance.
We are creating a system, that will eventually become complicated. The less we deal with complexity, the better. The less we have to try to guess the future, the more bullet-proof we get this to settle.
So instead of trying to set everything from day one, it just makes sense to choose a few bricks, then let it grow on its own, then guide the design, and repeat.
Oh, if I want to do this, this parameter will be needed. Ah, there is a side effect here. Oops, that's a race condition, better take care of that now.
There is a balance here. Don't take any decision, and you end up with noob spaghetti code, but take too many decisions, and you get a design that has been born at a time of ignorance. We are full of blind spot. Also, we get tired, distracted, hungry… I never bet on humans taking good decision regularly, I’d rather bet on decisions you can correct later.
With experience, you can set more and more things in advance, but even after two decades, I regularly catch myself overdoing it, and having to trash code because I wrote it thinking I knew what's up. I didn't know what's up. I had a beautiful image created by the brain about what's up.
But the map is not the territory.
Pay for what you need
TDD has a bad opportunity cost profile.
Because you commit to something first, then you have to follow through, you kinda sign a blank check with your code.
You decide upfront that you are going to keep this code, but you may not. In fact, you probably won't. I delete a lot of code before I'm happy with what I have, and this includes the API.
You also decided to test this code.
But, there are many parts of a code base, and actually many code bases, that are not worth testing.
Gasp!
What?
Am I advocating unreliable code?
Yes, I am.
Because it's all about a cost/reward ratio.
Some projects are very sensitive, failure cost money, business, reputation, user time, lives... They need to be very well tested.
Some projects would be better if they don't crash, but you can only afford a Pareto effort given what it brings on the table. So you test partially.
Some projects, you don't care. Let it crash. Let it burn. Let's test in prod while the users are connected. It. Doesn't. Matter.
For the first and last types, the choice is clear. But for the second one, you may only know if you want to test something after you have written the part. You don't want to test it in advance. You don't even know if it's worth testing because you don't know the cost of it yet.
Worse, a project might move from one category to another during its lifespan, several times, many times in fact! And it's not 3 categories, it's a whole spectrum, always moving, depending on the budget, politics, strategies, interest, technology, market, fun, sleep...
So what?
I've tried TDD, several times.
At first, in full zealot mode. This was clearly the one True Way. I didn't make the mistake to tell others, but damn, I tried.
I failed miserably. I've never been less productive.
Maybe it was me?
So I tried with different teams.
Most devs failed.
Almost all of them, including the ones that didn't fail, ended up less productive.
And pretty much everyone hated the process.
I've met people for whom it works, a few exceptions, very good programmers, methodical, experienced.
The nice thing is, they can do so on their side without requiring the entire team to do it, so their code gets the benefit of it. And people that can't TDD if their life depended on it can happily write the tests after the code.
I'm relieved I'm not the only one struggling with TDD :)
Loved the article, there are a bunch of hot takes! ha