What does MockK's `verify { repo wasNot Called }` assert, and how is that different from `verify(exactly = 0) { repo.save(any()) }`?
answer
- wasNot Called = zero calls on the whole mock
- exactly = 0 = zero calls matching this method + matchers
- inverse = true is the per-call equivalent
- listOf(a, b) wasNot Called for several mocks
- stubbing in every {} is not a recorded call
basics
~20 swasNot Called asserts the mock received zero recorded calls at all — any method, any arguments. exactly = 0 is narrower: that one method with those matchers never matched, while other calls on the same mock are allowed.
solid answer
~50 sThey differ in **scope**. `verify { repo wasNot Called }` is a whole-object assertion: the mock has no recorded calls whatsoever. Any method, any arguments, one call, and it fails. MockK also accepts a list — `verify { listOf(repoA, repoB) wasNot Called }` — for several mocks at once. `verify(exactly = 0) { repo.save(any()) }` is a per-call assertion: no recorded call matches `save` with those matchers. `repo.findById(1)` in the same test is fine. `verify(inverse = true) { repo.save(any()) }` says the same thing another way. Pick by intent. "This collaborator must not be touched on the validation-failure path" is `wasNot Called`. "We may read, but must never write" is `exactly = 0` on the write method — `wasNot Called` there would be wrong and would fail for the wrong reason. One detail worth knowing: stubbing inside an `every` block is recording, not invoking, so arranging stubs never breaks a `wasNot Called`.
code
kotlin · 10 linesservice.reject(order)
// the gateway must be untouched entirely
verify { gateway wasNot Called }
verify { listOf(gateway, mailer) wasNot Called }
// the repo IS used, but must never write
verify(exactly = 1) { repo.findById(order.id) }
verify(exactly = 0) { repo.save(any()) }
verify(inverse = true) { repo.save(any()) } // same statement, other spellinggo deeper
Say it plainly: wasNot Called means the mock got no calls at all; exactly = 0 means one specific call never happened.
Add the matcher point — exactly = 0 is only as broad as its matchers and does not cover other methods or overloads — and mention inverse = true and the list form.
Choose by intent and diagnose failures: absence-of-dependency assertions use the whole-mock form, and a surprising failure points at shared setup or an uncleared mock rather than the test body.
Standardise which negative the team writes for which claim, so a passing negative assertion is real evidence rather than a narrowly scoped statement that happens to hold.
## Two different negatives MockK gives you three ways to assert something did not happen, and they are not interchangeable. **Whole-mock:** `verify { repo wasNot Called }`. `Called` is a MockK object and `wasNot` an infix function; the assertion holds only if the mock has *no recorded calls at all*. It is the strongest negative available and the right one when the point of the test is that a dependency is untouched — no email service on the validation-failure path, no repository when the cache hits, no payment gateway for a rejected order. A list form covers several mocks in one statement: `verify { listOf(mailer, gateway) wasNot Called }`. **Per-call:** `verify(exactly = 0) { repo.save(any()) }`. Only calls matching `save` with those argument matchers are counted, and the count must be zero. Everything else the mock received is irrelevant. This is what you want when the collaborator is legitimately used but one particular interaction must not occur. **Per-call, inverted:** `verify(inverse = true) { repo.save(any()) }` asserts the described call did not happen — practically equivalent to `exactly = 0` for that call, and a matter of team style which one you standardise on. ## Matchers still shape the per-call form `exactly = 0` is only as broad as its matchers. `verify(exactly = 0) { repo.save(premiumUser) }` says nothing about `repo.save(otherUser)`; it asserts that no *matching* call happened. If you mean "never saved anything", write `any()`. If you mean "never saved this particular user", write the specific argument. A negative assertion with narrow matchers passing is very weak evidence, and it is easy to write one by accident. Overloads matter too: `save(user)` and `save(user, options)` are different methods, so `exactly = 0` on one says nothing about the other. `wasNot Called` has no such gap, which is part of why it is the better tool when your claim really is "untouched". ## What counts as a recorded call Only real invocations by the code under test are recorded. Calls you write inside `every { }` while arranging stubs are executed under MockK's recorder to *describe* a call, not to make one, so they leave no recorded call and never break a `wasNot Called` assertion. That surprises people who assume elaborate arrangement disqualifies the strong negative — it does not. What does break it is a genuine invocation from shared setup: a `@BeforeEach` that primes state by calling the collaborator, or a fixture that exercises the object under test before the test body runs. If your `wasNot Called` fails and the test body plainly does not touch the mock, look at setup — and at whether the mock is shared across tests without its recorded calls being reset between them. ## Choosing well A good rule: reach for `wasNot Called` when the *absence of the dependency* is the behaviour under test, and for `exactly = 0` when the dependency is used but one interaction is forbidden. State the reason in the test name — `doesNotChargeGatewayWhenOrderRejected` reads better than a bare assertion and tells the next reader which negative was intended. And note the asymmetry in failure quality: `wasNot Called` failures tell you *something* was called and show what; `exactly = 0` failures point at one signature. Where you can honestly use the whole-mock form, it both asserts more and diagnoses better.
- Your `verify { repo wasNot Called }` fails, but the test body never touches repo. Where do you look?At everything that ran before the assertion. Shared setup that primes state through the collaborator, a fixture that exercises the object under test, or a mock reused across tests whose recorded calls were never reset are the usual causes. Note that arranging stubs inside every blocks is not an invocation and cannot be the cause, so the call is a real one from somewhere in setup.
- When is exactly = 0 the wrong assertion?When you actually mean the collaborator was untouched. exactly = 0 covers only the method and matchers you wrote, so a different overload, a different argument, or another method on the same mock all slip through and the test passes while the dependency was used. If the claim is 'untouched', wasNot Called says it exactly and gives a better failure message.
saying these in an interview costs you the question
- Treating wasNot Called and exactly = 0 as synonyms
- Writing exactly = 0 with a specific argument and reading it back as "never called"
- Assuming stubbing a mock inside every counts as an interaction that breaks wasNot Called
- Not realising overloads are separate methods, so exactly = 0 on one says nothing about the other
- Using wasNot Called on a collaborator the test legitimately reads from, then loosening the whole assertion when it fails