Vasu Patel logoVasu Patel
← Clean by Default: Coding Standards
All chapters in this track
  1. 01Chapter 1: Why Coding Standards Matter: Readability, Onboarding, Defects, Consistency at Scale
  2. 02Chapter 2: Naming Conventions
  3. 03Chapter 3: Formatting and Style
  4. 04Chapter 4: Comments and Self-Documenting Code
  5. 05Chapter 5: Error Handling Conventions
  6. 06Chapter 6: Code and Project Organization (Files, Modules, Packages)
  7. 07Chapter 7: Writing Testable Code
  8. 08Chapter 8: Commit Hygiene: Atomic Commits, Messages, PR-Sized Changes
  9. 09Chapter 9: Language-Specific Idioms and Anti-Patterns Across TypeScript and Python
  10. 10Chapter 10: Security-Conscious Basics: Input Validation, Secrets Handling, Injection Risks
  11. 11Chapter 11: Enforcing Standards Automatically: Linters, Formatters, CI Gates
  12. 12Chapter 12: Creating, Documenting, and Evolving a Team Style Guide (Exceptions and Legacy Code)
  13. 13Chapter 13: Worked Example: Refactoring a Messy Function to Standard
  14. 14Chapter 14: Worked Example: Drafting a Style Guide Section From Scratch
  15. 15Chapter 15: Common Interview Questions About Coding Standards and Code Quality

Chapter 7 of 15

Chapter 7: Writing Testable Code

Why this matters

Testability isn't a separate concern from good design — code that's hard to test is almost always hard to test because it has an underlying design problem (hidden dependencies, mixed concerns, hard-coded external state), and the same fixes that make code testable also make it more maintainable generally. This chapter treats "is this easy to test?" as a genuinely useful design-quality signal, not just a QA checkbox.

Hidden dependencies: the root cause of most untestable code

[BEGINNER] Code that constructs its own dependencies internally (rather than receiving them from outside) is hard to test because a test has no way to substitute a controlled, predictable test double for the real dependency.

Before — a hidden dependency makes this untestable without hitting a real database and a real clock:

class OrderService {
  placeOrder(items: Item[]): Order {
    const db = new PostgresConnection();  // hard-wired -- a test can't substitute this
    const total = items.reduce((sum, item) => sum + item.price, 0);
    const order = { items, total, createdAt: new Date() }; // real system clock -- untestable timing
    db.save(order);
    return order;
  }
}

After — dependencies are injected, making every part substitutable in a test:

interface Database {
  save(order: Order): void;
}
interface Clock {
  now(): Date;
}

class OrderService {
  constructor(private db: Database, private clock: Clock) {}  // injected -- DIP, OOP book Chapter 10

  placeOrder(items: Item[]): Order {
    const total = items.reduce((sum, item) => sum + item.price, 0);
    const order = { items, total, createdAt: this.clock.now() };
    this.db.save(order);
    return order;
  }
}

// In a test: substitute controlled, predictable fakes -- no real database, no real clock:
const fakeDb: Database = { save: jest.fn() };
const fixedClock: Clock = { now: () => new Date("2026-01-01T00:00:00Z") };
const service = new OrderService(fakeDb, fixedClock);
const order = service.placeOrder([{ price: 1000 }]);
expect(order.createdAt).toEqual(new Date("2026-01-01T00:00:00Z")); // deterministic, repeatable
from datetime import datetime
from typing import Protocol

class Database(Protocol):
    def save(self, order: dict) -> None: ...

class Clock(Protocol):
    def now(self) -> datetime: ...

class OrderService:
    def __init__(self, db: Database, clock: Clock) -> None:
        self._db = db
        self._clock = clock

    def place_order(self, items: list[dict]) -> dict:
        total = sum(item["price"] for item in items)
        order = {"items": items, "total": total, "created_at": self._clock.now()}
        self._db.save(order)
        return order

[IN PRACTICE] This is precisely the OOP book's Chapter 10 Dependency Inversion Principle, and the direct connection between "well-designed" and "testable" is the whole point of this chapter: code designed with DIP in mind is, almost automatically, easy to test, because the exact same mechanism (depending on an abstraction, injected from outside) that decouples high-level logic from low-level details also happens to be exactly what a test needs to substitute controlled fakes.

Separating pure logic from side effects

[SENIOR INSIGHT] A pure function (same input always produces the same output, no side effects) is trivially testable — no setup, no mocking, no cleanup, just call it and check the result. Code that mixes pure computation with side effects (I/O, mutation of external state) forces every test of that mixed logic to also deal with the side effects, even when the test's actual interest is only in the computation.

# Mixed: computation and I/O tangled together -- testing the DISCOUNT LOGIC
# requires mocking a database call, even though the logic itself has nothing
# to do with databases.
def calculate_final_price(order_id):
    order = db.get_order(order_id)  # I/O
    discount = 0.1 if order.total > 100 else 0
    return order.total * (1 - discount)  # the actual logic worth testing directly

# Separated: pure logic extracted, testable with zero mocking at all
def calculate_discount(total: float) -> float:
    return 0.1 if total > 100 else 0

