Getting frequent feedback using Test-Driven Development

Cassio Dias
Salesforce Engineering
12 min readAug 12, 2019

TL;DR

This post is about the eXtreme Programming practice called Test-Driven Development, aka TDD, and how it can help us to produce more readable, maintainable, and testable applications through small and frequent feedback loops.

Test-Driven Development

Test-Driven Development (TDD) — first referred to as Test-First — was re-discovered at the beginning of the ’90s by Kent Beck when he was creating a “simple small talk test” framework called SUnit.

Later, Kent Beck released the book eXtreme Programming and officially included TDD as one of its core practices. Around the same period, Kent and his friend Erich Gamma wrote the same core of SUnit but using Java as the language, creating JUnit and the foundation of the whole XUnit family (Php Unit, Ruby Unit, etc.).

TDD became a big and famous subject and, around 2000, Kent Beck released the well-known book: Test-Driven Development By Example [tdd kent].

Actually, if you look for “test-first” before the 1990s you will find articles like:

  • “Report of The Nato Software Engineering Conference” [Garmisch, 1968];
  • “The Humble Programmer” [Dijkstra, 1972];
  • Craig Larman and Victor Basili [larman basili] in the early 1960s working on the Project Mercury, an IBM Federal Division project for NASA.

The 60s project Mercury led by Jerry Weinberg was not exactly TDD; however, the main idea was looking for methods to avoid bugs in production, manually writing the expected result first and coding until they get the expected behavior using very short iterations that were time-boxed (half-day).

https://blog.hichroma.com/visual-test-driven-development-aec1c98bed87

So, that’s the reason Kent Beck said that he re-discovered Test-First development, which he renamed later as Test-Driven Development.

The original description of TDD was in an ancient book about programming. It said you take the input tape, manually type in the output tape you expect, then program until the actual output tape matches the expected output. After I’d written the first xUnit framework in Smalltalk I remembered reading this and tried it out. That was the origin of TDD for me. When describing TDD to older programmers, I often hear, “Of course. How else could you program?” Therefore I refer to my role as rediscovering TDD.

— Kent Beck

TDD, or not TDD: that’s the question

http://agility-html.sevenspark.com/portfolio-portfolio_troopers-6

Some of you might think that applying TDD wouldn’t be good because we want to get things done; we want to ship our product as soon as possible and we cannot spend time writing a test before the real implementation.

TDD or not TDD is a dilemma and there are long debates across the internet over whether it’s valuable or not. I’ve seen successful products with and without TDD. In fact, regardless of the style, we have to deliver flexible and reliable software consistently, adding value as a product to our business.

Is TDD always applicable? Does it work really?

Well, as a traditional engineering answer: it really depends! We have to find the balance.

There is no silver bullet

Before we go further with TDD, let’s take one step back and think about some fundamental problems.

Frederick Brooks wrote a winning paper called “No Silver Bullet — Essence and Accidents of Software Engineering” [brooks 1986] where he claimed that:

there is no single development, in either technology or management technique, which by itself promises even one order-of-magnitude improvement within a decade in productivity, in reliability, in simplicity”.

https://en.wikipedia.org/wiki/File:Mythical_man-month_(book_cover).jpg

Brooks also created the concept of Essential complication and Accidental complication [brooks 1986][brooks mm], where essential is caused by the problem itself and accidental relates to problems which engineers create because we make mistakes.

Software is complex

Software is complex and its progress is nonlinear; developers are smart people and are able to understand and solve problems very quickly. However, software development is a learning process.

For each assumption that we make on a daily basis, there are at least a couple of solutions, each one with its own trade-offs.

And the development begins…

Let’s imagine a very common case that we have been through a hundred times: the project starts, the development team begins coding with a focus on “getting things done.” There are no tests or tests are only implemented“when necessary” because we have to “cut the corners.” “Let’s solve this later” is a common refrain.

The project progress in very good shape at first and everybody is happy!

