Clean Tests: Common Concerns, Part 2: “I’ll Have Twice As Much Code to Maintain”

The unit tests you produce in TDD represent an investment. You’ll want to ensure they continue to return on value and not turn into a liability.

Breaking Dawn - Loch Rusky
“Breaking Dawn – Loch Rusky,” courtesy John McSporran.License.

The concerns:

  • The tests will have their own defects.
  • I’ll have twice as much code to maintain.
  • The tests are handcuffs.
  • Testing everything (via TDD) results in diminishing returns.

I’ll have twice as much code to maintain

It’s true, do TDD and you’ll likely have as much test code as production code, if not more. However, most sizable non-test-driven systems contain over twice the code needed. In other words: if C represents the ideal amount of production code for a system’s implementation, and TC represents test code, then 2C ≈ C + TC. That’s a wash, to me. I’ll take the version that comes with the tests, thank you.

2C? Twice the code needed? How’s that? The short answer: In non-test-driven systems, there’s no high-confidence way to keep code cruft from building up.

The TDD Bet

I worked with a customer in the Great Lakes area some years ago. They’d deployed a production application prior to my arrival; I was told its initial non-test-driven release was around 180,000 lines of Java code. The team, dedicated to TDD, had slowly but surely added unit tests as they test-drove new features, meanwhile cleaning up what they could in the legacy code. By my arrival, they’d shrunk the codebase to around 70,000 lines; by the time I’d left, they’d shrunk it perhaps another 10,000 lines.

Granted, much of the bloat was a large swath of dead and generated code they’d spotted. But a significant amount was purely unnecessary code.

I’d take the TDD Bet on almost any sizable legacy codebase: Given enough time to add tests and clean things up, it could be reduced to under half its size. A codebase with 100,000 lines of bloated code can transform into a codebase with 50,000 lines of production code and 50,000 more of tests.

Where Does This Bloat Come From?

I’m guilty of producing bloated code on my first pass of implementing some small solution. It’s much like writing (for example, these blog entries): I’ll slam out a few sentences in an attempt to spew what’s in my head onto the computer screen. Then I’ll read what I wrote, trying to put myself into the shoes of another reader: Does it make sense? Is it as punchy as it could be? I usually find numerous opportunities for cleanup!

There’s nothing wrong with slapping out a sloppy paragraph as long as you clean it up later. The same approach works for coding, with the caveat that you don’t want to wait too long–“later” might never come because something new demands your attention. When doing TDD, we ensure we clean up the code every few minutes, using the confidence that a passing test suite gives us.

Without TDD, we don’t do nearly enough of these little bits of clean-up. It’s far too unsafe: The chance of breaking other stuff is way too high. By definition, our non-test-driven code stinks a little bit more every few minutes.

Duplicate code rears its ugly head all the time. Duplication can happen in many forms–small bits of logic appearing dozens of times throughout the codebase, different algorithms to accomplish the same purpose, rote mechanisms where more abstract concepts would suffice.

I watched a developer copy-and-paste an entire 100+ line function in order to introduce a small, 5-line-long code variance. The right thing, of course, would be to use delegation, polymorphism, whatever, as long as it didn’t mean duplicating logic. And the developer knew that, as we might hope. “But I don’t want to change that function. I don’t want to be the one who crashes production because I changed some working code in order to make the system ‘cleaner.'”

Rather than introduce design concepts that minimize redundancy when they must deal with behavior variances, developers make what they think is the least impactful change out of fear. For these programmers, the notion of cleaner provides no immediate, obvious value. We know clean will make their job easier over time, but fear now intimidates more.

So, yes, with TDD, you’ll have as much test code as production code to maintain, but overall the amount of code will not be significantly different.

More importantly, you won’t fear your own code.


Clean Tests: Common Concerns, Part 1

The unit tests you produce in TDD represent an investment. You’ll want to ensure they continue to return on value and not turn into a liability.

Magnetospheric Multiscale
“Magnetospheric Multiscale,” courtesy NASA Goddard Space Flight Center.License.

As a first step, find the right frame of mind. Start by thinking about your unit tests as part of your production system, not as an adjunct effort or afterthought. We consider the diagnostic devices built into complex machines (e.g. automobiles) as part of the product. Software is a complex machine. Your tests provide diagnostics on the health of your software product.

Here are some common concerns about TDD:

  • The tests will have their own defects.
  • I’ll have twice as much code to maintain.
  • The tests are handcuffs.
  • Testing everything (via TDD) results in diminishing returns.

I’ll tackle each one of these in a separate post, offering some thoughts about how to reduce your anxiety level. (No doubt I’ll add other concerns along the way.) Then I’ll move on to how to craft tests that are more sustainable and anxiety-free.

The tests will have their own defects.

It is absolutely possible for a unit test to be defective. Sometimes it’s not verifying what you think it is, sometimes you’re not executing the code you think you are, sometimes there’s not even an assert, and sometimes things aren’t in the expected state to start with.

Strictly adhering to the TDD cycle (red-green-refactor) almost always eliminates this as a real-world problem. You must always see the completed test fail at least once, and for the right reason (pay attention to the failure messages!), before making it pass. And writing just enough code to make it pass should help reinforce that it was *that code* that passed the test.

Other ways to ensure your tests start and stay honest:

  • Pair or mob (or review the tests carefully with some other form of review).
  • Minimize use of complex test doubles, such as fakes.
  • Avoid partial mocks at all costs.
  • Don’t mock past immediate collaborators.
  • Employ contract tests.
  • Fix the design issues that demand complex test setup.
  • Stick to single-behavior tests.
  • Design tests for clarity. Emphasize test abstraction and employ AAA.
  • Always read the tests carefully as your point of entry to understanding.
  • Improve each test, as warranted, each time you touch it.


Next: The tests are handcuffs.