With @pinia/testing, how do you start a toast component test with three unread notifications and force the notifications store's unreadCount getter?
answer
- keyed by store id
- merged after creation
- arrays are replaced, objects merged
- getters are writable in tests
- undefined restores the getter
basics
~20 sPass 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 linesimport { 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
Recall that createTestingPinia accepts initialState keyed by store id, and that getters can be assigned in tests.
Explain that initialState is merged after the store is created, objects merged and arrays replaced, and how undefined restores a forced getter.
Choose between seeding state and forcing getters, avoid a first render with default state, and handle the readonly getter typing.
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