Why I Practice TDD: Speed and Need
Learn how TDD lets you work faster while eliminating critical defects.
Join the DZone community and get the full member experience.Join For Free
“We need you to go faster, so we need you to stop practicing test-driven development,” said the manager. “Just ship it, and we’ll worry about problems later.”
For developers who have ingrained TDD as just how they develop software, the manager’s proposition is laughable. It’s akin to telling a race car driver, “We need you to go faster, so we’re going to take out the steering wheel, and we’re going to turn the windshield into seamless, opaque body molding for aerodynamic reasons.”
The metaphor provides a stark warning: Blind and steering-less driving will propel you into a wall. Similarly, sure, there’s the word “driven” in test-driven development, but what does it mean to build code blindly?
In other words, what’s the real risk of not doing TDD—or of not considering it in the first place?
Defects? So What?
The majority of software systems ever produced were built without test-driven development. The majority of reasonably sized software systems also share a few characteristics:
The cost of changing them increases dramatically over time.
They contain a large number of defects.
Few developers will dispute these claims.
We’ve learned to accept defects as a matter of course. Many programmers insist that defect containment efforts represent rapidly diminishing returns on value. It’s impossible to eliminate all defects, so they say, and we make a calculated choice to release software with a number of known defects. We “manage” these defects, meaning that we use high-end tools to capture information about each defect, from discovery to resolution.
A typical software product experiences many severe, costly defects in its lifetime and contains hundreds, even thousands, of lesser defects. These lesser problems range from continual nuisances that users must work around to the headaches of software that halts progress or produces incorrect results. Sometimes we don’t discover the incorrect results until it’s too late. Major defects can result in the loss of major revenue (millions of dollars), customers or, in some cases, the company itself.
Most companies have no clue about the true cost of a defect. It’s likely they really don’t want to know. Few dare to quantify the costs.
On the surface, defects represent rework—programmers must recode portions of the system that were not coded properly to begin with. But that’s just the tip of the iceberg. Here are some other costs associated with defects:
The time spent finding the cause of the defect.
The opportunity cost—many shops report spending over 40 percent of their development effort fixing incorrect solutions.
The effort spent retesting and redeploying the system after the fix was made.
The cost of supporting the defect in production (support desk costs).
The human cost of things like late-night outages.
The cost to our customers in terms of wasted time and inability to get their work done.
The Cost of Indifference
Developers aren’t stupid; they know that defects suck up a lot of time. Defects also embarrass companies and potentially threaten careers. As a result, most developers manually test their software to a reasonable extent, and most teams have a testing team run their software through a rigorous end-to-end regression testing process before delivering it.
For those who understand how much defects cost and slow us down, TDD might be worth the debate. But does TDD win hands down? Aren’t the developers’ manual testing efforts sufficient?
To bolster the case that TDD is worth the investment, let’s consider another reason related to defects. The fear of defects instills an extremely risk-averse stance in many developers. They get their code to work, test it (sometimes writing unit tests), then move on to the next challenge. What they don’t do much of is clean the code up: “If it ain’t broke, don’t fix it.” Their fear means that the quality of the code in the system gets worse, by definition.
In programming, there are infinite ways to code any particular solution. As you might imagine, most of these ways are less than ideal. These are just some of the core dysfunctional approaches developers can employ:
Organizing code in a labyrinthine manner, making it extremely difficult to follow
Phrasing code in an inscrutable manner, making it extremely difficult to understand
Introducing considerably more code than needed to effectively solve the problem
The net result of all three is excessive amounts of time wasted. In order to make changes to code, we must understand the existing code well and analyze the potential impacts of the changes we’re about to make.
What’s fascinating is that it’s usually pretty easy for the original programmers to construct code to avoid these dysfunctions. As long as this cleanup occurs before the code gets disseminated, we have a fighting chance. Otherwise, we are on to the next person who must waste time deciphering the tortuous product of the brilliant minds that came before.
As easy as it is, the cleanup doesn’t happen. “It ain’t broke, don’t fix it.” In other words, don’t be the one who introduces the system-crashing defect. “You broke the system because you were cleaning things up?” But I contend that not cleaning up code increases maintenance costs by an order of magnitude or more.
Night and Day in Code
An order of magnitude may seem like an exaggeration, but it doesn’t take long to discover 10x-bad-code in a typical system. And we invariably find that it now takes more time to do anything in our system than during the first few months we joyously worked on it.
This code comes from a production game. It took me about an hour to pin down exactly what happens as a result of executing the getBestTrade method. It should have taken five minutes. Here’s my first pass at a refactored implementation of getBestTrade.
The cleaned-up solution still contains a good deal of code, and it could use another cleanup pass, but getBestTrade has been distilled to declare the core algorithm:
Iterate through a handful of potential scoring combinations
For each combination:
If it’s appropriate to score the current set of cards, do so
Replace any previously established score (and cards to trade) if the new score is greater
The individual scoring algorithms have been isolated, and each now comprises a few lines of straightforward, declarative code. Every other bit of logic in the code has been similarly isolated to a small, understandable and easily verifiable chunk. This also allows us to focus on desired behaviors and outcomes instead of wasting time deciphering intertwined and dense implementation specifics.
Why I Practice TDD
Initially, it might take most of us a little longer to get started with TDD. There’s a learning curve, we must get used to organizing our systems differently, and we must get some testing constructs in place.
But our speed increases the more we practice. Once we’ve written a couple of tests and cleaned them up, it’s often dramatically easier to write the next several tests. We discover many things that speed us up in little ways, such as the ability of our programming tools to write bits of code for us because we’ve written the tests first, or the way having tests shrinks the time needed to understand what the system is supposed to do.
Most importantly, however, we go faster because TDD enables us to continually change the code. Every few minutes we take advantage of the opportunity to clean up the code, simply because we can do so and not fear breaking things. And clean we must: It’s fairly easy to write convoluted or overly complex code in a few minutes’ time.
I practice TDD because it allows me to go faster and it catches my mistakes before they reach others. But more importantly, it allows me to craft code that doesn’t demand more time to change.
Published at DZone with permission of Jeff Langr, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.