But suddenly, it starts slowing down…

Why?! The codebase is getting inflexible and tightly coupled, and the team is spending more time debugging and fixing potential bugs in production than producing value. The team also see an extra effort every time a refactoring is identified.

The team is afraid — they don’t have confidence in the code. They know it’s brittle and they’re not sure whether everything will still be working after any change.

The team just realized they are working on LEGACY CODE.

https://www.slideshare.net/KateSoglaeff/unit-tests-benefits

Welcome to the project, here is your codebase!

By the way, what is legacy code? Source code inherited from someone else? Hard to change, read, understand? Source code from older technology? Usually we think of legacy code as being something old, but in fact, new code can turn into legacy code pretty quickly, as soon as we lose control of our shiny new project.

Michael Feathers in “Working Effectively with Legacy Code” [feathers 2004] wrote an interesting definition about what is a legacy code:

“Legacy is code without test…”

Unfortunately, there’s no magic wand that we can wave to have a sustainable code base, where we can keep the code clean and be confident that the system will still be working after any change.

We have to work hard to build a discipline that gives us the CONFIDENCE to write, change, and improve our code while keeping it CLEAN and moving CONSISTENTLY.

And here is the point where TDD comes as a discipline: creating a whole new mindset and work as a designing tool.

What? TDD is a design tool?! Yes! TDD is not about tests, it’s about design!

TDD gives us the ability to think about the code before writing it. The action of writing tests first is more a process of thinking and planning than a verification. Defining the expectation first makes us write only the necessary code. Nothing else.

The 3 steps: red, green & refactor

TDD is a mental model (discipline) which relies on a very short feedback loop at the code level:

  • RED: You write the test (set the expectations) and see it failing; this is the step where we think about what we want to develop.

The purpose of this phase is to write the assumption that informs the future implementation of a feature. The test will only pass when the expectation is met.

First advice: the assumption must be small; remember, we want to avoid accidental complication!

Second advice: see it failing. This is an important result to avoid false positives.

This phase is where we implement the code to meet the assumption. The goal is to find a solution without worrying about optimization.

Here we have one of the key points: write the minimum amount of code. What TDD is saying to us? Small steps; avoid accidental complication.

  • REFACTOR: Now it’s time to clean up by refactoring the code!

Now that we have the green tests, we can think about how to implement our code better. This is refactoring — improving the internal structure of the code without changing its external behavior.

The philosophy is based on the idea that our mind is limited and we are not able to work at the same time on the two basic goals of all software systems:

  1. Correct behavior
  2. Correct structure

So, this cycle tells us to focus first on making the software work correctly, and then, and only then, to focus on a long-term flexible structure.

Make it work. Make it right. Make it fast

— Kent Beck.

TDD and my journey

I’ve been working with TDD since 2013 and, the last 3 years, as an XP (eXtreme Programming) advocate & consultant.

Let me share some of the key learnings for getting the most benefit out of TDD’s frequent feedback loop.

Simplicity

TDD gives me the ability to think more about simplicity, focusing on writing only the code necessary to pass the assumption. In that sense, we’re challenging the wrong assumptions as early as possible.

  • Is this what the customer wants?
  • If not, move on, don’t waste time.

With TDD I am able to apply practices like YAGNI (you ain’t gonna need it), DRY (don’t repeat yourself) and KISS (keep it simple & short) in every cycle of red, green, refactoring.

The TDD mindset helps me to avoid over-engineering, the designing of a product to be more robust or complicated than is necessary for its purpose. It avoids big design upfront, an approach in which the program’s design is to be completed and perfected before the implementation is started (AKA waterfall) and helps on the evolutionary design, which is the practice of growing a system in a natural way, by adding the minimum amount of code to satisfy the business needs, iteratively and incrementally.

Initially, like a waterfall developer, I was focused on creating the most flexible solution that — ironically — turned out to be a very inflexible and unused feature, sometimes leading to customer disappointment.

