Map classic test-double theory (stub, mock, fake, spy) onto Kotlin tooling and explain when you'd prefer a fake over a MockK mock.
answer
- Dummy/Stub/Mock/Fake/Spy = Meszaros taxonomy
- MockK: every=stub, +verify=mock, spyk=spy; fakes are hand-written
- State verification (fakes) vs interaction verification (mocks)
- Fakes for reusable behavior + refactor resistance
- Over-mocking couples tests to implementation = false confidence
basics
~20 sA stub gives canned answers, a mock also checks it was called, a fake is a lightweight working implementation, and a spy wraps a real object. In Kotlin, MockK builds stubs/mocks/spies, while fakes are hand-written classes. Prefer fakes when behavior matters more than call verification.
solid answer
~50 sTest doubles (Meszaros' taxonomy): a **dummy** is passed but unused; a **stub** returns canned data; a **mock** is a stub plus interaction verification; a **fake** is a simplified working implementation (e.g., an in-memory repository); a **spy** wraps a real object recording/overriding calls. In Kotlin, MockK covers stubs (`every`), mocks (`every` + `verify`), and spies (`spyk`), with `coEvery`/`coVerify` for suspend collaborators. Fakes are just ordinary Kotlin classes implementing the same interface. Prefer fakes when you're testing *state/behavior* across many tests (they reduce brittle, implementation-coupled `verify` assertions and read as black-box state verification), and prefer mocks for *interaction* verification or when a real implementation is heavy/non-deterministic (network). Over-mocking couples tests to call structure and produces false confidence; fakes shared via a test module scale better. The principal lens: choose the double that verifies the property you care about (state vs interaction) with the least coupling.
go deeper
Can name stub vs mock and create a basic mockk stub.
Distinguishes stub/mock/fake/spy and maps them to MockK's every/verify/spyk plus hand-written fakes.
Chooses state vs interaction verification deliberately and avoids over-mocking brittle tests.
Sets codebase-wide doubling strategy: shared fakes, mocks only at boundaries, and least-coupling property verification.
## The Taxonomy (Gerard Meszaros) - **Dummy** — passed to satisfy a signature, never used. - **Stub** — returns hardcoded answers (no verification). - **Mock** — a stub whose *interactions you assert* (was `save()` called once?). - **Fake** — a real, simplified implementation (in-memory DB, fake clock). - **Spy** — wraps a real object; records calls and can override some. ## Mapping to Kotlin Tooling | Double | Kotlin approach | |--------|-----------------| | Stub | `mockk()` + `every { } returns` | | Mock | `mockk()` + `every` + `verify`/`confirmVerified` | | Spy | `spyk(realObject)` | | Fake | hand-written class implementing the interface | | Suspend stub/mock | `coEvery` / `coVerify` | | Relaxed default | `mockk(relaxed = true)` | ```kotlin // Fake: a working in-memory implementation class FakeUserRepository : UserRepository { private val store = mutableMapOf<Long, User>() override fun save(u: User) { store[u.id] = u } override fun findById(id: Long): User? = store[id] } ``` ## State vs Interaction Verification - **State verification** (fakes): exercise the system, then assert on observable *state* (`repo.findById(1) shouldBe expected`). Black-box; survives refactors. - **Interaction verification** (mocks): assert specific calls happened (`verify { gateway.charge(...) }`). White-box; couples the test to *how* the code works. ## When to Prefer a Fake - The collaborator has rich behavior reused across many tests (a repository, a cache, a clock). - You want black-box, refactor-resistant tests. - The real thing is heavy or non-deterministic and a faithful in-memory version is cheap. ## When a Mock Is Right - You genuinely care that an interaction happened (an email was sent, an event published) and there's no observable state. - Building a fake would be more code than value. - The collaborator is at a true boundary (third-party gateway). ## Anti-patterns - **Over-mocking**: every collaborator mocked, tests mirror implementation, refactors break green tests (false signal). - **Mocking what you don't own** without a thin adapter — brittle when the library changes. - **Relaxed mocks everywhere** hiding missing stubs. ## Principal-level synthesis Pick the double that verifies the *property you care about* (state vs interaction) with the *least coupling*. Maintain shared fakes in a test-fixtures module; reserve mocks for boundary interactions and verification-only behavior.
- Why can heavy mocking give 'false confidence'?Interaction-verifying mocks assert how the code calls collaborators. After a refactor that preserves behavior, the calls change and tests fail (or pass while behavior broke), so green no longer means correct.
- Where do fakes live so they're reused across tests?In a shared test-fixtures/test source set or module implementing the production interfaces, so many test classes reuse the same in-memory FakeRepository, FakeClock, etc.
A mock is a hidden camera checking who entered a room; a fake is a cardboard replica of the room you can actually walk through — use the camera to verify an event, the replica to verify the outcome.
saying these in an interview costs you the question
- Calling any test double a 'mock' without distinguishing stub/fake/spy
- Mocking value objects or data classes instead of just constructing them
- Verifying every interaction so tests break on harmless refactors
- Building elaborate mocks where a tiny fake would be clearer
- Mocking third-party types directly instead of behind an owned adapter