A healthy store is a testable store. This episode covers createTestingPinia for a mock pinia, resetting state between tests, mocking actions and APIs with vi.fn and MSW, testing getters, and component testing with Vue Test Utils and Vitest.

Code without tests is a time bomb — especially state management used by many components. To answer this need, the Pinia team provides @pinia/testing: a utility for creating a mock pinia that makes testing stores and components easy.
Episode 17 covers Pinia testing thoroughly: createTestingPinia, resetting state between tests, mocking actions and APIs, testing getters, and component testing with Vue Test Utils and Vitest.
Install the testing tools first:
npm i -D vitest @vue/test-utils @pinia/testingFor store unit tests, enable a mock pinia with createTestingPinia:
import { setActivePinia } from 'pinia'
import { createTestingPinia } from '@pinia/testing'
import { beforeEach } from 'vitest'
beforeEach(() => {
setActivePinia(createTestingPinia({ createSpy: vi.fn }))
})createTestingPinia({ createSpy: vi.fn }) creates a pinia where every action automatically becomes a spy. In each test, setActivePinia guarantees clean state — no leakage between tests.
Because actions become spies, you can check calls without running the real side effects:
import { useCounterStore } from '@/stores/counter'
import { createTestingPinia } from '@pinia/testing'
it('calls the increment action', () => {
const pinia = createTestingPinia({ createSpy: vi.fn })
const store = useCounterStore(pinia)
store.increment()
expect(store.increment).toHaveBeenCalledTimes(1)
})useCounterStore(pinia) explicitly uses the mock pinia instance. The increment action doesn't actually add to state — it becomes a spy, so the test focuses on call behavior.
To test logic that depends on specific state, pass initialState:
const pinia = createTestingPinia({
initialState: {
cart: { items: [{ name: 'Coffee', price: 15000, qty: 2 }] },
},
})
const store = useCartStore(pinia)
expect(store.totalPrice).toBe(30000)initialState: { cart: {...} } fills the cart store's state before the test runs. This is the most convenient way to test getters with prepared data.
For async actions, mock fetch so the network is never touched:
it('fills the profile from the API', async () => {
vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
ok: true,
json: () => Promise.resolve({ name: 'Arman' }),
}))
const store = useUserStore()
await store.fetchProfile()
expect(store.profile?.name).toBe('Arman')
})vi.stubGlobal('fetch', ...) replaces the global fetch with a mock. For larger projects, use MSW to handle many endpoints realistically.
Finally, test components that use a store with Vue Test Utils:
import { mount } from '@vue/test-utils'
import CounterView from '@/components/CounterView.vue'
it('renders the value from the store', () => {
const wrapper = mount(CounterView, {
global: { plugins: [createTestingPinia()] },
})
expect(wrapper.text()).toContain('Value: 0')
})global: { plugins: [createTestingPinia()] } injects the mock pinia into every component under test. The component runs exactly like in production, but all store actions are controlled.
Tip
Focus tests on behavior: action calls, getter results, and what's rendered. Don't test Vue implementation details that the framework already handles.
Episode 17 equips you with complete testing tooling. You can now create a mock pinia with createTestingPinia, reset state between tests, initialize state, mock APIs and actions, and do component testing.
Key takeaways:
createTestingPinia({ createSpy: vi.fn }) turns actions into spies.setActivePinia in beforeEach keeps state clean.initialState fills initial state for testing getters.vi.fn or MSW for async actions.global.plugins in Vue Test Utils.In the next episode, episode 18, we'll discuss custom plugins and advanced lifecycle — using $onAction for tracking, subscribes that write to JSON, and global plugins for analytics and error handlers. This refines the plugin capabilities from episode 8.