How to reset mocks between tests
Run one test alone with npx vitest run file -t "name", then run its whole file. A test that only fails in the file is reading state a neighbour left behind. Which knob fixes it depends on what leaked: call counts, a patched implementation, or the spy itself.
Why check this
Run this when a test passes alone and fails in the suite, when a test passes in the suite and fails alone, and after adding the first vi.spyOn to a file that already had mocks in it.
The failure it prevents is an order-dependent suite. A leaked implementation makes a later test assert a number no production path produces, so the suite agrees with itself and disagrees with the application. The fixture below leaks a 19 percent VAT rate into a test written for 20 percent, and shows the reverse as well: a test that passes only because the rate leaked.
Prerequisites
- Node 22.23.2, Vitest 5.0.0 and Jest 30.5.0, on one Windows 11 machine, on 2026-09-12. Confirm yours with
npx vitest --version: the answer differs between the two runners. - Two scratch projects, one per runner, outside your repository. See the Vitest config for mocks.
- The module under test, one object with a method to spy on and one function that reads it.
cat > src/rates.js <<'EOF'
export const rates = {
vat() {
return 0.2;
},
};
export function gross(net) {
return Math.round(net * (1 + rates.vat()) * 100) / 100;
}
EOF
- The leaking test file. Test A installs a spy and never takes it off.
cat > test/leak.test.js <<'EOF'
import { it, expect, vi } from 'vitest';
import { rates, gross } from '../src/rates.js';
const audit = vi.fn(() => 'ok');
it('A: applies the German rate', () => {
vi.spyOn(rates, 'vat').mockReturnValue(0.19);
audit('A');
expect(gross(100)).toBe(119);
});
it('B: counts only its own audit call', () => {
audit('B');
expect(audit).toHaveBeenCalledTimes(1);
});
it('C: applies the standard rate', () => {
expect(gross(100)).toBe(120);
});
it('D: reuses whatever rate is in place', () => {
expect(gross(100)).toBe(119);
});
it('E: still sees a spy on rates.vat', () => {
expect(vi.isMockFunction(rates.vat)).toBe(true);
});
EOF
Steps
- Step 1.
Run the whole file and note which test fails.
npx vitest run test/leak.test.js --reporter=verbose✓ test/leak.test.js > A: applies the German rate 2ms ✓ test/leak.test.js > B: counts only its own audit call 0ms × test/leak.test.js > C: applies the standard rate 7ms → expected 119 to be 120 // Object.is equality ✓ test/leak.test.js > D: reuses whatever rate is in place 0ms ✓ test/leak.test.js > E: still sees a spy on rates.vat 0ms Test Files 1 failed (1) Tests 1 failed | 4 passed (5)B passed. On Vitest 5 the call history is cleared before every test already, so the classic count leak is not what is wrong here. C received 119 from a spy installed two tests earlier.
- Step 2.
Run the failing test on its own.
npx vitest run test/leak.test.js -t "C:" --reporter=verbose↓ test/leak.test.js > A: applies the German rate ↓ test/leak.test.js > B: counts only its own audit call ✓ test/leak.test.js > C: applies the standard rate 1ms ↓ test/leak.test.js > D: reuses whatever rate is in place ↓ test/leak.test.js > E: still sees a spy on rates.vat Test Files 1 passed (1) Tests 1 passed | 4 skipped (5)Green alone, red in the file. That pair is the definition of a leak, and it points away from C.
- Step 3.
Run the test that depends on the leak, also on its own.
npx vitest run test/leak.test.js -t "D:" --reporter=verbose× test/leak.test.js > D: reuses whatever rate is in place 7ms → expected 120 to be 119 // Object.is equality FAIL test/leak.test.js > D: reuses whatever rate is in place AssertionError: expected 120 to be 119 // Object.is equality - Expected + Received - 119 + 120D is the same bug from the other end: green in the file, red alone. Repairing the leak turns it red, so fix both in one change.
- Step 4.
Measure what each of the three methods does, rather than recalling it.
cat > test/matrix.test.js <<'EOF' import { it, vi } from 'vitest'; import { rates } from '../src/rates.js'; it('what each reset method does', () => { for (const method of ['mockClear', 'mockReset', 'mockRestore']) { const plain = vi.fn(() => 'orig'); plain.mockReturnValue('patched'); plain('call'); const spy = vi.spyOn(rates, 'vat').mockReturnValue(0.19); rates.vat(); plain[method](); spy[method](); console.log( method.padEnd(12), 'vi.fn calls=' + plain.mock.calls.length, 'returns=' + JSON.stringify(plain()), '| spy calls=' + (rates.vat.mock ? rates.vat.mock.calls.length : 'no longer a spy'), 'rates.vat()=' + rates.vat() ); vi.restoreAllMocks(); } }); EOFnpx vitest run test/matrix.test.js --reporter=verbosestdout | test/matrix.test.js > what each reset method does mockClear vi.fn calls=0 returns="patched" | spy calls=0 rates.vat()=0.19 mockReset vi.fn calls=0 returns="orig" | spy calls=0 rates.vat()=0.2 mockRestore vi.fn calls=0 returns="orig" | spy calls=no longer a spy rates.vat()=0.2All three clear the call history.
mockClearkeeps the patched return value, which is why it cannot fix C.mockResetputs back the implementation the mock started with, the factory passed tovi.fn()or the real method under a spy.mockRestorealso removes the spy from the object, sorates.vatstops being a mock function. - Step 5.
Run the same three methods under Jest, because the middle row is different.
The second project holds the same two files in CommonJS, with
requireinstead ofimportandjest.fninstead ofvi.fn. Nothing else changes.npx jest test/matrix.test.jsconsole.log mockClear jest.fn calls=0 returns="patched" | spy calls=0 rates.vat()=0.19 console.log mockReset jest.fn calls=0 returns=undefined | spy calls=0 rates.vat()=undefined console.log mockRestore jest.fn calls=0 returns=undefined | spy calls=no longer a spy rates.vat()=0.2Jest 30 strips the implementation instead of restoring it. The consequence, measured on the same fixture:
npx jest test/afterreset.test.jsconsole.log jest 30 after mockReset: rates.vat() = undefined gross(100) = NaN console.log jest 30 after mockRestore: rates.vat() = 0.2 gross(100) = 120Same method name, opposite result. A cleanup copied from a Jest answer into a Vitest suite, or the other way, changes what the code under test computes.
- Step 6.
Run the leaking file under each config flag and read the column.
cat > vitest.restore.config.js <<'EOF' import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { restoreMocks: true } }); EOFnpx vitest run test/leak.test.js --config vitest.restore.config.js --reporter=verbose✓ test/leak.test.js > A: applies the German rate 2ms ✓ test/leak.test.js > B: counts only its own audit call 0ms ✓ test/leak.test.js > C: applies the standard rate 0ms × test/leak.test.js > D: reuses whatever rate is in place 5ms × test/leak.test.js > E: still sees a spy on rates.vat 1ms Test Files 1 failed (1) Tests 2 failed | 3 passed (5)Swap the flag name for
clearMocksormockResetand run again. The four runs of this file gave: default, C fails.clearMocks: false, B and C fail.mockReset: true, C passes and D fails.restoreMocks: true, C passes, D fails and E fails as well, because the spy is gone by the time E looks for it.
How to read the result
| What you see | What it means | What to do |
| --- | --- | --- |
| Passes alone, fails in the file | The test reads state a neighbour left | Find the neighbour that installs it, then pick a flag below |
| Passes in the file, fails alone | The test depends on the leak | Rewrite it to install what it needs |
| Call counts too high | clearMocks is off | Turn it on, or it is on and the counts are real |
| A stale return value | clearMocks cannot touch implementations | mockReset: true, or mockReset() in afterEach |
| rates.vat is no longer a mock | restoreMocks removed the spy before the test | Install the spy inside the test that needs it |
| NaN after a reset under Jest | Jest's mockReset leaves the mock returning undefined | Use mockRestore for a spy, or re-set the implementation |
| Both flags fix it | They differ only on whether the spy stays attached | restoreMocks for spyOn, mockReset for vi.fn |
Common mistakes
What to check next
- How to verify a mock was called: the assertions whose counts these flags decide.
- How to spy on a function in jest: where the leaked spy usually comes from.
- How to mock fetch in vitest: a global stub outlives a test the same way, and needs
unstubAllGlobals. - How to detect flaky tests: a suite that fails only in some orders is measured the same way.
FAQ
How do I reset a mock after each test?
Prefer the config flag over a hook, so it covers every file: mockReset: true for implementations, restoreMocks: true for a spy on somebody else's object. In one file, afterEach(() => vi.restoreAllMocks()) does the same.
What is the difference between clear, reset and restore?
Clear empties the call history. Reset also puts the implementation back to what it started as. Restore also removes the spy from the object it was installed on. Step 4 prints all three in one run.
How does this map to Mockito?
Mockito's reset(mock) drops stubbing and invocations together, which is nearer to mockReset. The Mockito advice to build a fresh mock per test instead transfers: a mock created inside the test cannot leak into its neighbours.
Does clearMocks affect a spy created with vi.spyOn?
It clears its call history and keeps the replacement implementation. The spy stays installed on the object until something restores it.
Which flag should a new project turn on?
restoreMocks: true covers the most cases: it clears, resets and uninstalls spies. Check first that nothing relies on a spy surviving into the next test, as test E does.
Verified
Verified by Maks Vernynode 22.23.2vitest 5.0.0jest 30.5.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
intermediate12 minpublished updated Maks Verny