How to Write Testable Code

In the article Introduction to Unit Test, we highlighted the vital importance of Unit Testing. But before applying unit tests effectively in current or future projects, we need a crucial foundation: learning how to write Testable Code (Production Code designed to allow straightforward unit testing). This is often the hardest hurdle, so this article synthesizes key principles to help you write testable code effortlessly.

Before reading this article, make sure to review these prerequisite guides:

Write a Simple Unit

Writing a simple, cohesive Unit[1] makes testing significantly easier due to several key factors:

  • Fewer Test Cases are needed per unit.
  • Fewer dependencies are involved, which simplifies test setup and Mocking[2].
  • Drastically reduces the risk of missing edge cases.

What about complex business requirements?

Decompose large requirements into smaller, focused sub-problems and solve each one individually—the classic “divide-and-conquer” strategy ;)).

Separation Between Logic and Presentation

Separating business logic from persistence, UI presentation, and network/API I/O is mandatory for long-term maintainability. This separation is the foundational philosophy behind architectures like MVC, MVVM, and Hexagonal Architecture. Even if your project does not mandate unit tests, maintaining this boundary is essential software engineering hygiene.

Furthermore, this separation directly enables Consistent Units in the next section.

Consistent Units

Consistency (determinism) means that given identical input, a Unit will always return the exact same output. This deterministic predictability is what makes unit test assertions possible, as introduced in Introduction to Unit Test.

Common sources of non-determinism include:

  • Random number generation (Random, UUIDs).
  • System clock / current time calculations (e.g., computing age from a date of birth relative to now()).
  • External system I/O (Databases, REST APIs, Caching layers).
  • Context-dependent state (thread context, security context, session scope).

Here are 2 practical strategies to handle non-deterministic logic:

  1. Explicit Parameter Passing: Separate pure business logic from non-deterministic calls. Pass the output of the non-deterministic call as an explicit input argument into the pure logic unit, testing only the deterministic business logic.
  2. Abstractions & Inversion of Control: Abstract non-deterministic operations behind interfaces using IoC/DI. The business logic acts as the Client and non-deterministic logic as the Service Implementation. You test the business logic while substituting the dependency with a Mock2.

Apply IoC/DI

Adopting IoC/DI allows you to effortlessly substitute dependencies with Mocks2 in classes under test, while simultaneously satisfying DIP.

Apply S.O.L.I.D Principles

Embracing S.O.L.I.D principles provides immense testability benefits:

  • Single responsibility principle: Keeps classes small and cohesive, reducing the matrix of test cases per unit.

  • Open/Closed principle: Eliminates the need to rewrite existing unit test suites when extending features with new implementations.

  • Liskov substitution principle: Suppose Class A depends on Class B, and both are thoroughly unit tested. If you introduce Class C extending Class B and replace B with C, satisfying LSP ensures Class A does not need to be rewritten or retested to accommodate C.

  • Interface segregation principle: Combined with SRP, ISP ensures implementing classes only deal with relevant methods, making mock setups lightweight.

  • Dependency inversion principle: High-level business logic depends only on abstractions, allowing complete isolation in test fixtures via IoC/DI.

Conclusion

Writing Testable Code demands intentional discipline, but mastering it elevates your craftsmanship to the highest professional standard. Refactoring tightly coupled code into testable architectures later is costly and fraught with risk, so striving for testability from day one is always the winning approach.

Thanks for reading!

Reference articles

  1. A Unit refers to a method in OOP, a function in functional programming, or a procedure in procedural programming. 

  2. Mocking is simulating the behavior of external collaborators during unit testing. See details in Use Mock to make Unit Test easy ↩︎ ↩︎

updatedupdated2026-09-052026-09-05
Load Comments?