Always implement things when you actually need them, never when you just foresee that you need them.

— Ron Jeffries

Small Unit Focus

As described before, TDD helps me to avoid design upfront, resist over-engineering, and allow for evolution as the project grows. As a result, we produce simple, loosely coupled, and highly cohesive code.

Good developers always aim to produce code with a simple design, no matter the type of development methodology; however, TDD is a useful practice to encourage simplicity as an everyday practice.

Better requirements

TDD is beneficial to develop a deeper understanding of the functionality required prior to writing production code, simply by asking questions like “how do I test that?”; “is zero a valid input?”; “what happens if/when/then…”, etc.

When we write a test, we have to think about scenarios and boundaries a little bit more, and those questions give us a better requirement understanding. We are able to improve user stories, acceptance criteria, and definition of done.

Confidence to move faster

https://ast.wikipedia.org/wiki/Ficheru:Breaking_technique.jpg

Every step forward we make is a new challenge against our previous assumptions and design decisions. TDD gives us a frequent feedback process to move faster.

  • Correctness: Is it doing the right thing?
  • Regression: Has anything been broken? Is my code healthy enough to move?

Being able to clean the code in each cycle makes the codebase fearless and also increases developer satisfaction.

Refactoring is a joy!

Refactoring is an important part of TDD. As we produce smaller and more testable code with TDD, cleaning the code tends to be an enjoyable and interactive process. If something goes wrong, we know that any mistake will be spotted quickly; we have a safety net.

In my experience, applying refactoring without tests or using a complex test suite was frustrating and extremely expensive. Code that was not able to be refactored became a technical debt to eternity.

Productivity

Productivity in software development is something really hard to define [Fowler 2003]; however, TDD makes me more productive, not just because it reduces the amount of time I spend debugging, but because of the “end-to-end” development cycle where I will spend less time dealing with re-work, maintenance, and side effects and instead will be able to focus more on delivering business value.

TDD productivity has been studied by academics in depth:

  • IBM and Microsoft [ibm ms; Nagappan, 2008] concluded that the TDD experiment was very effective. Pre-release defect density of the four products analyzed decreased between 40% and 90% relative to similar projects that did not use the TDD practice;

Reduced defect density 40% — 90%;

  • TDD leads to the better requirement understanding and this consequently improves the correctness of the software; developers felt they anticipated the features better [CRISPIN]
  • Program reliability and understandability [Muller and Hagner] — the test becomes a source of documentation
  • TDD helps to write software in smaller, less complex, and well-tested units [Janzen and Saiedian]
  • More cohesive code with less coupling [Steinberg]

However, it’s always good to remember that productivity is a long-term investment as it needs additional development time and this cannot be avoided in case of the TDD approach. Results of the same study showed that the TDD approach uses approximately 15% — 35% more time for development [Nagappan, 2008]. So, before adopting TDD, make sure the whole team is on the same page.

TDD and legacy

One of the most valuable things about TDD is that it lets us concentrate on one thing at a time. We are either writing code or refactoring; we are never doing both at once. In fact, it’s not really TDD if you’re starting with legacy code — however — all your new code can be TDD. As you start a new feature, just use the discipline to write it.

If you can’t do that because the legacy code is too difficult to test, then start writing tests for them using a bigger scope (integration tests) and then slicing off with confidence that those bits are covered.

Assuming that you have existing code, then you need to create a priority list of components where testing them that makes the most sense. There are 3 factors to consider that can affect each component’s priority:

  • Logical complexity
  • Dependency level
  • Priority

Conclusion

TDD is not just simple validation of correctness; it’s about driving the design and keeping a consistent, interactive, incremental, and evolutionary coding process.

TDD will drive us to build all the pieces of our application with enough confidence that it will be robust and maintainable. It will also expose early problems (accidental complication) not just to the development team but also to the broader organization, giving the ability to painlessly adapt their software to address new business requirements or other unforeseen variables.

References

--

--