How do you write bad code that’s hard to test? There are few anti-patterns for that you should writing. These are: untestable code avoid with a lot of conditional behavior which depends on another un-readable code. Code Code that gives you different results based on the order in which it is executed with respect to other code. Different codes responsible for setting the same ( ) variable global Code that depends on a long chain of separate evaluations and assignments. I once had the displeasure (or pleasure because it taught me how 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: bad Bad, Unwieldy Code Let’s first understand what this code is trying to do. The function takes an argument called and return , but with a few caveats: valIncrementer val val+1 If is a number between 0 and 10, then the function returns . val val+1 If is outside the range of 0 and 10, then the function will return 0 if is less than 0 or 10 if is greater than 10. val val val If the flag is set, then the the function just returns the without changing it. disabled val 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 function gives you for different inputs. At a minimum, we need to test when 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. valIncrementer val equals the following: res 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 gets incremented when you want it to be incremented? Well, is assigned to be in the beginning, but it’s not guaranteed to stay that way. You’d have to step through , , and to make sure doesn’t get reassigned to something else. val nextVal val+1 firstStageSetter1 firstStageSetter2 secondStageSetter nextVal Better, More Testable Code 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: When a function is pure, it does not use anything that’s not given as arguments to compute (no global state) nor does it modify anything it is given. The pure function always just give back a new copy (no mutation). A good way to is If the answer is no, then its probably not a pure function. Pure functions: think about pure functions If I give this input to a function, will I always get the same output no matter what? Before you start coding, think about how to break down what you have to do into a set of simple building blocks that each does one very simple thing. My better delegates various aspects of its responsibility to helper functions. It separated the code from the code, which can be separately unit tested. Decoupled Design: valIncrementer what I should do doing it 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. ’s only responsibility is to tie all these functions together with one if-statement. valIncrementer Why did I make 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. incrementedVal Why do people write unwieldy, untestable code? I see two reasons for this: Most people don’t plan to write bad code. It just happens as more functionality is added over time. It’s often not feasible to refactor your code after a change, especially when your code is an API and your code rewrite could break your users code. Technical Debt: Sometimes there are also management pressure and schedule/fiscal constraints to minimize change to other parts of code that “works” (the argument, among ). points out: Bad Culture: don’t fix what’s not broken many other common excuses This article 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 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. resources out there If you are just starting on a project, here are some guidelines: make sure to design your code well to account for future revisions. This means make code maintainability and testability a design goal and design your code with the expectation that your code will have to change a lot to add more capability in the future. Be careful with design patterns such as object oriented programming. While object oriented programming forces designers to go through an extensive planning phase, making baseline designs with less flaws, it has been lately for its “highly structured physical environments in which after-the-fact changes are prohibitively costly, if not impossible”. The problems with objected programming are summed up nicely by Joe Armstrong in by Peter Siebel: “The problem with object-oriented languages is they’ve got all this implicit environment that they carry around with them. You wanted a banana but what you got was a gorilla holding the banana and the entire jungle.” under a lot of fire Coders at Work Try to avoid complex dependency injections. Dependency injection is a the client of a service from the service’s implementation by allowing a client to configure its use of the service at compile time. In practice, complex dependency injection means many things need to be specified in the constructor before the service can be fully configured for a client. The more complex the dependency injection, the more tightly coupled the behavior is to the environment, making a standalone test of the service’s implementation more difficult. If your dependency injection gets too complex, you can always split up a big, complicated service into smaller, simpler services with their own dependency injections. technique for decoupling 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. Derick Bailey’s Blog Thanks for reading! 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: (Also in ) Mishko’s Guide: To Writing Testable Code PDF Version Unit Tests, How to Write Testable Code and Why it Matters A Response To Why Most Unit Testing is A Waste A Functional Alternative To Dependency Injection in C++ Object Oriented Programming and Dependency Injection Object Oriented Programming is an expensive disaster which must end Goodbye Object Oriented Programming