TDD: what’s the purpose?
By Marco Lijoi, Software Architect
If you want to know more about Test Driven Development (TDD) you can search the web and you’ll find mostly technical descriptions about this topic. But in this post, you’ll read about how to implement it into your daily work, the impact it has on teams and how it can change mindsets for the good. I’ll give you a real-life example and share how we adopted TDD in the Edge team at Adevinta.
Start with a test that fails
TDD is defined as a development technique where you must first write a test that fails before you write new functional code.
This definition has always confused me:
“How can you write a test for a code that doesn’t exist yet? Is it not just easier to write the test after?”
Probably yes, but the purpose of TDD
- is NOT to use tests to prove that your code is working
- but is to use tests to help you write code that does what it’s supposed to do!
When you write the test after the code, you’re focusing solely on the code passing the test and this means that a bug is always possible. But if you write the test in advance, you’re focusing first on what the code should do and then on implementation.
This is the real meaning of TDD: forcing the developer to focus only on what should be done, preventing misuse of tests. As a side effect, TDD also helps developers write cleaner, reusable code, making it easier to follow the SOLID, KISS, and YAGNI principles.
Small is beautiful
Writing tests in advance is hard. The easiest way to write a good test case is to keep the test itself really short. This forces you to apply a proper separation of responsibilities and concerns.
Also, the TDD development loop says you should write a failing test (assertion) and then write the smallest code that makes it pass. Seems easy, right? In practice, it’s not but you should do it anyway. This is true especially when you’re studying a new development language because it’s hard to find a tutorial or guide that starts with teaching you how to write tests rather than just teaching you the language syntax. So, when you need to solve a problem, it’s natural to start writing code and you’ll probably end up writing too much code before writing the test. If this happens, you must delete your code and start again, following the TDD approach.
It needs time, practice and patience to change your behaviour, but in the end, you’ll learn that, by having this mindset, you’ll write cleaner and better code.
Practise means progress
If you need to practise you can use katas — a good approach to learning TDD by solving easy tasks that have already been solved by other developers, so you can compare yourself and improve your skills.
This is only the starting point: learning with katas can help, but when it comes to using TDD in your day-by-day work, you’re on your own to solve problems unless you’re doing pair-programming.
Practical advice and purpose
Applying TDD principles in practice is tricky and mistakes are easy to make. So, the first thing you should do is find someone with more experience and plan some pair-programming sessions.
For example, one person can start writing a single test assertion and the other should write the code to make it pass. Then you can switch roles after some time or you can use the pomodoro technique and switch roles after a fixed amount of time.
Learning TDD is easier if you know how to write tests, but it’s essentially a question of practise. I can give you some more practical advice that I’ve learned over the years, specifically to clarify the purpose of TDD. I remember when I had to develop a new feature and use the response from an endpoint I’d found in the third-party service documentation. I started to write some code with unit tests and I was mocking that endpoint response by following the documentation. The feature was entirely based on that endpoint response.
After finishing all unit tests and classes, I wrote the integration test by calling the real service and had a nasty surprise: the documentation was wrong and that endpoint became unavailable. How much time I could have saved if only I had started writing the integration test first!
Now I know that, when facing a development problem, you should start by writing your intent: that is to say, by writing what you want to achieve or solve. For example, if you’re fixing a bug, you should write the test to reproduce the bug and you should add some assertion to check the expected behaviour. If you’re adding a new feature to your service, you should add a test to “describe” your expectation. As an example, when you need to add a new endpoint to your service, you should start by checking the status code for the successful response. Then you should check the response body, the headers… and don’t forget possible errors.
And if you need to use a particular Model in your Controller, you should mock it. This way, you’re not only developing the controller but also designing the Model. When you’ve finished writing the Controller, you’ll already have the interface for the Model and you can start writing a test for it. This loop ends when your feature is ready. But as you can see, you’ve already designed the components that you need in your application. That’s why I prefer to say that TDD is in reality a Test Driven D̶e̶v̶e̶l̶o̶p̶m̶e̶n̶t̶ Design methodology.
In summary, you should add the test that will help you clearly declare what you should do, even if this is sometimes the most complex test.
Small steps make sense
I remember when I started to apply TDD rules, I thought I had to write the full test before starting to write the code, but I was wrong. You should just write the first test assertion or expectation and then write the code, then add another test assertion and write more code… and so on.
It seems crazy: writing tests and code at the same time appears to be bad practice since this behaviour is forcing you to switch constantly from test and code, but it’s not crazy at all! This constant loop lets you focus on solving the problem in a step-by-step loop, facing one small problem at a time.This attitude is the real benefit of using TDD every day, because it helps you save a lot of time.
Code quality is the priority
I admit, before joining Adevinta, I didn’t realise how powerful TDD could be. I’ve spent months trying to understand how TDD should really be applied and I’m still trying to force myself to delete the code that I sometimes write without first writing a test for it. But inside the team, we’ve decided that code quality has a higher priority over everything else.
When reviewing our team-mates’ code, we’re also reviewing tests and if some of them are missing, the pull request (PR) is rejected. We should all probably start the review by checking the tests because they can also be used to document what the code should do.
To help us in this process, a team in our organisation has developed Quality Gate, an internal tool that collects code statistics. It’s really helpful for that purpose as it can highlight code that is not covered by tests.
Also, TDD can help in using more interfaces in the code and this makes it easier to write shorter PRs because interface implementation can usually be done later. Small PRs also means a faster review, since the reviewer can focus (better) on a smaller subset of code.
If a developer is following TDD, you’ll see the code is often easier to read and understand, and this means the whole team (not only the person who wrote it) will be glad to be in charge of it. But if there’s a really long class or it has too many responsibilities, you’ll have serious problems when trying to understand and maintain the tests. This is a code smell showing you that someone is not using TDD at all or at least not in a proper way.
In summary
Everyone in the Edge team now knows how TDD can make a developer’s life easier. We had so many chats, meetings and presentations to align on this point. As a result, even the new joiners on the team know they have to apply TDD and we’re planning to improve our pair-programming skills to facilitate the mindset change that TDD requires.
So, when you need to learn a new programming language, my advice is to start by learning how the testing framework works. Tests should be the first code you should write because you can use it to make sure you’re learning the language in the right way. And that’s a great place to start.