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?
answer
- actions become spies
- stubbed means the body never runs
- stubActions accepts more than a boolean
- createSpy is vi.fn or jest.fn
- the testing pinia is already active
basics
~10 sBy 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).
solid answer
~40 s`createTestingPinia()` returns a pinia for **component** tests. Every store created on it has its actions replaced by spies made with `createSpy`, which defaults to `jest.fn` or, with Vitest globals, `vi.fn`; otherwise you pass `createSpy: vi.fn` (the function, not `vi.fn()`). With the default `stubActions: true` the spies are empty, so clicking the toast's close button records `dismiss('n1')` but leaves `items` unchanged. `$patch` and `$reset` are wrapped in spies too, and `stubPatch`/`stubReset` turn them into no-ops. The testing pinia also becomes the active pinia, so after mounting with it `useNotificationsStore()` in the test returns the same store the component uses, and I assert `expect(store.dismiss).toHaveBeenCalledWith('n1')`. `stubActions: false` runs real actions while still spying; an array or a function stubs selectively.
code
ts · 24 linesimport { mount } from '@vue/test-utils'
import { createTestingPinia } from '@pinia/testing'
import { expect, it, vi } from 'vitest'
import ToastList from '@/components/ToastList.vue'
import { useNotificationsStore } from '@/stores/notifications'
it('asks the store to dismiss a toast', async () => {
const wrapper = mount(ToastList, {
global: {
plugins: [createTestingPinia({
createSpy: vi.fn,
initialState: {
notifications: { items: [{ id: 'n1', text: 'Saved', level: 'info', read: false }] },
},
})],
},
})
const store = useNotificationsStore()
await wrapper.get('[aria-label="Dismiss"]').trigger('click')
expect(store.dismiss).toHaveBeenCalledWith('n1')
expect(store.items).toHaveLength(1)
})go deeper
Recall that createTestingPinia() from @pinia/testing stubs store actions by default, and that you read the store in the test with the same useStore().
Explain what stubbing means (spies with no body, state unchanged), the stubActions forms, and why createSpy needs vi.fn or jest.fn.
Anticipate the traps: stubs returning undefined for async actions, typing spies, $patch and $reset spies, and setup-store internal calls bypassing stubs.
Set the team's split between isolated component tests with stubbed stores and integration tests with real ones, so each behaviour is tested once.
## What createTestingPinia is for `@pinia/testing` exports `createTestingPinia()`, a pinia built for **unit-testing components** that use stores. The idea is to test the component and the store separately: the component test checks that the component calls the right actions with the right arguments and renders the store's state; the store's own unit tests check what the actions do. Install it as a dev dependency. `@pinia/testing` 2.0 is ESM only and expects `pinia` 4.0.2 or later. ## What it does by default 1. It creates a pinia and makes it the **active pinia**, so store calls in the test body find it. 2. For every store created on it, it replaces each action with a **spy**. With `stubActions: true` (the default) the spy has no implementation: the action body never runs and the spy returns `undefined`. 3. It wraps `$patch()` and `$reset()` in spies as well. They still work unless `stubPatch: true` or `stubReset: true` makes them do nothing. 4. It makes getters writable, so a test can force a getter's value. ```ts const wrapper = mount(ToastList, { global: { plugins: [createTestingPinia({ createSpy: vi.fn })] }, }) const store = useNotificationsStore() ``` Passing the testing pinia to the mount installs it into the component's app (the mounting options themselves belong to Vue Test Utils). Because the testing pinia is also active, `useNotificationsStore()` in the test returns the same store instance the component uses. ## Asserting on actions ```ts await wrapper.get('[aria-label="Dismiss"]').trigger('click') expect(store.dismiss).toHaveBeenCalledWith('n1') expect(store.items).toHaveLength(1) ``` The second assertion documents the stub: `dismiss` was called but did not run, so the notice is still there. That is the point; whether `dismiss` removes the notice belongs to the store's unit test. ## Choosing what to stub | `stubActions` value | Effect | |---|---| | `true` (default) | every action is an empty spy | | `false` | every action runs for real, wrapped in a spy | | `['dismiss']` | only the named actions are stubbed; the rest run and are spied | | `(name, store) => boolean` | decided per action, once, when the store is set up | Use `false` for an integration-style component test where the store's behaviour matters, and the array or function forms when one action talks to the network and the others are pure. ## createSpy `createTestingPinia()` needs a spy factory. It uses `jest.fn` when Jest's global is present, or `vi.fn` when Vitest runs with `globals: true`. Otherwise it throws the coded error `PINIA_TESTING_C0001`, and you pass one explicitly: `createTestingPinia({ createSpy: vi.fn })`. Passing a called spy such as `vi.fn()` instead of the function throws `PINIA_TESTING_C0002`. ## Typing the spies Spied actions are real spies at runtime but keep their original types, so `store.load.mockResolvedValue(...)` is a type error. The Pinia docs show a small helper, `mockedStore(useStore)`, that maps each action's type to the test framework's mock type; with it, `mockedStore(useNotificationsStore).load.mockResolvedValue(undefined)` is typed. For one-off cases a cast works too. `vi.spyOn(store, 'load')` is an alternative when a test wants to mock a single action on a store that was not stubbed. ## When not to stub Stubbing isolates the component, but it also means the test cannot catch a mismatch between what the component expects and what the action really does. Two habits keep that risk small: 1. Cover each action in the store's own unit tests, so stubbing it elsewhere is safe. 2. Keep a few integration-style component tests with `stubActions: false` for the critical paths, such as a toast that must disappear when dismissed. ## Things that surprise people - An empty spy returns `undefined`. A component that does `store.load().then(...)` crashes under the default stubs; give the spy a resolved value first, or turn stubbing off for that action. - Spied actions are still typed as the original actions, so calling `.mockResolvedValue()` needs a typed helper or a cast. - In a setup store, an action that calls another action through its local variable bypasses the spy on the store; option stores calling through `this` do not have that gap. - A setup store's own `$reset` function is left to `stubReset`, not `stubActions`. ## Common mistakes - Asserting that state changed after a stubbed action ran. - Creating a second, plain pinia in the test and asserting against a different store. - Passing `vi.fn()` instead of `vi.fn`.
- The toast component calls store.load().then(showFirst); why does its test crash under the default createTestingPinia() settings?With `stubActions: true`, `load` is an empty spy that returns `undefined`, not a promise, so `.then` is called on `undefined`. Configure the spy before mounting, for example `store.load.mockResolvedValue(undefined)` via a typed helper, or leave `load` unstubbed with the array or function form of `stubActions`.
- What does stubPatch: true change in a component test?It turns `store.$patch()` into a spy with no implementation, so a component that updates the store through `$patch` records the call without changing state. Without it, `$patch` is still spied but applies the patch normally. `stubReset` does the same for `$reset()`.
saying these in an interview costs you the question
- createTestingPinia runs the real actions by default and only records calls
- A stubbed async action returns a resolved promise
- createSpy should be passed as vi.fn() so each store gets its own spy
- The test must create its own pinia to read the component's store
- stubActions only accepts true or false