Clearing Mocks in Vitest
/
Vitest is a popular testing framework for Node and frontend JavaScript applications. In this post I’ll describe an effective approach for managing mocks as they’re shaped and reshaped from test to test.
In Vitest, mocks are created with vi.spyOn
and vi.mock
. vi.spyOn
is the one I use most often. For the purposes of this post, I’ll stick with vi.spyOn
, but the restoring logic works the same way in both.
Clearing, Resetting, and Restoring
vi.clearAllMocks
, vi.resetAllMocks
, and vi.restoreAllMocks
are the functions used to broadly change the state of mocks in a test suite.
Let’s explore the situations you might need them.
Never Reset Mocks
resetAllMocks
replaces the mock implementations with undefined functions, and zeroes out any invocation state.
resetAllMocks
is a pitfall you might encounter when writing Vitest tests because there’s rarely a need to replace mock implementations with undefined functions. I consider it a pitfall because the naming suggests something useful, but it breaks tests that rely on some kind of mocking implementation. It’s confusing that a function named reset
doesn’t reset state in a more intuitive way. There may be niche debugging scenarios where this is useful. I’m not sure. Leaving the mock implementation as-is, as clearAllMocks
does, is normally a much better approach when resetting state between tests.
Rarely Restore Mocks
restoreAllMocks
restores the original unmocked implementations, and zeroes out any invocation state.
Vitest mocks remain unchanged between tests and between describe blocks in the same test file, but they are restored to original implementations between test files. This behavior has the nice property that we can build up whatever complexity of mocking we need inside of a test file and modify parts of it along the way. restoreAllMocks
has the effect of removing all mocking. You rarely want to undo mocking in a wholesale way, you usually only want to change some of it.
In some cases it’s useful to build all of your mocks from scratch–maybe a test file is making broad changes to mocks from test to test, or maybe you wish to have idempotent mocking that’s expensive to construct but easy to reason about. For that reason, restoreAllMocks
should only occasionally be used.
Frequently Clear Mocks
clearAllMocks
leaves mock implementations as-is, and zeroes out any invocation state.
I typically set up spies in beforeEach
or in the test themselves. The mock state is cleared between tests with clearAllMocks
and the mock implementations remained mocked for the next tests. This is the way.
Here’s an example of what’s usually needed:
beforeEach(async () => { // start each test with cleared state vi.clearAllMocks();
vi.spyOn(axios, 'create').mockReturnValue(vi.fn());
vi.spyOn(logService, 'info').mockReturnValue(vi.fn()); // ...
vi.spyOn(producerService, 'getInstance') .mockImplementation(async () => { return { connect: vi.fn(() => Promise.resolve()), send: vi.fn(() => Promise.resolve()), on: vi.fn(), }; });});
// Tests
Notes on vi.mock
The problem with vi.mock
is that it hoists its implementation at runtime, so it’s usually not possible to supply local or imported values. For that reason, I tend to avoid it.
vi.mock
mocks the entire module, so I’ll still use it in certain cases. For example, constant variables are common in projects, so if you need to replace an exported constant value for a test, you can use the following pattern.
import * as constants from './constants/index.js';
// Make the constants mutablevi.mock('./constants/index.js', async (importOriginal) => ({...(await importOriginal()),}));
//.. you can modify those values nowconstants.foo = 'bar';
Conclusion
This is the main pattern I’ve settled on for clearing mocks for integration and unit tests in NodeJS a frontend systems. Vitest mostly has feature parity with Jest, so most of this should be true of Jest as well.
Testing is a complex area of software development, and much ink has been spilled on how to properly manage test doubles. Hopefully this post demystifies this one aspect!