How to verify a mock was called
Pass the code a vi.fn(), run it, then assert expect(gateway.charge).toHaveBeenCalledWith(payload). The weaker toHaveBeenCalled() passes on the run below even though the amount left in euros and the gateway was charged in dollars. Only the argument assertion prints a diff naming the field that differs.
Why check this
Run this when the only observable effect of the code is an outbound call: a payment gateway, an email transport, an analytics sink, a queue publisher. Nothing comes back to assert on, so the double is the whole test.
The failure it prevents is a silent contract break. The module below hardcodes currency: 'USD' while the order was quoted in EUR. A suite that asserts the gateway was called stays green through that, through a customer email appearing in the payload, and through calls arriving in the wrong order. Each needs its own assertion.
Prerequisites
- Node 22.23.2 and npm. Every output on this page came from one Windows 11 machine on 2026-09-12.
- Vitest 5.0.0. Confirm with
npx vitest --version, because the failure format and the matcher names moved between majors. See the Vitest mock function API. - A scratch project. Build it outside your repository so a fixture cannot reach your own suite.
mkdir mockdemo && cd mockdemo
npm init -y > /dev/null
npm pkg set type=module
npm install -D vitest@5
mkdir src test
- The code under test.
placeOrdercalls a collaborator once,trackEventscalls one three times and reuses a single object to do it, which is what step 3 turns on.
cat > src/checkout.js <<'EOF'
export async function placeOrder(order, gateway) {
const payload = {
id: order.id,
amount: order.totalCents,
currency: 'USD',
customer: { id: order.customerId, email: order.email },
};
return gateway.charge(payload);
}
export function trackEvents(events, sink) {
const frame = { type: '', id: 0 };
for (const event of events) {
frame.type = event.type;
frame.id = event.id;
sink(frame);
}
}
EOF
Steps
- Step 1.
Assert only that the collaborator ran, and watch a wrong currency pass.
cat > test/called.test.js <<'EOF' import { describe, it, expect, vi } from 'vitest'; import { placeOrder } from '../src/checkout.js'; const order = { id: 'A-1', totalCents: 900, currency: 'EUR', customerId: 7, email: 'k@example.com' }; describe('placeOrder', () => { it('calls the gateway', async () => { const gateway = { charge: vi.fn().mockResolvedValue({ ok: true }) }; await placeOrder(order, gateway); expect(gateway.charge).toHaveBeenCalled(); expect(gateway.charge).toHaveBeenCalledTimes(1); }); }); EOFnpx vitest run test/called.test.jsRUN v5.0.0 C:/…/mockdemo Test Files 1 passed (1) Tests 1 passed (1) Start at 11:02:39 Duration 237ms (transform 49%, import 34%, tests 10%, worker 8%)Green. The gateway was charged in the wrong currency and this test cannot see it.
- Step 2.
Assert the argument instead, and read the diff.
cat > test/args.test.js <<'EOF' import { describe, it, expect, vi } from 'vitest'; import { placeOrder } from '../src/checkout.js'; const order = { id: 'A-1', totalCents: 900, currency: 'EUR', customerId: 7, email: 'k@example.com' }; describe('placeOrder', () => { it('charges the order currency', async () => { const gateway = { charge: vi.fn().mockResolvedValue({ ok: true }) }; await placeOrder(order, gateway); expect(gateway.charge).toHaveBeenCalledWith({ id: 'A-1', amount: 900, currency: 'EUR', customer: { id: 7, email: 'k@example.com' }, }); }); it('passes a loose shape check', async () => { const gateway = { charge: vi.fn().mockResolvedValue({ ok: true }) }; await placeOrder(order, gateway); expect(gateway.charge).toHaveBeenCalledWith(expect.objectContaining({ amount: 900 })); }); }); EOFnpx vitest run test/args.test.jsFAIL test/args.test.js > placeOrder > charges the order currency AssertionError: expected "vi.fn()" to be called with arguments: [ { id: 'A-1', amount: 900, …(2) } ] Received: 1st vi.fn() call: @@ -1,9 +1,9 @@ [ { "amount": 900, - "currency": "EUR", + "currency": "USD", "customer": { "email": "k@example.com", "id": 7, }, "id": "A-1", Number of calls: 1 Test Files 1 failed (1) Tests 1 failed | 1 passed (2)Three things to read. The diff sorts keys alphabetically, so its order is not the order your code wrote them in.
Number of calls: 1is how you tell a wrong argument from a second unexpected call. The header truncates the expected object at two fields, so the diff body is the only full record.The second test in that file passed. It asserts
toHaveBeenCalledWith(expect.objectContaining({ amount: 900 })), which ignores every other field, including the currency and the customer email. - Step 3.
Assert the order of the calls with
toHaveBeenNthCalledWith.cat > test/order.test.js <<'EOF' import { describe, it, expect, vi } from 'vitest'; import { trackEvents } from '../src/checkout.js'; const events = [ { type: 'view', id: 1 }, { type: 'add', id: 2 }, { type: 'buy', id: 3 }, ]; describe('trackEvents', () => { it('sends three frames', () => { const sink = vi.fn(); trackEvents(events, sink); expect(sink).toHaveBeenCalledTimes(3); expect(sink).toHaveBeenCalledWith({ type: 'buy', id: 3 }); }); it('sends the view frame first', () => { const sink = vi.fn(); trackEvents(events, sink); expect(sink).toHaveBeenNthCalledWith(1, { type: 'view', id: 1 }); }); }); EOFnpx vitest run test/order.test.js❯ test/order.test.js (2 tests | 1 failed) 19ms ❯ trackEvents (2) × sends the view frame first 12ms FAIL test/order.test.js > trackEvents > sends the view frame first AssertionError: expected 1st "vi.fn()" call to have been called with [ { type: 'view', id: 1 } ] - Expected + Received [ { - "id": 1, - "type": "view", + "id": 3, + "type": "buy", }, ]The first call is reported as the third event. The other test in the file passed both
toHaveBeenCalledTimes(3)andtoHaveBeenCalledWith({ type: 'buy', id: 3 }), so the count is right and the order looks wrong. - Step 4.
Print the recorded calls before believing the diff.
cat > test/inspect.test.js <<'EOF' import { describe, it, expect, vi } from 'vitest'; import { trackEvents } from '../src/checkout.js'; const events = [ { type: 'view', id: 1 }, { type: 'add', id: 2 }, { type: 'buy', id: 3 }, ]; describe('trackEvents', () => { it('records what each call received', () => { const sink = vi.fn(); trackEvents(events, sink); console.log(JSON.stringify(sink.mock.calls)); console.log('same object in every call:', sink.mock.calls[0][0] === sink.mock.calls[2][0]); }); it('copies each frame before asserting', () => { const seen = []; const sink = vi.fn((frame) => seen.push({ ...frame })); trackEvents(events, sink); expect(seen[0]).toEqual({ type: 'view', id: 1 }); expect(seen).toHaveLength(3); }); }); EOFnpx vitest run test/inspect.test.js --reporter=verbosestdout | test/inspect.test.js > trackEvents > records what each call received [[{"type":"buy","id":3}],[{"type":"buy","id":3}],[{"type":"buy","id":3}]] same object in every call: true ✓ test/inspect.test.js > trackEvents > records what each call received 3ms ✓ test/inspect.test.js > trackEvents > copies each frame before asserting 2msmock.callsstores references, not copies.trackEventsreuses oneframeobject, so all three entries are that object holding its final state. The code sent the events in the right order and the recording cannot show it. The second test proves the point: a double written asvi.fn((frame) => seen.push({ ...frame }))copies each argument at call time, andseen[0]is the view frame.The default reporter printed neither
console.logline. Passing files hide their stdout until you add--reporter=verbose. - Step 5.
Pair every negative assertion with a positive one on the same double.
cat > test/wiring.test.js <<'EOF' import { describe, it, expect, vi } from 'vitest'; import { placeOrder } from '../src/checkout.js'; const order = { id: 'A-1', totalCents: 900, currency: 'EUR', customerId: 7, email: 'k@example.com' }; describe('placeOrder', () => { it('writes no audit line for a free order', async () => { const gateway = { charge: vi.fn().mockResolvedValue({ ok: true }) }; const audit = vi.fn().mockName('audit'); await placeOrder({ ...order, totalCents: 0 }, gateway); expect(audit).not.toHaveBeenCalled(); }); it('writes an audit line for a paid order', async () => { const gateway = { charge: vi.fn().mockResolvedValue({ ok: true }) }; const audit = vi.fn().mockName('audit'); await placeOrder(order, gateway); expect(audit).toHaveBeenCalled(); }); }); EOFnpx vitest run test/wiring.test.js❯ test/wiring.test.js (2 tests | 1 failed) 8ms ❯ placeOrder (2) × writes an audit line for a paid order 3ms FAIL test/wiring.test.js > placeOrder > writes an audit line for a paid order AssertionError: expected "audit" to be called at least once ❯ test/wiring.test.js:18:19 16| const audit = vi.fn().mockName('audit'); 17| await placeOrder(order, gateway); 18| expect(audit).toHaveBeenCalled(); | ^Both tests build
const audit = vi.fn()and neither passes it toplaceOrder, which takes two parameters. The first test assertsnot.toHaveBeenCalled()and passes, proving nothing. The twin fails, which is the only reason you find out.mockName('audit')is what puts the name in the message instead ofvi.fn().
How to read the result
| What you see | What it means | What to do |
| --- | --- | --- |
| toHaveBeenCalled() passes | The collaborator ran, with unknown arguments | Replace it with toHaveBeenCalledWith |
| A diff on one field | The call happened, the payload is wrong | Fix the code or the expectation, and keep the assertion |
| Number of calls: 1 under a diff | One call, wrong argument | Read the diff, not the count |
| Number of calls: 2 under a diff | An extra call you did not expect | Add toHaveBeenCalledTimes before chasing the payload |
| Every recorded call shows the last value | The code reuses one object across calls | Copy the argument inside the double, then assert on the copy |
| A negative assertion passes on its own | The double may never have been wired in | Write the positive twin and watch it fail |
| expected "vi.fn()" to be called | The double has no name | Add .mockName('audit') for readable failures |
Common mistakes
What to check next
- How to spy on a function in jest: what to do when the collaborator is an import rather than a parameter.
- How to reset mocks between tests: call counts that survive into the next test make every assertion here unreliable.
- How to mock fetch in vitest: the same assertions applied to a network call.
- How to detect flaky tests: a call assertion that fails one run in ten is usually an ordering problem.
FAQ
How do I verify a method was not called?
expect(mock).not.toHaveBeenCalled(), and never on its own. Add a test that asserts the same double was called in a case where it should be. Step 5 shows the negative passing and the positive failing on a double nobody wired in.
How do I check the arguments a mock was called with?
toHaveBeenCalledWith(...) for any call, toHaveBeenNthCalledWith(n, ...) for a specific one, toHaveBeenLastCalledWith(...) for the last. To look rather than assert, read mock.calls, an array with one entry per call holding that call's arguments.
How does this map to Mockito?
verify(gateway).charge(payload) is expect(gateway.charge).toHaveBeenCalledWith(payload). verify(gateway, never()).charge(any()) is expect(gateway.charge).not.toHaveBeenCalled(). Mockito's ArgumentCaptor corresponds to reading mock.calls, and it has the same reference hazard as step 4.
Why does my assertion pass when the value is wrong?
Three usual reasons: the assertion is toHaveBeenCalled() and ignores arguments, it is wrapped in expect.objectContaining and ignores the field, or the double under assertion is not the one the code used.
Are these matchers the same in Jest?
The matcher names are the same. The failure output is not. The diff layout, the Number of calls line and the stdout handling on this page are Vitest 5.0.0.
Verified
Verified by Maks Vernynode 22.23.2vitest 5.0.0
Each output block is what the command above it printed on that date, on the host named in the step. Figures read from a live site move between runs. Compare the shape of the answer rather than the digits, and see the methodology for how a page is re-verified.
Related on this site
intermediate9 minpublished updated Maks Verny