skip to content

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%

answer

  1. keyed by store id
  2. merged after creation
  3. arrays are replaced, objects merged
  4. getters are writable in tests
  5. undefined restores the getter

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.

solid answer

~40 s

`createTestingPinia({ initialState })` takes an object keyed by **store id**, the first argument of `defineStore()`, here `notifications`, not the `useNotificationsStore` name. When a store is created, the testing pinia merges that entry into `store.$state`: nested plain objects are merged, while arrays and other values are replaced, so a partial state is enough. For getters, the testing pinia makes them **writable**: `store.unreadCount = 9` forces the value, and it stays forced even when state changes, which lets me test the badge's `9+` rendering without building nine notices. Assigning `undefined` restores normal computation. Getters are typed readonly, so in TypeScript the assignment usually needs a cast. Outside a testing pinia, getters stay read-only.

code

ts · 19 lines
ts
import { nextTick } from 'vue'
import { mount } from '@vue/test-utils'
import { createTestingPinia } from '@pinia/testing'
import { expect, it, vi } from 'vitest'
import ToastBadge from '@/components/ToastBadge.vue'
import { useNotificationsStore } from '@/stores/notifications'

it('caps the badge at 9+', async () => {
  const wrapper = mount(ToastBadge, {
    global: { plugins: [createTestingPinia({ createSpy: vi.fn })] },
  })
  const store = useNotificationsStore()

  // @ts-expect-error: getters are typed readonly
  store.unreadCount = 12
  await nextTick()

  expect(wrapper.text()).toContain('9+')
})

go deeper

for a junior

Recall that createTestingPinia accepts initialState keyed by store id, and that getters can be assigned in tests.

for a middle

Explain that initialState is merged after the store is created, objects merged and arrays replaced, and how undefined restores a forced getter.

for a senior

Choose between seeding state and forcing getters, avoid a first render with default state, and handle the readonly getter typing.

for a principal

Guard against tests that override so much that they only test templates; agree when forcing derived values is acceptable.

## Two ways to arrange a store for a component test A component test often needs the store in a particular state before mounting: three unread notifications, an empty list, a badge that should read `9+`. `@pinia/testing` gives two tools for this: - **`initialState`** for state: a per-store partial state applied when each store is created. - **Writable getters** for derived values: a test can assign a getter's value directly. Both avoid running actions, which with the default stubs would not run anyway. ## initialState ```ts createTestingPinia({ createSpy: vi.fn, initialState: { notifications: { items: [ { id: 'n1', text: 'Saved', level: 'info', read: false }, { id: 'n2', text: 'Retrying upload', level: 'warning', read: false }, { id: 'n3', text: 'Sync failed', level: 'error', read: false }, ], }, }, }) ``` How it is applied: 1. The key is the **store id**, the string passed as the first argument of `defineStore()`. A key that matches no id is silently ignored. 2. When the store is created, its normal initial state is built first, from `state()` or the setup function's refs. 3. Then the testing pinia **merges** the entry into `store.$state`. Nested plain objects are merged key by key; arrays, `Date` values and other non-plain values are **replaced**. 4. The object configures every store on that pinia, so one `initialState` can seed several stores at once. Because it is a merge, you only write the fields the test cares about. Seeding after mounting with `store.$patch()` also works, but the component may already have rendered and run effects with the default state; `initialState` avoids that first render. ## Writable getters A getter is normally read-only: it is a cached computed value derived from state. Inside a testing pinia, getters become writable: ```ts const store = useNotificationsStore() store.unreadCount = 9 ``` - The forced value **sticks**: changing `items` does not recompute `unreadCount` while it is overridden. - Other getters that read `unreadCount` see the forced value. - Assigning **`undefined`** restores the original getter, which recomputes from state. - It works for option-store getters and for `computed()` in setup stores. - Getters are typed as read-only properties, so TypeScript usually needs a cast or an expected-error comment on the assignment. | Need | Tool | Scope | |---|---|---| | component starts with specific data | `initialState` | applied at store creation | | derived value hard to reach through data | assign the getter | from the assignment until reset | | change state during the test | `store.items = ...` or `store.$patch(...)` | immediate | ## Seeding several stores at once The toast component may read more than one store, for example a settings store that decides whether toasts auto-hide. `initialState` covers them all in one object: ```ts initialState: { notifications: { items: [/* three notices */] }, settings: { autoHideMs: 0 }, } ``` Each entry is applied when its store is created, whether the component or the test creates it first. That also makes cross-store getters testable: if `unreadCount` ignored muted levels read from the settings store, seeding both stores exercises the real combination, while forcing `unreadCount` would skip it. Two details are easy to miss: 1. `initialState` is read when a store is **created**. A store that already exists on that pinia does not pick up later changes to the object. 2. State written by `initialState` goes through the store's normal state, so a setup store's returned refs receive the values and its getters recompute from them. ## Which to prefer Prefer **state** when it is easy to build: three notices in `initialState` exercise both the component and the real `unreadCount` getter. Force the **getter** when building the state is laborious or irrelevant, for example the `9+` overflow rendering, or a getter that combines other stores. Overriding everything turns the test into a test of the template only. ## Common mistakes - Keying `initialState` by the composable name instead of the store id. - Expecting an array in `initialState` to be appended to the default array; it replaces it. - Forgetting that a forced getter ignores later state changes, then asserting a recomputed value. - Assigning getters on a regular pinia and expecting it to work outside tests.

  • initialState seeds items with three notices, but the store's default state also had one welcome notice; how many does the store hold?
    Three. `items` is an array, and the testing pinia replaces non-plain values instead of merging them, so the default array is swapped for the seeded one. Only nested plain objects are merged key by key.
  • After store.unreadCount = 12, the test pushes a notice; what does unreadCount return, and how do you get the real value back?
    It still returns 12: an overridden getter ignores state changes until reset. Assign `store.unreadCount = undefined` and the testing pinia restores the original computation, which then reflects the current `items`.

saying these in an interview costs you the question

  • initialState is keyed by the useNotificationsStore composable name
  • initialState replaces the entire store state, so every field must be given
  • Arrays in initialState are appended to the store's default arrays
  • A forced getter recomputes as soon as the state changes
  • Getters can be assigned on any pinia, not only a testing pinia