A Practical Guide to Mocking Svelte Stores with Vitest

I'm using Svelte stores in this post as an example, but the principle applies to any function or variable you want to mock with vitest, with any js framework.

Imagine this...

You've just refactored some complex code into a killer custom Svelte store that encapsulates some database calls and other logic. You can't wait to get this merged, but you know without tests the PR is getting sent straight back.

When it comes to unit testing some of your components that are using this new store, you only want to put the component under test, so you create a simple mock for your store.

Let's say your component uses a current project id...

// Doesn't work 🚫
const mockStore = writable<number>(1);

vi.mock('$lib/stores/projects', () => ({
        currentProjectId: mockStore
}));

test('first test' => {    
    // ...
})

test('second test' => {
    // change mock
    mockStore.set(5)
    // ...
})

However, vitest gives you an error.

If you are using "vi.mock" factory, make sure there are no top level variables inside, since this call is hoisted to top of the file.

All you've done is create a simple mock, right?

Before you start wondering if you can convince your teammates that there are legitimate reasons you cannot test this code, or you'll do the tests in a follow-up PR 🤨 let's look at how vitest is using mock and hoisting.

vi.mock

The mock function has some nuances - regardless of where you have created your mock, it is always hoisted to the top of the file.

JavaScript Hoisting refers to the process whereby the interpreter appears to move the declaration of functions, variables, classes, or imports to the top of their scope, prior to execution of the code.

https://developer.mozilla.org/en-US/docs/Glossary/Hoisting

Whether your mock function is inside a describe block, in the beforeAll or inside a test, it doesn't matter to vitest - it is getting sent to the top of the file.

This has some side effects you need to be aware of...

Re-mocking won't work

If you've not done much mocking before, you'd be forgiven for thinking something like this will work - re-calling the mock function at the beginning of each test.

// this test may fail due to conflicting mock 🚫
test('first test' => {
    vi.mock('$lib/stores/projects', () => ({
        currentProjectId: writable<number>(1),
    }));
    // ...
})


test('second test' => {
    vi.mock('$lib/stores/projects', () => ({
        currentProjectId: writable<number>(5),
    }));
    // ...
})

However, now that we know these mocks get sent to the "top" of the file, this can lead to some head-scratching as we now have two conflicting mocks in the scope.

In practice, you only want to mock once and change the result of the mock.

Local variables won't work

The next logical thing to do is to create a local store that we can modify before our tests run...

const mockStore = writable<number>(1);

vi.mock('$lib/stores/projects', () => ({
        currentProjectId: mockStore
}));

test('first test' => {    
    // ...
})

Now we're hit with the error we mentioned in the intro. As the mock is being hoisted and the mockStore is not, when the mock runs for the first time mockStore does not yet exist.

Fine, let's try moving the store to another file and import it.

import { mockStore } from './mocks'

vi.mock('$lib/stores/projects', () => ({
        currentProjectId: mockStore
}));

test('first test' => {    
    // ...
})

Nope, same problem, mock is being called before the import.

vi.hosting

All is not lost though, vi.hoisting saves the day by allowing us to wrap anything we may want to define locally in this function to hoist above the mocks.

For local variables, this is now a very simple fix to our code...

// Works! ✅
const mockStore = vi.hoisted(() => writable<number>(1));

vi.mock('$lib/stores/projects', () => ({
        currentProjectId: mockStore
}));

test('first test' => {    
    // ...
})


test('second test' => {
    // change mock
    mockStore.set(5)
    // ...
})

Awesome, but maybe you have a more complex mock and you prefer to have it in a separate file, how do we hoist the import above the mock?

Dynamic imports

You will used to be importing like this:

import { mockCurrentProjectStore } from './mockStores'

This is known as a static import, but there is another way known as, you probably guessed, a dynamic import.

const { mockCurrentProjectStore } = await import('./mockStores')

There are some small differences that are not relevant to our discussion here, check the MDN docs for more info, but for us, the key point is that we can pass this await import() expression to vi.hoisted to ensure that our import is available before our mock.

// Works! ✅
const { mockStore } = vi.hoisted(() => await import('./mockStores'))

vi.mock('$lib/stores/projects', () => ({
        currentProjectId: mockStore
}));

test('first test' => {    
    // ...
})

Summing up

In my experience understanding what's happening behind the scenes with mock and hoisting makes testing much faster, enjoyable and intuitive. And remember, the above applies to any variables or functions you are using to mock.

Additional Reading

Official docs - https://vitest.dev/api/vi#vi-mock

How does vitest do the hoisting? - https://github.com/vitest-dev/vitest/blob/main/packages/vitest/src/node/hoistMocks.ts

Dynamic imports - https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import