Introduction
This article assumes unit tests. The ideas are guidelines, not commandments; use them where they make sense for your product and team.
What Makes Code Testable?
Code that is hard to test usually hides its state, mixes third-party concerns into domain logic, and couples dependencies so tightly that every change ripples through the system. Code that is easy to test is the opposite: it is observable, predictable, and localized.
1. Observable (Observability)
Observable code exposes a small, clear surface. Public APIs are few, methods return values or have consistent side effects, and state does not mutate in surprising places. Well-placed logs and exceptions act as deliberate observation points. Single-responsibility design and deliberate interfaces shrink what you need to watch, so “what must be observed” stays small.
2. Predictable (Predictability)
Predictable code behaves in ways you can narrate before you run it: given this input and this state, you know what happens. Names align with behavior, state is minimal, and side effects live in one place. The goal is straightforwardness—same input, same result—without caches or hidden state changing outcomes behind your back.
3. Locality (Localized Change)
When code changes, the blast radius should be small. Wrapping third-party services with your own interfaces keeps external churn out of the domain and gives tests a stable seam. Updates then stay inside the adapter, mocks stay simple, and the domain stays steady even when dependencies move.
Summary
Testable code is observable, predictable, and localized. When you design for those three qualities, tests get simpler, and the codebase stays easier to change.