The idea: replace the collaborators, keep the unit

A unit test runs one piece of code, the unit under test, on its own. Here it is OrderService.placeOrder(sku, qty). In production the service talks to a pricing API, a Postgres database and an SMTP server. A test does not want any of them: they are slow, they need a network, and the test cannot make them answer what it needs.

So the service takes its collaborators as constructor parameters typed by interfaces, the ports PricingPort, OrderRepository and Mailer. A test plugs a test double into each port: an object that stands in for the real one, like a stunt double stands in for an actor. The service cannot tell the difference.

OrderService in the middle with three ports: PricingPort, OrderRepository and Mailer. Above, production plugs in the pricing API, Postgres and SMTP. Below, the test plugs in StubPrices (a stub), InMemoryOrderRepo (a fake) and mock(Mailer) (a mock)
The same three ports take the real adapters in production and test doubles in a unit test.

The canvas runs one test from start to end: arrange (build the doubles), act (call placeOrder("BOOK", 2)), assert (check the result). Every call and every return flies as a box. Demo 1: green test, all three doubles.

Stub: returns a canned answer

A stub answers calls with values the test picked in advance: priceOf(any sku) → 12.00. It has no logic and it does not check how it was called. Its job is to feed input into the unit, so the test can steer it down a path. To test the error branch, tell the stub to throw PriceUnavailable; there is no need to break a real pricing service. Demo 2: stub throws, error path.

The weakness shows in Demo 5: wrong SKU, the stub does not notice. The service asks the price of "PEN" instead of the SKU it was given. The stub answers 12.00 anyway, and the test passes with the bug inside. A stub that answers only priceOf("BOOK") (Mockito: when(prices.priceOf("BOOK")).thenReturn(12.00)) would have caught it.

Fake: a working, simpler implementation

A fake really works, but takes a shortcut that makes it unfit for production. InMemoryOrderRepo keeps orders in a HashMap instead of Postgres: save stores, findById finds. Other common fakes are an in-memory file system, a fake clock and an embedded database.

With a fake the test checks state: after the act it reads the data back, assertEquals(order, repo.findById(1)). If the service forgets to save, the map is empty and the read returns null. Demo 3: forgot save, the fake catches it.

Mock: records calls, the test verifies them

A mock does nothing useful. mock(Mailer.class) sends no e-mail; it only writes down every call it gets. At the end the test asks whether the expected call happened: verify(mailer).sendConfirmation(order). This is an interaction check.

Sending an e-mail leaves no state the test could read back, so a call is the only thing to check. If the service forgets it, Mockito fails with Wanted but not invoked. Demo 4: forgot e-mail, the mock catches it.

Left, state check with a fake: the service saves Order#1 into the fake's map, then the test calls findById(1) and compares the order it gets back. Right, interaction check with a mock: the service calls sendConfirmation, the mock adds it to its recorded calls, then the test calls verify and the mock compares recorded with expected calls
A fake is checked by reading its state back; a mock is checked by verifying the calls it recorded.

Which double catches which bug

Bug in OrderServiceStubFakeMockTest result
nonefeeds 12.00stores Order#1records 1 callpasses
forgets repo.save(o)–✗ findById(1) is nullnot reachedfails at assert 2
forgets sendConfirmation(o)–✓✗ wanted 1 call, got 0fails at verify
asks priceOf("PEN")answers anyway✓✓passes, bug missed

Like JUnit, the page stops a test at its first failed assertion, so in the save bug the verify line never runs.

Dummy, stub, spy, mock, fake

The names come from Gerard Meszaros (xUnit Test Patterns, 2007). The page shows three of five:

DoubleWhat it doesThe test checks
dummyfills a parameter, never usednothing
stubreturns canned answersthe unit's output
spya stub that also records callsthe calls, after the act
mockrecords calls against expectationsthe calls (verify)
fakea simpler working implementationthe state it holds

In everyday speech "mock" often means any double, because Mockito's mock() makes objects that can act as a stub (when(...).thenReturn(...)), a spy or a mock.

Too many mocks

A mock checks how the unit does its job, not what it produces. Verify every call, in order, and a harmless refactoring (two calls merged into one, a call moved) breaks the test even though the behaviour is the same. A common rule: stub or fake the queries, mock only the commands whose effect the test cannot see any other way, like an e-mail sent or a message published. Prefer a fake when the collaborator holds state.

See also Hexagonal Architecture, where the same ports take a test driver, an in-memory repository and fake adapters, and SOLID Principles for dependency inversion, which makes the ports possible.

What the page leaves out

Argument matchers and captors (ArgumentCaptor), call order (InOrder), strict stubs that fail on unused stubbing, partial mocks (spy() on a real object), mocking static methods, contract tests that check a fake behaves like the real adapter, and integration tests with Testcontainers that run the real Postgres are left out.