QuestionsTesting

Testing pure functions vs. side effects

Side EffectsEasyTesting

Why are pure functions the easiest thing to test, and how do you make code with side effects testable?

What it tests

Understanding how purity affects testability and the design moves that isolate side effects.

Approach & answer

A PURE function's output depends only on its inputs and it causes no observable side effects (no I/O, no mutation of external state, no reading the clock or random). That makes it trivially testable: give inputs, assert the output, done — no setup, no doubles, no teardown, fully deterministic, and fast. This is why pushing logic into pure functions is a testability superpower: the hard-to-test parts shrink. SIDE-EFFECTFUL code — hits the network, writes a file, reads Date.now()/Math.random(), mutates a global, logs, touches the DOM — is harder because the result isn't determined by inputs alone and running it does something to the world. Three moves make it testable. (1) SEPARATE the effect from the decision: extract a pure 'calculate what to do' function and keep a thin impure shell that performs the effect. You unit-test the pure core exhaustively and only lightly test the shell. (2) INJECT dependencies rather than reaching for them: pass the clock, the fetch function, the random source, or the repository as arguments (or via a constructor), so a test can pass a fake — e.g., accept `now = () => Date.now()` so tests pass a fixed time. (3) Use CONTROLLED doubles at the real boundaries for the effects you can't remove: fake timers for time, MSW for network, an in-memory repo for the database. The design payoff is broader than tests — code that's easy to test (small pure functions + injected dependencies + effects at the edges) is also easier to reason about and reuse. So 'this is hard to test' is usually a design signal: the logic and the effect are tangled and want to be pulled apart.

Use this technique when

Refactoring hard-to-test code; deciding where to put logic vs. effects.

Code

// Hard to test: reads the clock + performs the effect inline
function greet() {
  const h = new Date().getHours();               // hidden input
  document.title = h < 12 ? 'Morning' : 'Hello'; // effect
}

// Testable: pure decision (inject the hour) + thin effectful shell
function greetingFor(hour) {                      // PURE, trivial to test
  return hour < 12 ? 'Morning' : 'Hello';
}
function applyGreeting(now = () => new Date()) {  // shell, inject clock
  document.title = greetingFor(now().getHours());
}

expect(greetingFor(9)).toBe('Morning');
expect(greetingFor(15)).toBe('Hello');

References