The Power of TDD – it’s not just about testing!

May 24, 2024 | Technical Excellence, Testing & QA

To be agile we need to move and adapt fast. Software developers have come up with hands-on techniques to enable this speed. One of these techniques is test-driven development (aka TDD).

While TDD has “test” in the name, it has less to do with testing in the traditional sense than with software architecture, developer experience (DX) and customer-centricity. And that is the reason for many misunderstandings and frankly wrong applications of the technique in practice.

Correctly applied, TDD significantly increases development speed, product quality and customer value.

TDD is simple, but it definitely isn’t easy.
– Ron Jeffries

Traditional Testing

We all test

Most product development relies on some sort of testing. We want or need to build confidence that our product actually works.

It’s pretty common to have testers or a QA team kick the tires before we ship software to customers. This testing can be done manually, semi-automated or fully automated – but it usually happens AFTER we built the features.

Sometimes tests cases are created upfront (test-first). The QA team can then use these test cases to run through the scenarios the product management originally envisioned.

Sometimes the developer codes, runs it and checks whether it works. It’s a less formal testing, but it is still testing.

Sometimes we leave the testing to the customer, which is probably not the best way to do it in the long term.

I guess it’s safe to say: we all test the product in one way or another.

What is common to all these approaches is that they first build features and then test them. If we think ahead, we already define test cases before we build the feature, but we still only run the tests after we built the feature. That is the first difference between traditional testing and TDD.

Test early and often

We know we will always insert defects into our product. The more code we write, the more defects we insert overall. So the bigger the batch of features is we built, the more time we will spend testing (and fixing). Traditional approaches often reserve significant capacity for testing before releasing something. In theory it seems to make sense to bundle all testing (and fixing) in one final phase before releasing the next version. However, there are two additional effects that make this final testing phase a bad idea:

Defects don’t grow linearly The more code we write, the higher the defect rate per line of code. We can assume that 100 lines of code are more complex than 10 lines of code. And usually the number of defects does not only grow linearly with lines of code, but the rate at which we insert defects also increases due to complexity.

Cost for fixing a defect increases with time Additionally it’s much harder to find a defect in 100 lines than to find it in 10 lines. This is why we often struggle during these late test phases right before release. There is just so much code which could cause the bug, that we spend a lot of time searching all of it. Let alone that we often have forgotten the details of what we knew when were writing that code weeks or months ago.

All this suggests that testing earlier and more often would be preferable to one big test phase towards the end. But for that to happen, testing must not be expensive or time-consuming.

Test-Driven Development (TDD)

And TDD does exactly that:

  • we ensure testing is quick and painless, so we can do it often
  • we write a minimal amount of code and test it right away

We’ll talk about how to do it in a second. Just note how this approach radically shortens the time we need to find and fix defects! If we know we broke it in the last 10 lines of code, the problem is usually easy to spot and fix. It’s also much more stress-free (talking DX) than trying to find the problem in 1000 lines of code.

How TDD works

The TDD approach is fairly simple. It consists of 3 steps:

  1. Write a small failing test
  2. Write code to make the test pass
  3. Refactor your code design

Then you repeat this cycle. It only takes a few minutes to go through this cycle once, so you run through it many times a day.

I often refer to this cycle as the red-green-refactor cycle:

  1. You write a failing test, which means the test suite is red.
  2. You write code to make the tests green.
  3. You refactor your code to optimize design and architecture.

I will provide a hands-on example of this in action for Agile Navigators (paying subscribers) next week.

Red-green-refactor cycle in test-driven development (TDD)
The red-green-refactor cycle of TDD

It’s actually very simple – and that’s the beauty and power of TDD.

We always start from a green state, meaning all previous tests pass and we are sure the code is in a stable state. Then we engage with the outcome the customer needs and write a little test scenario for the first part of that. The test is red (it fails) – so we know the test actually works. Then we write code until the test suite is green again. We’ll get to all the benefits this gives us soon. Once we’re green, we have a stable state again. On that state we can clean up our code and improve its design, while keeping the test suite green. When that’s done, we add the next test scenario to drive out additional functionality.

The key of TDD lies in taking small steps. You will see how small in the article for Agile Navigators next week. If we make our steps too big, we will quickly notice this and can step back though. Developers get a good feel for how much they can bite off without choking. The initial advice is: take baby steps.

Not just about QA

I already said that TDD is not primarily about testing or QA – despite the name.

