Test-Driven Development with Clojure

Learn when TDD improves Clojure design, how it fits a REPL-driven workflow, and how to avoid implementation-driven tests that make refactoring harder.

Test-driven development (TDD): A workflow where you write a failing test for an important behavior, make it pass with the smallest sensible change, and then refactor while keeping the behavior intact.

Clojure is a strong fit for TDD because so much useful code can be expressed as functions over data. That does not mean every change should begin with a test. It means tests are often the fastest way to clarify a contract once you know what behavior matters.

Why TDD Works Well in Clojure

TDD helps most when:

  • the behavior can be stated clearly
  • the inputs and outputs are data
  • the boundary is crisp
  • the implementation would otherwise sprawl

That matches a lot of Clojure work:

  • parsing and validation
  • event transformation
  • domain rules
  • permission checks
  • persistence adapters

The value is not ritual. The value is design pressure. A failing test forces you to answer useful questions early: what goes in, what comes out, what happens on invalid input, and what details should stay hidden.

Red, Green, Refactor with a REPL Nearby

TDD in Clojure should still use the REPL aggressively. A practical loop looks like this:

  1. write a small failing test for a real behavior
  2. explore edge cases in the REPL if the data shape is still fuzzy
  3. implement the narrowest honest solution
  4. refactor while the test stays green
1(deftest parse-order-total-test
2  (is (= {:subtotal 100M
3          :tax 13M
4          :total 113M}
5         (billing/parse-order-total
6           {:subtotal "100.00"
7            :tax-rate "0.13"}))))

The test gives you the target. The REPL helps you discover the implementation. Those tools reinforce each other rather than compete.

Start from Contracts, Not Helpers

Good TDD tests in Clojure usually protect:

  • public function behavior
  • returned data shape
  • invalid input handling
  • edge cases
  • side-effect boundaries

They should usually avoid:

  • asserting private helper calls
  • forcing one exact decomposition
  • pinning every intermediate value
  • mocking internal collaboration that could stay as plain function calls

If a refactor preserves behavior but breaks many tests, the tests are probably specifying choreography instead of the contract.

A Better TDD Example

The design benefit becomes clearer when the domain is a little messy:

 1(deftest classify-login-risk-test
 2  (is (= :challenge
 3         (auth/classify-login
 4           {:country "CA"
 5            :new-device? true
 6            :failed-attempts 0})))
 7  (is (= :deny
 8         (auth/classify-login
 9           {:country "CA"
10            :new-device? true
11            :failed-attempts 7}))))

Writing the tests first pushes you to decide:

  • should the return value be a keyword, boolean, or map?
  • where do thresholds live?
  • what state does the function really need?
  • can the function stay pure if data gathering happens elsewhere?

That pressure often produces smaller functions and cleaner boundaries.

When TDD Helps Less

TDD is not the first move for every problem. It is weaker when:

  • the domain is still poorly understood
  • the real question is architecture, not function behavior
  • the code is mostly wiring and infrastructure spikes
  • the fastest way to learn is a disposable experiment

In those cases, do the spike first. Then turn the discovered contract into tests once the behavior is stable enough to name.

Common Failure Modes

The usual mistakes are:

  • testing implementation structure instead of behavior
  • overusing mocks or with-redefs
  • forcing TDD into exploratory work where the problem is still unknown
  • treating green tests as proof of complete correctness

TDD narrows design mistakes. It does not replace integration testing, property testing, or production feedback.

Practical Heuristics

Use TDD when behavior is important and nameable. Prefer tiny cycles. Let the test shape the contract, not the internal structure. When the design is still exploratory, learn in the REPL first and bring the result back under tests once the boundary becomes real.

Ready to Test Your Knowledge?

Loading quiz…
Revised on Thursday, April 23, 2026