Testable code — best practices

Andreea Andro
2 min readMar 8, 2021

Many of the best practices for writing testable code also conform to general code best practices. Code that is easily testable often also tends to be highly maintainable and resilient against changing business requirements.

1. Separation of concerns

Ensure that there is a strong separation between the different parts of the application. Distinct areas of functionality like data retrieval, data processing, data display, and event handling should typically be separated into individual subsystems and further broken up into individual modules.

This makes the code easier to maintain. Also makes it possible for components to be easily replaced with mock objects during testing. This allows units of code to be tested in total isolation from the rest of the system, eliminating failures caused by unintentional side-effects or incorrect interactions between multiple parts of the system, and also makes writing tests easier by reducing the amount of setup that needs to occur in order to run application components successfully.

Separating concerns also allows tests to be smaller, which makes any test failures that occur faster and easier to investigate and correct than they would be in a less granular system.

2. Object-oriented code design

Code that runs procedurally in response to an external condition, like an anonymous function that executes when a browser becomes ready, is impossible to unit test because there are no individual units of functionality that can be called, only one large blob of code held in closure. It also means that there is no way for multiple tests to execute fully independently of each other, which often leads to difficult-to-debug test failures caused by side effects being carried forward from previous tests.

Using an object-oriented code design with constructor functions means that each test can instantiate a fresh copy of any object under test.

3. Loose coupling / dependency injection

Tight coupling between components, where component A specifically requests module B instead of exposing a mechanism where a module like B can be passed to component A, increases testing difficulty by requiring the explicit dependencies of the component to be redefined from within the module loader instead of simply passing an alternative to component A during testing. It also makes it difficult to modify the behaviour of components at runtime by making it impossible for alternative implementations of external dependencies to be provided to different instances of a component. The name of the mechanism for passing dependencies into an object, instead of having an object reaching outside of itself for its dependencies, is called dependency injection.

4. Elimination of globals

Use of global variables encourages state to be shared across different components and tests.

--

--