TDD is more an architecture and design approach and it centers around the customer needs. What do I mean by that?

Because we start with writing the test instead of writing the implementation, we have to design the code interface automatically. We think about which methods we want to call and what we expect back. We treat the (not yet existing) implementation as a black box. We’re operating on the problem side of what a customer needs instead of on the solution side. This has several benefits:

  • We design better interfaces in code
  • We look at the feature from the perspective of the customer
  • We don’t get carried away by implementation details

It turns out that code written in TDD fashion tends to be more understandable, maintainable and well organized.

Advantages of TDD

Better code design is just one of many advantages of TDD. I attempt to list a few benefits of test-driven development here:

  • Clean code. By explicitly designing interfaces we think more about good design. And by constantly refactoring we never end up with unreadable or unmaintainable code.
  • Understanding requirements & customer focus. Because we start with customer requirements, we ask questions and understand customer needs before we implement something. Developers have a much better grasp of why they’re building this feature.
  • No gold-plating. We stop once we get to green. This prevents us from “adding a little more functionality for the future” which is never needed and would just be a waste of time.
  • Free up-to-date documentation. Automated tests document the expected behavior of the system. They also document how to use the system through its interfaces. New hires get this documentation for free – including code samples to copy/paste from! And as a bonus, this documentation never goes out of date with the actual software system.
  • Clear thinking. Focusing on just one tiny aspect of functionality frees up brain capacity to really think about the business problem and a smart solution. And during refactoring we can fearlessly optimize the code without worrying to break things. A developer does not have to keep all other potentially affected parts of a change in their mind! They simply try it and see if it’s still green. You will never have to hear the words “never touch a running system” or “better don’t touch that piece of code” again.
  • Sustainable pace. TDD actually lets the developer go home on time in the evening. Instead of “just finishing” that thing they started, because now they have all the open strings in their head, they can rely on the next tiny failing test to tell them where to pick up again the next morning. Trust me, that’s an underrated benefit for DX, developer sanity and long-term performance!
  • Adaptable architecture. Since we refactor after each tiny step, we constantly evolve the architecture to be the best architecture for the current set of features. No rigid architectures anymore against which developers have to fight or that are subpar for the current feature set.
  • Automated regression tests. Oh yes, as a side-effect, we also get an automated test suite 😁

I’m sure I missed a few benefits. But this list alone should persuade anybody that TDD is worth it.

The cost of TDD

When I talk about TDD, people often argue that it would slow things down and require a lot of extra work. Well, reality has shown the opposite to be true.

But the initial ramp-up of the testing stack as well as learning the skills and accepting the change in thinking for developers can incur significant costs.

Also the maintenance of test infrastructure and a test suite can be significant.

In my experience, it has always been worth it. By a huge factor! The biggest hurdle is often whether business and developers believe in the approach and push through to make it happen.

How to get started

I will go into more detail on how to get started in the article for Agile Navigators (paid subscribers) next week. But let me say that you want to start sooner rather than later – it makes things so much easier.

Adding the infrastructure for automated testing to an existing legacy code base can be exhausting. And if management and/or developers are not yet persuaded, they’re likely to give up and be disappointed.

So whenever you can, start with TDD from day one!

Conclusion

Test-driven development is a proven design and quality technique. Despite its name, it’s actually more about good architecture and customer focus than about testing.

While TDD might seem slower and a lot of effort at first glance, it actually speeds up development significantly. Almost as a side effect it reduces defects and creates high quality code.

If you want to get started with TDD, it’s best to start today. The later in a product’s lifecycle you add TDD, the more expensive and tedious the ramp-up will be. The hardest part of starting TDD is often the change in mindset of both managers and developers.

And just because TDD comes from the software world, it can (and is) also applied in hardware. For example, a company like Tesla invests heavily in test automation of their software as well as their hardware. This high level of TDD is a large part of what enables Tesla to innovate so quickly.

If you have questions or want help in defining your testing strategy or move to TDD, just ping me. I offer workshops with your teams to help you hit the ground running!

avatar

Thank you for reading The Agile Compass. I’m Matthias, here to help you help those around you become agile.

To get more, consider upgrading to a paid subscription. You’ll join our Discord community and be able to listen to audio versions of my articles.


Liked this? Get more for FREE

Subscribe to The Agile Compass and become an even better agile practitioner.
You'll receive valuable in-depth articles on agile topics via email.
Join me in making this world a more agile place 💛

    We respect your privacy. Unsubscribe at any time.