skip to content

Unit-Testing Store Logic

Store logic is tested against a fresh active pinia; components get a testing pinia with stubbed actions. Interviewers probe what stubActions misses in setup stores and how to seed state.

on this pageshow

explore

questions

5

How do you unit-test a Pinia notifications store's actions and getters, and why call setActivePinia(createPinia()) before each test?

level: juniorimportance: must knowfreq 50%

answer

  1. no component, no mount
  2. stores need an active pinia
  3. the pinia caches stores
  4. fresh instance in beforeEach
  5. plugins wait for an app

basics

~20 s

Call setActivePinia(createPinia()) in beforeEach, then call useNotificationsStore() directly and exercise real actions and getters. A fresh pinia per test gives a fresh store, because stores are cached per pinia and state would otherwise leak between tests.

solid answer

~40 s

A store is plain logic, so I test it without mounting anything: in `beforeEach` I call `setActivePinia(createPinia())`, and inside each test `useNotificationsStore()` picks that pinia up without needing an app. Then I call real actions like `push()` and `dismiss()` and assert on state and getters such as `unreadCount`. The fresh pinia matters because the store is created once per pinia and cached: reusing one pinia would carry the first test's notifications into the next. One catch: store plugins only run once a pinia is installed in an app, so if the store depends on a plugin I also install the pinia into an empty `createApp({})`, or use the testing helper's `plugins` option.

code

ts · 18 lines
ts
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'

export interface Notice { id: string; text: string; level: 'info' | 'warning' | 'error'; read: boolean }

export const useNotificationsStore = defineStore('notifications', () => {
  const items = ref<Notice[]>([])
  const unreadCount = computed(() => items.value.filter((n) => !n.read).length)

  function push(text: string, level: Notice['level'] = 'info') {
    items.value.push({ id: crypto.randomUUID(), text, level, read: false })
  }
  function dismiss(id: string) {
    items.value = items.value.filter((n) => n.id !== id)
  }

  return { items, unreadCount, push, dismiss }
})

go deeper

for a junior

Recall the pattern: setActivePinia(createPinia()) in beforeEach, then call the store and its real actions and getters directly.

for a middle

Explain that the pinia caches stores by id, so a shared pinia leaks state between tests, and why no component is needed.

for a senior

Handle the edges: plugins that need an installed pinia, async actions with mocked dependencies, and why $reset() is not a substitute.

for a principal

Decide how much logic lives in stores versus components, since store logic is the cheapest layer to test thoroughly.

