I. Introduction
Why in this time and age do we still need to discuss Test Driven Development (TDD)? As one of my connections, Vincenzo Ciaccio, put it “for at least a couple of decades we have known that is the right thing to do, and the ones who disagree had to adapt", right? Everyone knows they have to employ it. Yet, it ‘s pretty common knowledge in the tech industry that many companies and developers don’t. In fact, a number of companies and developers don’t write tests at all.
This is a way to remind ourselves why TDD is a good thing, and possibly to brainstorm a way of thinking and working that makes it easy, if not natural, to use it.
So… to cover the basics, Test-Driven Development is a software development methodology that requires writing a unit test case before the code it is testing, and then writing the code to make it pass.
And why is this a good thing? It would be much easier to list reasons why it wouldn’t be, and just say thank you for coming, since there isn’t any reason why it wouldn’t be, but as I stated before, we need to remind ourselves why it is a good thing.
First and foremost, to make sure that once we’re done we do have tests, since too many times leaving the tests for later means leaving them for never. That alone brings some secondary benefits such as more confidence in the codebase, more confidence in future refactoring, ease of debugging (going by exclusion), maintainability, easier onboarding, and so on.
The tests themselves will be of better quality, because, as the process dictates, we start from a failing test and write the code to make the test succeed, thus ensuring that the test indeed tests the code we are working on.
The process itself helps to clarify requirements, design choices, ensures code testability (by design), and helps to simplify the code overall, since we are writing code from simple tests, rather than tests from, quite often, unnecessarily complicated code.
II. The TDD Cycle
The TDD cycle is traditionally described with the mnemonic red, green, refactor.
Red means that we first write a test case that fails, of course if there is no code for our intended purpose, a good test can’t possibly pass.
Green means that we write the minimum amount of code needed for the aforementioned test to run green.
Refactor it’s self explanatory, the code can be refactored making sure the test continues to pass.
Let’s analyse in more detail this TDD process, and describe how it would look like in practice.
- Make a list of functionalities, functions, or methods (basically any testable unit of code) that might be needed. It doesn’t need to be exhaustive at first, and it’s fine to prune it if something turns out to be unnecessary.
This step aids with requirements, by encouraging defining the what and the how in a detailed and practical manner. Before I embraced TDD, I always had an idea of how I was going to write a piece of code until I started writing it, then I kept changing my mind till I had rewritten it three or four times. Following TDD principles, working on a list of methods and test cases for each of those methods helped me have a more realistic view of what a piece of software was supposed to do, and how. Now I only rewrite my code once or twice… - For each of those functionalities write a test case. Needless to say it should fail if it’s correct, and that’s what this step is for, ensuring tests’ correctness.
When writing a test for a new code it forces one to think about the simplest way of using the functionality, the minimum requirement for it to work, and how it should fail. Naturally the tests will be the simplest they could be, and highlight the minimum essential requirements for that bit of code. When writing, or modifying a test for a bug fix, this becomes the chance to write or improve the test that failed to point out the bug. - After each test case write enough code to make it pass.
This is ‘the actual job’ step, the fun part, and being driven by the test makes it easier and simpler. During this step it might turn out that more test cases are needed and can be added to the list in point 1. - Refactor test case and code, if needed.
Here is where the code gets production ready, the structure of the code might change, and might result in more test cases that can be added to the list in point 1, or more details added to the test case.
III. Common Challenges and Misconceptions
This post wouldn’t be complete without the now expected section on misconceptions. There has been quite some scepticism and a lot of confusion about TDD, the following are some of the myths that have made its adoption difficult:
- TDD is time consuming
- We can’t write tests until we know the design / TDD neglects software design
- All tests must be written before the code
- TDD is incompatible with iterative development
Let’s see what’s wrong with those statements about TDD.
TDD is time consuming
For the first point, it is usually said that the initial cost in terms of time is an investment, as it will save more time in the long run. I would like to add my view on this. Tests have to be written, what difference does it make when they are written? In my personal experience most if not all the people, developers and managers alike, that complained about it being time consuming, were always the ones that never write tests, not even after the code. I mentioned managers, because they are the ones that say “can we deliver it quicker if we don’t spend time testing?” or “can we push it over the line and we can focus on the tests later?”. Later… Latever… Never…
We can’t write tests until we know the design / TDD neglects software design
Design. This is a concept that would require a post of its own, since I have seen quite few developers rushing to write code with minimal or no thought beforehand, and only thinking about the design while writing the code. Which probably is the origin of the myths about TDD and design. I was always taught that a good software engineer should design its software before writing it, and this clearly separates the steps of design and coding. Furthermore, designing a piece of software, whether a full project or a single functionality, shouldn’t really involve implementation details. Once we have class’ properties and methods signatures, we have all we need to either write the code for those methods, or tests for them. So what prevents us from writing tests, once we have the design?
All tests must be written before the code
I have to admit there was a time where I thought this was the case, but even that wasn’t so much more of a pain, surely not a reason not to do it. None of this matters, though, because as Kent Beck himself wrote in his recent article “Canon TDD”, anything that is different from the TDD isn’t a valid point to dismiss TDD. So this point is bogus. TDD only requires us to write a single test case before we write the code for it. What quite often I end up doing for complex methods is:
- Decide which logic will be simpler and/or quicker to write between the successful run of the method and the failing one
- Write the test case for it (either the success or failure)
- Write the code for it
- Write the test case for the remaining outcome
- Write the code for it
Of course if the whole situation is a lot more complicated than that, i.e. if I need different test cases for different ways of failing, I adapt.
Sometimes, for simpler methods, I feel like writing all the test cases for the single method, and then focus on writing the code for the full method. Strictly speaking this can’t be considered TDD, but sometimes is quicker, and the code is written after the tests anyway.
TDD is incompatible with iterative development
The last point is about TDD being incompatible with iterative development. The reasons why someone would think that elude me. At this point it should be clear that this process is suitable for both writing new code, or modifying existing one, so if we add or change the requirements, technical or otherwise, we can still apply it.
IV. Possible Strategies to Facilitate Adoption
Let’s brainstorm some practical strategies to facilitate adoption of Test Driven Development, some that require a stimulus from management, and some that call for more personal developer initiative.
The first, and sensible way of achieving the objective would be a gradual approach, starting with smaller projects, or single features. This blends well with the next point, refactoring legacy code. Writing, or improving tests for legacy code, before refactoring is a good way to get going, of course given testability of code isn’t the main refactoring purpose.
Leadership input could make a real difference. A good expedient to get it done willingly or less so is for the tech lead to create tickets for tests to go with features and bugs ones, and maybe assign the latter ones only on completion of the former ones.
Customising tests to the needs of the specific project or team could help with embracing TDD. Of course there are teams that already have tests focused on the context, e.g. contract testing for microservices, or data validation for data-intensive applications. But if your team doesn’t, it could possibly make the process more relevant and easier to adopt.
Could there be anything else that could make Test Driven Development more widespread, and hopefully less of a perceived burden?
V. Conclusions
Test Driven Development is a powerful approach that remains relevant in the ever evolving field of software development. It isn’t as ubiquitously adopted as it should be, often not even by the ones who understand its benefits. It might require a bit of extra effort in terms of time and resources, at least at the beginning, but it pays off in the long run, and gets a lot easier with practice. All that is needed is to start practising it.
Everytime a new feature or even a small change is needed, everytime we need to swat a bug, we can start from the test. Do we have a test? Does it pass? Why is it failing or not failing? Do we need a change in the code? Let’s make the relevant test fail, and then carry on with the process. There are several ways in which we can start embracing TDD, and they don’t need to be done all at once, every little bit is better than nothing.
-
Write Better Code with SOLID Principles (PHP Examples)
By Dan Draper Published
-
Improving our internal tools - Part I: Design process for Flexi UI
Change is the trigger & tool to put us ahead of the present and open a better, more productive, and more efficient future.
By Karen Alonso Published
-
Upgrading to MacOS Monterey 12.6.5
Resolving "Invalid Active Developer Path" Issue After MacOS Monterey 12.6.5 Update
By Nirvaan Published