def calculate_final_price_pure(total: float) -> float:
    return total * (1 - calculate_discount(total))

# The I/O-touching function becomes a thin wrapper around the pure logic:
def calculate_final_price(order_id):
    order = db.get_order(order_id)
    return calculate_final_price_pure(order.total)

# Testing the actual business logic now requires NOTHING but a plain function call:
assert calculate_final_price_pure(150) == 135.0

This pattern — sometimes called "functional core, imperative shell" — pushes as much logic as possible into pure, easily-testable functions, leaving only a thin outer layer responsible for I/O and side effects, which typically needs far less exhaustive testing since it contains little to no actual logic to get wrong.

Avoiding global/static mutable state

[BEGINNER] Global mutable state (a module-level variable, a singleton with mutable internal state — the OOP book's Chapter 4 Singleton discussion applies directly) makes tests order-dependent and non-isolated: one test's mutation of global state can silently affect a completely unrelated test that happens to run afterward, producing intermittent, confusing test failures that depend on execution order.

// Global mutable state -- tests can interfere with each other silently
let requestCount = 0;
function trackRequest() { requestCount++; }

// Test A increments requestCount. Test B, run afterward, sees a "polluted"
// starting value it didn't expect -- and whether this causes a failure
// depends entirely on TEST EXECUTION ORDER, which is exactly the kind of
// hard-to-reproduce bug concurrency taught us to recognize (companion
// Concurrency book, Chapter 11) -- test isolation failures share the same
// "intermittent, order-dependent" signature.

The fix: encapsulate state in an instance (created fresh per test) rather than a module-level global, exactly the OOP book's Chapter 3 encapsulation principle, applied to test isolation specifically.

Common mistakes

  1. Constructing dependencies internally rather than injecting them, making it impossible for a test to substitute a controlled fake without extensive mocking-framework gymnastics or, worse, requiring a real database/network connection just to run a unit test.
  2. Mixing pure computation with I/O/side effects in the same function, forcing every test of the logic to also deal with mocking the side effects.
  3. Relying on global or static mutable state, causing tests to silently interfere with each other depending on execution order — a subtle, intermittent test-suite failure mode that's genuinely hard to diagnose.
  4. Treating "this code is hard to test" as a testing problem to work around (with increasingly elaborate mocking) rather than a design signal pointing at a hidden dependency, mixed concern, or global state problem worth fixing directly.

Checklist: writing testable code

  1. Inject dependencies (databases, clocks, network clients, file systems) rather than constructing them internally — this is Dependency Inversion, and it's simultaneously good design and the direct enabler of substitutable test doubles.
  2. Extract pure logic away from I/O and side effects wherever possible — a "functional core, imperative shell" structure minimizes how much of the codebase needs mocking-heavy tests at all.
  3. Avoid global/static mutable state; encapsulate state in instances created fresh per test (or per request) instead.
  4. When code is hard to test, treat that difficulty as a design smell — identify the specific hidden dependency, mixed concern, or global state causing the difficulty, and fix the design rather than reaching for heavier mocking tools.

Discussion / interview questions

  1. "Why is code with hidden, internally-constructed dependencies hard to test?" Strong answer: A test has no way to substitute a controlled, predictable fake for a dependency the code constructs itself internally — testing such code either requires heavy mocking-framework tricks or actually exercising the real dependency (a real database, a real clock), which is slow, flaky, and hard to make deterministic.

  2. "What is 'functional core, imperative shell,' and why does it help testability?" Strong answer: Separating pure computational logic (no side effects, trivially testable) from a thin outer layer handling I/O and side effects — this minimizes how much of the codebase requires mocking-heavy tests, since the bulk of actual logic can be tested with plain function calls and no setup.

  3. "How can global mutable state cause intermittent, hard-to-diagnose test failures?" Strong answer: One test's mutation of shared global state can silently affect a different, unrelated test that runs afterward, causing failures that depend on test execution order — a subtle, hard-to-reproduce failure mode, fixed by encapsulating state in instances created fresh per test rather than relying on module-level globals.

  4. "If a piece of code is hard to test, what would you consider first: writing more elaborate mocks, or something else?" Strong answer: Consider the difficulty a design signal first — check for hidden, internally-constructed dependencies, mixed pure-logic-and-side-effects, or global mutable state — and fix the underlying design (dependency injection, separating logic from I/O) rather than reaching immediately for heavier mocking machinery to work around it.

Chapter summary / cheat sheet

  • Hidden, internally-constructed dependencies are the root cause of most untestable code — inject dependencies instead (Dependency Inversion), which is simultaneously good design and directly enables test substitution.
  • Separate pure logic from I/O/side effects ("functional core, imperative shell") — pure functions need no mocking at all and are trivially testable.
  • Avoid global/static mutable state — it causes silent, order-dependent test interference; encapsulate state in fresh instances instead.
  • "This code is hard to test" is a design smell, not just a testing inconvenience — fix the underlying design issue rather than reaching for heavier mocking tools.