At John Hancock, we use Test-Driven Design (TDD) for all our new initiatives. TDD has three simple steps:
- Before you write functional code, write a test that proves if the implementation works or fails.
- Write the code and watch the test pass.
- Check if the added functionality changes your holistic approach and if you want to consolidate/refactor some of your code. If yes, refactor these sections.
That sounds simple enough, so why write an article about a three-step process? Why do we, at John Hancock Digital Enablement, get so excited about TDD? We have seen major impacts to our developers, products and culture:
- Increased motivation and self confidence.
- Shorter Release-Cycles.
- Increased Code Quality.
- Massive Reduction of Bugs in Production.
The Foggy Path
Imagine your development process like a foggy path, where you can see directly in front of you, but anything further away becomes foggy and imprecise. Imagine it like a journey into the unknown.
Obstacles may appear suddenly, forcing you to rethink your approach (refactoring), or you may get above the fog after a long journey and realize that there is a much simpler way to reach your goals (also refactoring).
This uncertainty and constant change may appear to be frightening and trigger anxiety, but TDD is the safety net, because you know that what you’ve built holds up against all the risks that have been identified. As a developer, I can take much higher risks and try different approaches and get quick validation.
The test cases built out as part of the TDD flow are ideally integrated into a continuous integration/continuous development (CI/CD) Platform, giving fast and reliable feedback to the engineers about unexpected behaviors their code change may have caused. Because the feedback to the engineer is near instant, the developer is familiar with the change that caused the error and is able to resolve it rapidly.
TDD Is the Opposite of “Never Change a Running System”
A well-known issue is that the later a bug is found, the more costly it is to resolve. TDD enables fast feedback loops so bugs can be resolved quickly and cost efficiently.
As an application grows, TDD gives you a solid foundation of everything that has already been built. You can be confident that what you have built is solid and that new functionality can be added to it easily and without taking the risk of breaking everything. In other words, TDD is the opposite of “never change a running system” and enables us to build even better software.
TDD Always Provides Up-to-Date Code Documentation
TDD provides excellent documentation toward the requirements and behaviors of the application. While documentation always lags behind the functional code and typically is written after a functionality is implemented, in the world of TDD everything starts with a test. The test documentation is always up to date and gives developers an easy and understandable way to figure out what the application is achieving without dissecting the entire code.
The following quote from Terry Pratchett explains it best: “If you do not know where you come from, then you don’t know where you are, and if you don’t know where you are, then you don’t know where you’re going. And if you don’t know where you’re going, you’re probably going wrong.”
As an application grows, evolves and is built up over time, and as it’s passed down and handed over, TDD gives it documentation and describes its solid foundation. The act of writing a test case forces a developer to stop and think about the functionality that is to be implemented and describe its desired outcomes.
TDD Provides Early Feedback
Through TDD we receive early feedback about the path we are about to set foot on. Early indicators can help us correct our assumptions or help rephrase or rewrite a requirement so that we deliver customer value.
Early indicators that we’ve encountered:
- Functionality cannot be measured: Why are we implementing this if we cannot measure its outcome?
- Functionality does not add any value.
- Functionality cannot be tested: Similar to the first bullet, if a functionality cannot be tested, how can we accept it’s delivery?
- Functionality can be easily embedded into an existing part of the application that needs refactoring: This can trigger the refactoring before we start the implementation.
As described, it is very helpful to think about the steps ahead before jumping in, which can be difficult for engineers to do. TDD does so in a language that is natural to engineers.
TDD Embraces an Ever-Changing World and Removes Fear of Change
TDD as a part of eXtreme Programming (XP) accepts and embraces an ever-changing world we cannot control. Changes may cause discomfort and anxiety as planning becomes difficult/different.
In an unstable, disorganized software project, changes are thrown at it, a solid foundation is missing, testing is skipped or postponed to the end and refactoring is skipped all in the name of on-time delivery. The first delivery of the application may be on time, but as time goes on the anxiety grows, there are random bugs everywhere, the more bugs that are fixed, the more issues pop up and you essentially end up playing a game of whack-a-mole in which you will never be in control.
In a project that embraces XP principles, such as TDD, we try to remain in control from the beginning, facing challenges early so they do not bite us later on. A solid test base gives developers, and everybody else on the team, confidence that the delivered solution will work, but most importantly it removes the fear of change.
Without fear of change, we can try all sorts of crazy things, test out ideas and see them turn out or fail without the code failing, without losing a customer, without getting that phone call in the call center.
Traditional QA and Release Cycles Are too Slow for this New Era of Ever-Changing Needs
As software grows, its testing becomes more intense. So, with each new piece of functionality there must be a corresponding test case. The manual execution of those tests becomes lengthy, the feedback loop to the developers slower, releases get held up, customers do not get new functionality in time and, in the end, money is lost.
Traditional QA and release cycles are too slow for this new era of ever-changing needs. What if I want to release daily or weekly? Do we have to execute the test procedures every time? What if bugs are found and repaired? Do we need to rerun the entire set?
The answer is yes—but if a machine does it in a few minutes, who cares?
While I am not a proponent of no human testing, what I am saying is that 95% of manual QA can be automated and executed by a machine, and TDD provides a fantastic test set to get started with. Humans may catch errors that are hard for a machine to identify or imagine since human behavior is hard to predict, so spot testing as well as exploratory and behavioral testing are still needed.
We Are Left with a Team that Is Hungry for More and Only Afraid of the Enormity of the Possible
What are we left with at the end of a TDD project? We have a well-documented, easy to understand, easy to change and stable code base lead by a team that is not afraid of change. We are left with a team that is hungry for more and only afraid of the enormity of the possible.