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

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
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

  1. 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.

  2. 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.

  3. 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
    + 120

    D 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.

  4. 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();
      }
    });
    EOF
    
    npx vitest run test/matrix.test.js --reporter=verbose
    
    stdout | 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.2

    All three clear the call history. mockClear keeps the patched return value, which is why it cannot fix C. mockReset puts back the implementation the mock started with, the factory passed to vi.fn() or the real method under a spy. mockRestore also removes the spy from the object, so rates.vat stops being a mock function.

  5. 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 require instead of import and jest.fn instead of vi.fn. Nothing else changes.

    npx jest test/matrix.test.js
    
      console.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.2

    Jest 30 strips the implementation instead of restoring it. The consequence, measured on the same fixture:

    npx jest test/afterreset.test.js
    
      console.log
      jest 30 after mockReset: rates.vat() = undefined  gross(100) = NaN
    
    console.log
      jest 30 after mockRestore: rates.vat() = 0.2  gross(100) = 120

    Same 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.

  6. 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 } });
    EOF
    
    npx 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 clearMocks or mockReset and 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

Sign: A guide says mocks keep their call counts between tests, and yours do not.Cause: Vitest 5 clears call history before every test by default. Setting clearMocks: false brings the counts back and makes test B here fail with 'expected 1 times, but got 2 times'. Read your own config before copying a fix.
Sign: mockReset leaves the mock returning undefined and the code under test returns NaN.Cause: That is Jest 30. Vitest 5 puts back the factory implementation and, under a spy, the real method. Step 4 and step 5 print the same loop on both runners, and only the mockReset row differs.
Sign: Turning on restoreMocks fixes one test and breaks another.Cause: A spy installed in one test is gone by the next. Any test that assumed the spy was still attached, as E does here, fails as soon as the leak is repaired. Expect the repair to surface those, and fix them in the same change.
Sign: afterEach(() => vi.clearAllMocks()) is added and the failure stays.Cause: Clearing removes the recorded calls and keeps the patched behaviour. For a leaked return value the method is mockReset, and for a spy on somebody else's object it is mockRestore.

What to check next

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.

intermediate12 minpublished updated Maks Verny