To TDD or not to TDD? Is this question even relevant or maybe it’s enough to just write tests, no matter before the code or afterward? Let’s see.
Pros and cons of TDD
TDD (Test-driven development) has been a somewhat controversial topic for quite a while now. Most people fall somewhere in the spectrum between “no TDD at all” and “TDD all the things”. DHH with his famous piece resides on one side of the spectrum, and Robert Martin represents the other.
In short, DHH opposes testability and cohesion by making the argument that if you make your code testable, you incur a damage in a sense that all connections between otherwise cohesive pieces of the code are now lost and it’s hard to read and understand it that way. (Here’s, by the way, my take on this: Test-induced design damage or why TDD is so painful)
The only way to go fast is to go well.
Note that when it comes to discussions about TDD, it’s usually not about whether to write tests per se, it’s about when to do so. Everyone agrees that you need tests. The tests bear an important role of their own – increase your confidence in the code base’s correctness and facilitate refactoring. The argument is mostly about whether or not the order in which you write them matters.
So, does this order matter?
It does. I will call the two approaches to testing “test-first” and “code-first”. Test-first is when you write tests before code – test-driven development. Code-first is what the most programmers would consider the normal way to develop software: you write the code first and only after that decide how to cover it with tests.
The main benefit of TDD is that it helps validate the tests you write. And the best part is that this validation is baked into the red-green-refactor routine itself, so you don’t even need to think about it much.
How does this work? You create a failing test first, make sure it fails for a good reason, e.g. the system under test (SUT) doesn’t implement the required functionality yet or does it incorrectly, and only after that modify the SUT. It helps resolve this issue which can be described as “Who judges the judge?”. That is, if your tests verify the SUT’s currentness, how to verify the correctness of the tests themselves? Write another set of tests? Is it turtles all the way down?
By seeing the test failing, you put a stop to this infinite regression. Now, you’ve manually validated the test and know that, should the functionality change, the test will point that out. It greatly reduces the number of false negatives – when your tests remain green despite the fact that the functionality is broken.
When writing code before tests, you will have to do this validation manually. You will need to change the SUT’s implementation in order to simulate a bug, verify that the test fails, and then change the implementation back again. It works but requires the additional step and thus is more tedious and error-prone.
Another benefit of writing tests before code is that it pushes you to increase the test coverage. Again, this perk comes at almost no cost; all you need to do is follow the red-green-refactor routine.
OK, what are the benefits of the code-first approach then? There are two of them:
- The speed with which you deliver code,
They flow from the fact that you are not dragged down by the tests. It helps write the code itself, and it also helps if you decide to change the direction and completely re-design the domain model. Sure, you will need to write tests eventually, but you can postpone this phase and gain some benefits in the short term.
To TDD or not to TDD
So, where on this spectrum do I fall? I believe that you should apply both approaches but you need to do that at different stages of development.
When it comes to writing successful software, you need to focus on two things:
- Doing the right thing.
- Doing the thing right.
In this order, precisely.
What it means is that it’s important to first recognize what the heck you are going to build, and only after that build it. The first stage involves experiments and it’s critical to perform them as quickly as possible, to make sure you don’t waste time on the wrong things.
That is where the code-first approach shines. Use the code you write as a design tool: prototype the solution and see if it sticks. If it does, proceed to the second stage. If not, throw the solution away, rinse and repeat.
Once you recognize what you need to do, do that right. I.e., follow the test-first approach and reap the long-term benefits, such as good test coverage and quality tests.
It’s important to recognize both stages. Without discovering the right thing, there can be no long-term as you can simply run out of money before you find the market fit. And without doing the things right, you are setting the code base up for slow deterioration.
And it’s not only about start-ups. This is applicable even if you develop a line of business application. When you start working in a new problem domain, it’s important to get a sense of what this domain is all about. You do that by talking to the customer and sketching some quick solutions. Tests at this stage will be an impediment rather than an assistance. It’s just too risky to invest in them at this particular point as there’s a good chance you’ll throw the code away. And even if not, even if you get everything right at the first attempt, you still want to get this initial understanding of the domain as quickly as possible. Tests will only be a burden for you here.
Once you familiarize yourself with the problem domain, go with the test-driven development. The problem with TDD is that you need to know exactly what you’ll be building in order to be productive. However, once you do know that, you can benefit a great deal from the test-first approach.
Of course, your tests still need to be well written and overall maintainable, but that’s outside the scope of this article. To learn how to do that, check out my online course about Pragmatic Unit Testing.
- Benefits of the test-first approach:
- Test quality
- Test coverage
- Benefits of the code-first approach:
- Speed of development
- You need to apply both approaches at different stages of the project
- Write code before tests when you need to explore a new problem domain or build a prototype
- Write tests before code when you know exactly what you’ll be building