Return on Investment for Test-Driven Development
Posted on Fri 28 April 2023 in Methodology
I recently came across a thread discussing Test-Driven Development (TDD) on one of the major social networks. The discussion revolved around when to use TDD and when it can be skipped. As a consistent TDD practitioner, I find these discussions interesting, especially when it comes to understanding the perspectives of those who argue against TDD.
One particular response in the thread caught my attention:
TDD should not be used for simple functions because the return on investment is too low.
I guess the reasoning behind this is that simple functions can be verified through manual inspection, and their simplicity makes them easy to change. While this argument makes some sense, it’s not uncommon for functions that begin as simple to grow complex over time, thereby defeating manual verification.
Recently, I reviewed a bug fix for a seemingly simple function that clearly lacked a test-driven approach, as it had no automated tests at all. Here’s the original (but anonymized) Kotlin code:
data class Colors(var red: Boolean? = false, var green: Boolean? = false)
fun describeColors(colors: Colors): String {
return listOfNotNull(
colors.red?.takeIf { it }.let { "Red" },
colors.green?.takeIf { it }.let { "Green" }
).joinToString(" & ")
}
This code defines a Colors class with flags for red and green. There’s a function that describes a Colors instance by listing the active colors, separated by “&“.
At first glance, the function seems straightforward. You could argue that the implementation is more complex than necessary, but its functionality is relatively limited.
However, the function has a critical flaw—it always returns “Red & Green”, regardless of the contents of the Colors instance.
Before examining the fix, let’s consider the following:
- The original author seemingly deemed the function simple enough to skip TDD, saving some development time. (They apparently also skipped manual testing, but that’s a different topic.)
- The bug persisted in production for an extended period of time, causing confusion and frustration for users.
- Eventually, a user submitted a support case to address the issue.
- A JIRA ticket was created and prioritized.
- A developer diagnosed the problem, wrote unit tests, fixed the bug, and submitted a PR.
- Upon reviewing the fix, I found myself thinking, “If only the original author had used TDD…”
I’m not trying to be snarky, but this example highlights why the initial claim about TDD’s low return on investment for simple functions is flawed. Even though the bug was easy to fix, it demanded time and attention from various parties, including users. This could have been easily avoided.
Had the original author employed TDD, the bug would never have been committed to version control. The refactor step would likely have led to a simpler implementation. Most importantly, users would have received accurate information, and everyone involved in fixing the bug could have focused on other tasks.
Now that’s a high return on investment for TDD!
Epilogue
Here is a fixed version of the function:
fun describeColors(colors: Colors): String {
return listOfNotNull(
colors.red?.takeIf { it }?.let { "Red" },
// added: ^
colors.green?.takeIf { it }?.let { "Green" }
// added: ^
).joinToString(" & ")
}
The problem with the original function is that, while takeIf correctly evaluates to true or
null depending on the input, the let block is called in any case and doesn’t care about the
argument value. By using the safe call operator ?., the let block is only called if the
input value is true.
As noted earlier, the implementation is unnecessarily complex, and there are multiple different ways to make it simpler, but that’s left as an exercise to the reader!