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:
Let’s first understand what this code is trying to do.
valIncrementer function takes an argument called
val and return
val+1, but with a few caveats:
valis a number between 0 and 10, then the function returns
valis outside the range of 0 and 10, then the function will return 0 if
valis less than 0 or 10 if
valis greater than 10.
disabledflag is set, then the the function just returns the
valwithout 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
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:
valIncrementerdelegates 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:
Create your free account to unlock your custom reading experience.