N
Nuxt4mo ago
miracoly

Mocking Pinia Store in Vitest Component Test

I would like to test my Nuxt3 component with vitest and nuxt/test-utils. This component uses a Pinia store. Component (simplified):
<template>
<Foo :leagues="allLeagues" />
</template>
<script setup lang="ts">
const leagueStore = useLeagueStore();
const { retrieveAllLeagues } = leagueStore;
const { allLeagues } = storeToRefs(leagueStore);

await callOnce(retrieveAllLeagues);
</script>
<template>
<Foo :leagues="allLeagues" />
</template>
<script setup lang="ts">
const leagueStore = useLeagueStore();
const { retrieveAllLeagues } = leagueStore;
const { allLeagues } = storeToRefs(leagueStore);

await callOnce(retrieveAllLeagues);
</script>
Store (simplified):
import { useBackend } from '~/composable/useBackend';
import { type League } from '~/utils/models/league';
import type { ComputedRef } from 'vue';

export const useLeagueStore = defineStore('leagueStore', () => {
const allLeagues = ref<League[]>();

const retrieveAllLeagues = async (): Promise<void> => {
const { data } = await useBackend<League[]>('/v1/leagues');
allLeagues.value = data.value ?? undefined;
};

return {
allLeagues,
retrieveAllLeagues,
};
});
import { useBackend } from '~/composable/useBackend';
import { type League } from '~/utils/models/league';
import type { ComputedRef } from 'vue';

export const useLeagueStore = defineStore('leagueStore', () => {
const allLeagues = ref<League[]>();

const retrieveAllLeagues = async (): Promise<void> => {
const { data } = await useBackend<League[]>('/v1/leagues');
allLeagues.value = data.value ?? undefined;
};

return {
allLeagues,
retrieveAllLeagues,
};
});
I've now tried to test it as described on the Pina page about testing. Test:
import { describe, expect, test } from 'vitest';
import { renderSuspended } from '@nuxt/test-utils/runtime';
import { screen, within } from '@testing-library/vue';
import { createTestingPinia } from '@pinia/testing';

describe('foo', () => {
const { _1, _2, _3 } = leagues;

test('bar', async () => {
const testingPinia = createTestingPinia();
const store = useLeagueStore(testingPinia);
store.allLeagues = [_1, _2, _3];
await renderSuspended(DashboardPage, {
global: { plugins: [testingPinia] },
});

expect(store.retrieveAllLeagues).toHaveBeenCalledOnce();

[_1, _2, _3].forEach(league => {
expect(screen.getByText(league.title)).toBeVisible();
});
});
});
import { describe, expect, test } from 'vitest';
import { renderSuspended } from '@nuxt/test-utils/runtime';
import { screen, within } from '@testing-library/vue';
import { createTestingPinia } from '@pinia/testing';

describe('foo', () => {
const { _1, _2, _3 } = leagues;

test('bar', async () => {
const testingPinia = createTestingPinia();
const store = useLeagueStore(testingPinia);
store.allLeagues = [_1, _2, _3];
await renderSuspended(DashboardPage, {
global: { plugins: [testingPinia] },
});

expect(store.retrieveAllLeagues).toHaveBeenCalledOnce();

[_1, _2, _3].forEach(league => {
expect(screen.getByText(league.title)).toBeVisible();
});
});
});
Both assertions fail, the first one with AssertionError: expected "spy" to be called once, but got 0 times. Its like my component does not pick up the testing Pinia instance for some reason.
1 Reply
miracoly
miracoly3mo ago
Thanks for the idea! I was able to mock it with mockNuxtImport() and vitest's mock functionality.
import { beforeEach, describe, expect, test, vi } from 'vitest';
import { mockNuxtImport, renderSuspended } from '@nuxt/test-utils/runtime';
import { screen } from '@testing-library/vue';

const { useLeagueStoreMock, retrieveAllLeagues } = vi.hoisted(
() => ({
useLeagueStoreMock: vi.fn<any, Partial<LeagueStoreTree>>(),
retrieveAllLeagues: vi.fn(),
}),
);

retrieveAllLeagues.mockImplementation(async (): Promise<void> => {
await Promise.resolve();
});

mockNuxtImport('useLeagueStore', () => useLeagueStoreMock);

beforeEach(() => {
useLeagueStoreMock.mockRestore();
});

describe('foo', () => {
beforeEach(async () => {
useLeagueStoreMock.mockImplementation(() => ({
allLeagues: computed(() => [...]),
retrieveAllLeagues,
}));
});

test('initializes allLeagues state', async () => {
await renderSuspended(DashboardPage);
...
});
import { beforeEach, describe, expect, test, vi } from 'vitest';
import { mockNuxtImport, renderSuspended } from '@nuxt/test-utils/runtime';
import { screen } from '@testing-library/vue';

const { useLeagueStoreMock, retrieveAllLeagues } = vi.hoisted(
() => ({
useLeagueStoreMock: vi.fn<any, Partial<LeagueStoreTree>>(),
retrieveAllLeagues: vi.fn(),
}),
);

retrieveAllLeagues.mockImplementation(async (): Promise<void> => {
await Promise.resolve();
});

mockNuxtImport('useLeagueStore', () => useLeagueStoreMock);

beforeEach(() => {
useLeagueStoreMock.mockRestore();
});

describe('foo', () => {
beforeEach(async () => {
useLeagueStoreMock.mockImplementation(() => ({
allLeagues: computed(() => [...]),
retrieveAllLeagues,
}));
});

test('initializes allLeagues state', async () => {
await renderSuspended(DashboardPage);
...
});
That does the trick. In my component, I utilize callOnce, and it appears to truly execute only once within a single test file. That's why retrieveAllLeagues isn't restored with mockRestore(). I suppose if someone wants to ensure the correct methods are called with callOnce, they either need to assert them in the proper sequence or employ multiple test files.