All projects
Case study/2026

Fakes vs Mocks.

A Kotlin/Spring Boot project demonstrating the trade-offs between fakes and mocks across five growing levels of complexity. Each branch is a self-contained scenario with its own tests, ready to clone and run.

Kotlin Spring Boot Testing TDD DDD

01Architecture

The final branch (level-4-payment-gateway) shows the PlaceOrderUseCase at full scope: customer lookup, per-item stock checks, order creation, stock reservation, domain event publishing, and a call to an external payment gateway. The diagram is the level-4 dependency graph.

Level 4 — full checkout with side effects + external system Customer Repository Order Repository Stock Repository Event Publisher Payment Gateway — external — PlaceOrder UseCase 5 collaborators · Kotlin · Spring Boot · JUnit 5 · mockk

02By the numbers

Each branch runs the same three tests — happy path, a rename, a private-method extraction — once with mocks, once with fakes. Numbers below are median wall-clock from the included benchmark script, run on the same machine for all branches.

Level 0
mock ~40ms
fake ~5ms
8×
Level 2
mock ~530ms
fake ~17ms
31×
Level 4
mock ~890ms
fake ~22ms
40×

The gap widens as dependencies pile up. Mocks generate a proxy class for every collaborator at test time; fakes are just plain objects doing a HashMap.put().

03The approach

Why fakes-first

Every branch ships a fake implementation of every collaborator — FakeCustomerRepository, FakeStockRepository, and so on — backed by an in-memory store. The mock-based tests are kept next to them so you can run both and watch which one breaks when you refactor.

Contract tests

An abstract CustomerRepositoryContractTest runs against both the fake and the JPA implementation. The two pass identically, so a divergence surfaces at test time, not in production.

Side effects get mocks, state gets fakes

The event publisher and the payment gateway are wrapped behind interfaces you control, and the tests assert on the side effect (a published event, a gateway call) — not on state. That's where mocks earn their place.

External system ownership

The payment gateway is the one place the project intentionally mocks a third-party type. Wrapping it behind your own interface is the only thing that keeps the rest of the suite from baking in library API shape.

04The branches

Each branch builds on the previous one. The benchmark script (./scripts/benchmark.sh) runs the whole suite 30 times and reports median and p95.

  1. 00
    main README and the benchmark script. Nothing else.
  2. 01
    level-0-single-repository PlaceOrderUseCase with one dependency. Mocks and fakes look identical here.
  3. 02
    level-1-two-repositories Adds StockRepository. The first time verify(exactly = 1) lies after a private-method extraction.
  4. 03
    level-2-full-checkout Adds OrderRepository and a full pre-validation pass. Fakes stop caring about call order.
  5. 04
    level-3-domain-events Adds the event publisher. Mocks do their actual job well here.
  6. 05
    level-4-payment-gateway Adds the external payment gateway behind a wrapper interface.
Case study

Want the full story? The case study has it.

The write-up walks through the design decisions, the failure modes, and what each approach bought or cost — across five levels of complexity, two testing strategies, and the benchmarks to compare them.