How do you write bad code that’s hard to test?
There are few anti-patterns for untestable code that you should avoid writing. These are:
I once had the displeasure (or pleasure because it taught me how bad bad code can be) of trying to prove the correctness of code that incorporated all those anti-patterns. It was written in C++ for a particular embedded systems application that I’m not going to get into. But I recreated the gist of what the code does in JavaScript as shown below:
Let’s first understand what this code is trying to do.
The valIncrementer
function takes an argument called val
and return val+1
, but with a few caveats:
val
is a number between 0 and 10, then the function returns val+1
.val
is outside the range of 0 and 10, then the function will return 0 if val
is less than 0 or 10 if val
is greater than 10.disabled
flag is set, then the the function just returns the val
without changing it.Not only is the above code difficult to read, it is also difficult to test. To prove the correctness of the code, you have to run a set of tests to see what the valIncrementer
function gives you for different inputs. At a minimum, we need to test when val
is a number between the valid range (i.e., 0 and 10), and the edge cases when it’s less than 0, when it’s equal to 0, when it’s equal to 10, and when it’s greater than 10.
res
equals the following:
The bad, unwieldy code gives us the right answer, but it’s not possible to unit test the different portions of the code that handles one aspect of the overall logic. For instance, how would you test whether val
gets incremented when you want it to be incremented? Well, nextVal
is assigned to be val+1
in the beginning, but it’s not guaranteed to stay that way. You’d have to step through firstStageSetter1
, firstStageSetter2
, and secondStageSetter
to make sure nextVal
doesn’t get reassigned to something else.
This is how I would rewrite the bad, unwieldy code to improve maintainability and testability of the code.
Notice how much shorter my rewritten code is compared to the bad, unwieldy code. There are two guiding principles for my refactoring:
valIncrementer
delegates various aspects of its responsibility to helper functions. It separated the what I should do code from the doing it code, which can be separately unit tested.Each helper function is responsible for a simple task and can be very short. It only cares about its own arguments and is insulated from the effect of whatever is happening outside of it. valIncrementer
’s only responsibility is to tie all these functions together with one if-statement.
Why did I make incrementedVal
a separate function? I don’t have to for a simple function like that. However, suppose this is not as simple as just incrementing a number. Suppose this operation requires accessing the database and can have latency and other side effects. We want to segregate code that’s not deterministic from code that is deterministic.
I see two reasons for this:
As it turned out, more flexibility led to devs writing code that others actually struggled to understand. It would be tough to decide if one should feel ashamed for not being smart enough to grasp the logic, or annoyed at the unnecessary complexity. On the flip side, on a few occasions one would feel “special” for understanding and applying concepts that would be hard for others. Having this smartness disparity between devs is really bad for team dynamics, and complexity leads invariably to this.
If you are already in that situation, then recognize that you are adding to the technical debt when you continue to neglect code refactoring and code rewrite and there’s a point in which the cost/time of making changes to it and making sure it still works outweighs the benefit of not refactoring. There are resources out there that helps you recognize symptoms that your code is untestable and suggestions for how to refactor. It’s likely many others are also struggling with the same problem you have so there’s opportunity to be creative and collaborate with other developers in the same ecosystem on an open source solution.
If you are just starting on a project, here are some guidelines:
The problem with complicated dependency injection is that it could contribute to the “It works on my machine phenomenon” as illustrated by this cute cartoon below.
If you found this story interesting, feel free to clap 👏
Here’re some follow up reading for those interested in learning more on this topic: