Mocking Svelte Stores in Vitest
Say you have an application built with Svelte and SvelteKit (because of course you do, that’s why you’re here). And let’s say that application is hooked up to a database or an external API in a clever way using custom Svelte stores. Because of this, you can access the data from that store
inside of Svelte components quite easily like so:
<script>
import { customStore } from '$stores/myCustomStores';
</script>
{#await customStore.init()}
... waiting
{:then}
{#each $customStore as value}
{value}
{/each}
{/await}
As an awesome developer who practices Test-Driven Development (TDD), you recognize that using a store
directly in your component introduces an external dependency. Therefore, any tests for this particular component will now have to take this store
into consideration.
Instead, you could pass the value of the store
to the component as a prop and only iterate on the prop values. This has the benefit of keeping tests for this particular component simple:
<script>
export let storeVals = [];
</script>
{#each storeVals as value}
{value}
{/each}
import { it, expect } from 'vitest';
import { render, screen } from '@testing-library/svelte';
import CustomComponent from './CustomComponent.svelte';
it('shows the provided vales', () => {
render(CustomComponent, {storeVals: [1, 2, 3, 4]});
expect(screen.getByText('1')).toBeVisible();
expect(screen.getByText('2')).toBeVisible();
// ...
});
Of course, this solution comes with its own drawbacks. For instance, what if this component is nested really, really, really deeply within other components? This particular component may use the values from the store
but its ancestors/parent components do not necessarily need or make use of those values. In this scenario, we could potentially be passing the prop data through a half dozen other components, none of which have any use for the data! Furthermore, we’ve simply moved the problem to another component and its respective tests!
Instead of passing the store values as props, let’s leave the example code as is and refactor our tests so that they accommodate the logic surrounding our store
. Of course, if we were practicing True TDD ™, we would have started with the tests and then changed the code. But Svelte stores
are so simple and easy to work with that this seems like one of those edge case we can make an exception for 😉.
By the way, if you’re looking for an effective methodology for testing your Svelte applications, consider Daniel Irvine’s book, Svelte with Test-Driven Development. As far as I know, it’s the only comprehensive resource specific to testing SvelteKit applications. My own testing strategy is derived from its contents so everything in this post is based heavily on the resources provided from that text.
The Custom Store
Before we address how to mock the store, we should see what it looks like. In this case, let’s assume that our custom store is built around Svelte’s writable
store, though it could just as easily be readable
or derived
. What matters is that its customized and intended to be used throughout Svelte components and pages. In this instance, the store looks like so:
import { writable } from 'svelte/store';
const store = writable({});
export const customStore = {
...store,
init: async () => {
// connect to DB and populate store with DB data
},
set: async (newVal) => {
// add a new value to the DB
}
}
The store is identical to a standard Svelte writable store
except for two key differences. The first being that it has a new method attached to it which is an asynchronous method called to populate the store with data from the database; init()
. The second difference is that the provided set()
method (from writable
) is overwritten with custom logic for updating the values in the database. Basically, when values are assigned to the store
, the database is updated. And really, that’s all there is to it!
Mocking the Store
Now that you’ve seen the customStore
object, refresh your memory with how the component we’re trying to test looks:
<script>
import { customStore } from '$stores/myCustomStores';
</script>
{#await customStore.init()}
... waiting
{:then}
{#each $customStore as value}
{value}
{/each}
{/await}
To keep the example component testable, we’ll need to create a mock of the dependency we’ve introduced. Fortunately, this is very simple when using Vitest. We can simply include import { vi } from 'vitest'
at the top of the tests and then we’re safe to start using [vi.mock()](https://vitest.dev/api/vi.html#vi-mock)
.
import { it, expect, vi, afterEach } from 'vitest';
import { render, screen } from '@testing-library/svelte';
import CustomComponent from './CustomComponent.svelte';
it('shows the provided vales', () => {
render(CustomComponent);
expect(screen.getByText('1')).toBeVisible();
expect(screen.getByText('2')).toBeVisible();
// ...
});
vi.mock('$stores/myCustomStores', async () => {
const mockValues = [1, 2, 3, 4];
const { writable } = await import('svelte/store');
return {
customStore: {
...writable(mockValues),
init: vi.fn()
}
};
});
afterEach(() => {
vi.restoreAllMocks();
});
This test is nearly identical to the test which assumed we would pass values via props directly to the component in the render()
function. Of course in this test, that’s no longer necessary because we’re running the test with a mocked version of our custom store, which is seen at the bottom of the test. And because vi.mock()
is hoisted, it doesn’t matter where in the test it appears; it will always appear at the top of the scope prior to execution of the code.
To expand on this mock and how it works, some default values are added to the mockValues
constant. We then import writable
from svelte/store
and provide those default values to writable()
to initialize the store. Any extra methods attached to the store are then mocked by using [vi.fn()](https://vitest.dev/api/vi.html#vi-fn)
. Essentially, we’re overwriting store used by the component with a completely different one. In this case, another store that isn’t hooked up to a database or API. Lastly, make sure to call vi.restoreAllMocks()
to ensure this test doesn’t conflict with any other tests.
Resources
And just like that, our tests should be passing again! I stumbled around with various resources all over the internet before I figured out what I was doing. Maybe some of them will be helpful to you! If you’re looking to ensure your Svelte application is reliable, consider checking out Daniel Irvine’s book.