skip to content

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%

answer

  1. plugins wait for installation
  2. an empty app for plain pinias
  3. the plugins option
  4. fakeApp installs one for you
  5. stubbing must run last

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.

solid answer

~40 s

`pinia.use(plugin)` holds the plugin back until `app.use(pinia)` installs the pinia; a unit test with just `setActivePinia(createPinia())` never installs it, so a plugin that adds, say, a `createdAt` timestamp to each notice silently does nothing. With a plain pinia I install it into an empty `createApp({})` in the test setup. With `createTestingPinia`, I pass `plugins: [timestampPlugin]`: those are pushed straight into the pinia's plugin list, **before** the testing pinia's own action-stubbing step, so they run immediately and the stubs wrap whatever they add. Calling `testingPinia.use(plugin)` instead queues it until installation, so it does not run without an app, and when it does, it runs after the stubs and can undo them. `fakeApp: true` installs the testing pinia into an empty app when a test needs `app`-level behaviour.

code

ts · 19 lines
ts
import { createApp } from 'vue'
import { createPinia, setActivePinia } from 'pinia'
import { beforeEach, expect, it } from 'vitest'
import { timestampPlugin } from '@/stores/plugins/timestamp'
import { useNotificationsStore } from '@/stores/notifications'

const app = createApp({})

beforeEach(() => {
  const pinia = createPinia().use(timestampPlugin)
  app.use(pinia)
  setActivePinia(pinia)
})

it('stamps new notices', () => {
  const store = useNotificationsStore()
  store.push('Saved')
  expect(store.items[0].createdAt).toBeTypeOf('number')
})

go deeper

for a junior

Recall that Pinia plugins only run once the pinia is installed in an app, which plain store tests do not do by default.

for a middle

Explain the two fixes: an empty createApp({}) for plain pinias, and the plugins option of createTestingPinia for component tests.

for a senior

Explain the ordering: plugins before stubs so spies wrap plugin changes, why use() breaks that order, and when fakeApp is needed.

for a principal

Make test setup match production setup: one shared helper that creates the pinia with the app's real plugins for every suite.

## Why plugins go missing in tests A Pinia **plugin** is a function registered with `pinia.use()` that runs for every store as it is created. `pinia.use()` has one rule that matters for tests: if the pinia has not been installed in an app with `app.use(pinia)` yet, the plugin is **queued** and only activated during installation. In an application that is invisible, because `main.ts` installs the pinia before anything uses a store. In a unit test the usual setup is `setActivePinia(createPinia().use(timestampPlugin))`, and nothing ever installs that pinia. The plugin never runs, the store behaves differently from production, and tests may pass for the wrong reason or fail with confusing errors (a missing property the plugin was supposed to add). ## Store unit tests: install into an empty app The Pinia docs solve it with a throwaway app: ```ts const app = createApp({}) beforeEach(() => { const pinia = createPinia().use(timestampPlugin) app.use(pinia) setActivePinia(pinia) }) ``` One empty app can be reused across tests; each test still gets a fresh pinia. ## Component tests: the plugins option `createTestingPinia()` takes a `plugins` array: ```ts createTestingPinia({ createSpy: vi.fn, plugins: [timestampPlugin] }) ``` These plugins are pushed **directly** into the pinia's active plugin list, skipping the wait for installation. The order in which the testing pinia runs its steps for each new store is: 1. merge `initialState` into the store; 2. run the plugins from the `plugins` option; 3. make getters writable; 4. replace actions with spies and wrap `$patch`/`$reset`. Running the stubbing step **last** matters: if a plugin wraps or adds actions, the spies are applied on top, and the test's assertions on `store.someAction` see every call. ## Why not testingPinia.use() The docs say explicitly not to add plugins with `testingPinia.use(MyPlugin)`. A testing pinia is a pinia, so `use()` follows the normal rule: | How the plugin is added | Runs without an app? | Order relative to stubs | |---|---|---| | `plugins: [p]` option | yes | before the stubs | | `testingPinia.use(p)`, then mounted | only after installation | after the stubs, can replace spied actions | | `testingPinia.use(p)`, never installed | no | never runs | A plugin that returns wrapped actions after the stubs have been installed replaces the spies, so `expect(store.push).toHaveBeenCalled()` inspects a function that is no longer on the store. ## fakeApp `createTestingPinia({ fakeApp: true })` creates an empty app and installs the testing pinia into it. Use it when a store test (without mounting a component) relies on something that exists only after installation, such as plugins added through `use()` or the `app` in a plugin's context. When the testing pinia is passed to a mounted component, the component's app installs it anyway. ## A shared helper keeps tests honest When the application installs several Pinia plugins, the simplest way to keep tests consistent with production is one helper that every suite uses: ```ts export const appPiniaPlugins = [timestampPlugin, auditPlugin] export function createStoreTestPinia() { const pinia = createPinia() appPiniaPlugins.forEach((p) => pinia.use(p)) createApp({}).use(pinia) setActivePinia(pinia) return pinia } ``` Component tests then pass the same list: `createTestingPinia({ createSpy: vi.fn, plugins: appPiniaPlugins })`. The production `main.ts` registers the same array, so a plugin added to the app cannot be forgotten in tests. Plugins with side effects, such as sending audit entries, should read their dependencies from modules the tests can fake, rather than being left out of the list. ## Related options worth knowing - `stubPatch: true` makes `$patch()` a no-op spy; `stubReset: true` does the same for `$reset()`. - Plugins run for stores created **after** they are active, so create the testing pinia before calling any store. - `initialState` is applied before user plugins run, so a plugin that reads state at creation (for example to restore saved values) sees the seeded state. - A plugin that adds state to every store runs after the `initialState` merge, so unless it uses the documented `Object.hasOwn(store.$state, key)` guard, it overwrites a value the test seeded for that key. ## Symptoms of a missing plugin When a test passes locally in isolation but the feature fails in the app, or a test fails with a property that "should exist", check whether the plugin actually ran: a property the plugin returns is `undefined` on the store, and actions it wraps behave unwrapped. A one-line assertion on a plugin-provided property in a shared setup test catches the whole class of problem early. ## Common mistakes - Relying on a plugin in a store test with a pinia that no app installed. - Calling `testingPinia.use()` and asserting on spies the plugin later replaced. - Mounting with a plain `createPinia()` in some tests and a testing pinia in others, and getting different plugin behaviour.

  • Why does createTestingPinia push the plugins option straight into the plugin list instead of calling use()?
    To control order and timing. Pushing directly makes the plugins active immediately, without waiting for installation, and places them before the action-stubbing step, which the testing pinia adds last so that its spies wrap everything plugins added or changed.
  • When is fakeApp: true useful if the testing pinia is already passed to mount()?
    Mostly it is not: mounting installs the pinia in the component's app. `fakeApp` helps in store-only tests that use a testing pinia without mounting anything, when something needs the pinia to be installed, such as plugins added through `use()` or code that reads the app from the plugin context.

saying these in an interview costs you the question

  • setActivePinia() installs the pinia, so plugins run in plain store tests
  • testingPinia.use() is the documented way to add plugins in component tests
  • Plugins in the plugins option run after the action stubs are applied
  • fakeApp: true is required whenever a component is mounted
  • A plugin added after a store exists is applied to that store as well