## What a store unit test needs A Pinia store is logic: state, getters derived from it and actions that change it. Testing it does not require a component, a DOM or Vue Test Utils. What it does require is a **pinia**, the instance created by `createPinia()`, because `useNotificationsStore()` must find one to create and cache the store in. In an app, `app.use(pinia)` provides it. In a unit test there is no app, so you make one pinia **active** yourself: ```ts import { beforeEach, describe, expect, it } from 'vitest' import { createPinia, setActivePinia } from 'pinia' import { useNotificationsStore } from '@/stores/notifications' describe('notifications store', () => { beforeEach(() => { setActivePinia(createPinia()) }) it('counts unread notices', () => { const store = useNotificationsStore() store.push('Saved') store.push('Disk almost full', 'warning') expect(store.unreadCount).toBe(2) }) it('starts empty in every test', () => { expect(useNotificationsStore().items).toEqual([]) }) }) ``` `setActivePinia()` sets the pinia that store calls fall back to when they are not inside a component, so `useNotificationsStore()` needs no argument. ## Why a fresh pinia before each test The pinia **caches** each store by id: the first `useNotificationsStore()` creates it, later calls return the same object. If one pinia were shared by the whole file: 1. Test A pushes two notices. 2. Test B calls `useNotificationsStore()` and receives the same cached store, still holding A's notices. 3. Test B's assertions now depend on test order. Creating a new pinia in `beforeEach` gives every test a new cache and therefore a new store built from its initial state. That is cheaper and more reliable than resetting stores by hand. | Setup | Isolation | Notes | |---|---|---| | `setActivePinia(createPinia())` in `beforeEach` | full: new store per test | the documented pattern | | one pinia for the file, `store.$reset()` after each test | partial | `$reset()` exists only on option stores | | no active pinia | none | the store call fails with the no-active-Pinia error | For long suites, `disposePinia(pinia)` also exists: it stops the pinia's effect scope and clears its stores, plugins and state, which is useful when tests create many pinias or run effects. ## What to assert - **Actions**: call them with real arguments and check the resulting state, including edge cases (dismissing an unknown id, pushing an empty message). - **Getters**: arrange state through actions or `$patch()`, then read the getter; a getter is cached, so check it updates when its inputs change. - **Async actions**: stub the network layer (the API module the store imports), `await` the action and assert on state. Test the **real** actions here. Stubbing a store's own actions in its own unit test would test nothing. ## When the store depends on a plugin Plugins added with `pinia.use()` are held back until the pinia is installed in an app. With only `setActivePinia(createPinia().use(plugin))`, the plugin never runs and the store behaves differently from production. The docs fix is an empty app: ```ts const app = createApp({}) beforeEach(() => { const pinia = createPinia().use(timestampPlugin) app.use(pinia) setActivePinia(pinia) }) ``` The testing helper package offers a `plugins` option for the same need. ## Store tests versus component tests The notifications feature has two halves, and each has its natural test: | Test | Pinia setup | What it proves | |---|---|---| | store unit test | `setActivePinia(createPinia())` | `push`, `dismiss` and `unreadCount` behave correctly | | toast component test | a testing pinia with stubbed actions | the toast renders the store's state and calls the right actions | Keeping the logic in the store and the rendering in the component means most behaviour is covered by the fast, simple store tests, and component tests stay short. A store test also runs without a DOM environment unless the store itself touches browser APIs. ## Arranging state without actions Sometimes a test needs a state that is awkward to reach through actions, for example a notice that is already marked read. Two options keep the test honest: - write state directly (`store.items = [...]`) or with `store.$patch({ items: [...] })` before acting; - or add a small action to the store if the state is a legitimate case the app also needs. Avoid reaching into private implementation details; the store's public state, getters and actions are its contract. ## Common mistakes - Creating the pinia once at the top of the file and wondering why tests pass alone but fail together. - Mounting a component just to test store logic. - Forgetting that module-level variables in the store file are not reset by a new pinia. - Expecting plugins to run on a pinia that no app has installed.

  • Would calling store.$reset() in afterEach be an equivalent way to isolate tests of this notifications store?
    No. It is a setup store, and setup stores have no built-in `$reset()`; in development the default throws. Even for option stores, `$reset()` only restores `state()` and leaves anything else in the store file untouched. A new pinia per test is simpler and isolates everything the pinia holds.
  • How do you test an async action of the notifications store that loads notices from an API?
    Keep the real action and fake its dependency: mock the API module the store imports (with your test runner's module mocking), `await store.load()`, then assert on `items` and any loading or error state. Stubbing the action itself would skip the logic you are trying to test.

saying these in an interview costs you the question

  • Store logic can only be tested by mounting a component
  • One pinia per test file is enough because stores reset themselves
  • setActivePinia() is only needed for server-side rendering
  • Plugins added with pinia.use() run even if no app installs the pinia
  • Store actions should be stubbed in the store's own unit tests
open as a page

When a toast component test mounts with createTestingPinia() from @pinia/testing, what happens to the notifications store's actions, and how do you assert on them?

level: middleimportance: must knowfreq 44%

basics

~10 s

By default createTestingPinia() replaces every action with a stub spy, so calls are recorded but the real code never runs. Get the store with useNotificationsStore() in the test and assert with expect(store.dismiss).toHaveBeenCalledWith(id).

open as a page

With @pinia/testing, how do you start a toast component test with three unread notifications and force the notifications store's unreadCount getter?

level: seniorimportance: should knowfreq 33%

basics

~20 s

Pass initialState: { notifications: { items: [...] } } to createTestingPinia, keyed by the store id; it is merged into the store when created. Force a getter by assigning store.unreadCount = 9, and assign undefined to restore it.

open as a page

In a Pinia setup store, notifyError() calls push() directly; why does a test's stub of push never see that call, and what can you do?

level: seniorimportance: should knowfreq 27%

basics

~20 s

createTestingPinia replaces actions on the store object, but inside a setup store notifyError() calls the local push function it closed over, not store.push. The real push runs; option stores avoid this because this.push goes through the store.

open as a page

A Pinia notifications store depends on a store plugin; how do you make that plugin apply in tests, and why not call testingPinia.use()?

level: seniorimportance: nice to knowfreq 21%

basics

~20 s

Plugins added with pinia.use() only run once the pinia is installed in an app. For a plain pinia, install it into createApp({}); with createTestingPinia, pass the plugin in its plugins option so it runs before the action stubs.

open as a page