Today, I’d like to address the topic of when to write tests: before the production code or after.
1. Test-last approach
The test-last approach is the regular approach most of us adhere to, and it’s also the most intuitive way to write tests.
Basically, you write the application code first and then cover it with tests. Nothing too complicated here; this is the "normal" way to develop software.
2. Test-first approach
The test-first approach is the opposite: you first write a test for the functionality you are about to develop and then write the functionality itself.
This is the Test-Driven Development (TDD) in a nutshell. There’s more to TDD than just that (see below), but the ordering of the test and production code is the most important difference from the conventional approach.
Here’s the TDD process on a diagram:
Write a failing test — First, you outline the requirements for the soon-to-be-developed functionality and express them as assertions in the test.
Make sure the test fails for a good reason — Next, you need to make sure the test fails because the requirements aren’t yet implemented. Developers often skip this step, but it’s a crucial one. The test must fail due to issues with the functionality under test, not some unrelated issue like an application-wide exception.
Make the test pass — At this step, you are implementing the actual functionality the test covers. At this point, the quality of the code is not important; the goal is to make the test turn green.
Refactor — If the code from the previous step turned out messy, clean it up. The reason for separating this step from the previous one is that it’s easier to refactor existing messy code than to write clean code from the get-go.
3. Test-first vs test-last
Believe it or not, both test-first and test-last approaches have their benefits and drawbacks. Let’s discuss the benefits of each and then let’s see how you can use both approaches to the maximum advantage.
3.1. Benefits of TDD
So, what’s the point of TDD? Why would you come up with such a complicated process?
When writing tests, you are constantly dealing with the problem that’s best described as:
Who judges the judge?
Your tests verify the application code, but who verifies the tests? How can you make sure your tests check the underlying functionality, and not just sit around doing nothing?
This is the problem TDD solves.
By seeing the test fail because of the lack of underlying functionality (or because it’s implemented incorrectly), you validate that test. You know that, should the application code stop working properly, the test will point that out.
In other words, this practice reduces the chance of false negatives (false passes): you already saw the test failing and can expect it to do that again.
This is why step 2 (making sure the test fails for a good reason) is so important. If the test fails for an unrelated reason (say, the arrange part is set up incorrectly), you aren’t validating that test and can’t know if it will fail should the underlying functionality break.
With the test-last approach, you have to do this validation manually: change the application code to simulate a bug, make sure the test fails, and then change the application code back again. This is tiresome and a lot of people just skip it, which may result in incorrect tests.
TDD is great because this step of validating your tests is baked in to the code-writing process itself, you don’t have to do any extra work.
3.2. Benefits of the test-last approach
So, what are the benefits of writing the application code first, then?
Since you don’t have to worry about tests, you can easily change your application code: refactor or even completely redesign it.
With tests, it’s harder to make such a move because you’d have fix those tests after each redesign.
3.3. When to use each approach
Both approaches have their merits and you should apply each of them at the right stage of your project.
Those stages can be divided into 2 categories:
Doing the right thing
Doing the thing right
Meaning, you first need to outline the scope of the project, to make sure it’s the right solution, and only then build that solution.
The first stage involves experimenting, sketching your domain model, whiteboard sessions, etc. You don’t need tests at this stage: they will only drag you down and there’s a good chance you’d have to throw them away along with the code you are experimenting with.
Once (after enough iterations) you are sure you are doing the right thing, you can proceed to step 2: doing that thing right. That’s where you can follow TDD and get all the long-term benefits, such as good test coverage and quality tests.
The problem with TDD is that, in order to be productive, you need to know exactly what you are building. However, once you do know that, you can benefit a great deal from the test-first approach.
Another area where TDD shines is bug-fixing. If you find a bug, don’t just fix it right away. First, write a test to reproduce that bug, make sure it fails, and then fix it. This way, you’ll never need to worry about that bug again.
Enjoy this message? Here are more things you might like:
Workshops — I offer a 2-day workshop for organizations on Domain-Driven Design and Unit Testing. Reply to this email to discuss.
Unit Testing Principles, Patterns and Practices — A book for people who already have some experience with unit testing and want to bring their skills to the next level.
Learn more »
My Pluralsight courses — The topics include Unit Testing, Domain-Driven Design, and more.
Learn more »