verifying actions, getters, and store interactions cleanly
Testing Pinia stores in isolation takes more than instantiating a store and calling an action. Between createPinia and createTestingPinia, real versus mocked actions, and cross-store dependencies lie decisions that determine the meaningfulness and maintainability of an entire test suite.
Table of contents
- 1. Why Pinia stores need isolated tests
- 2. Test setup: createTestingPinia vs. createPinia
- 3. Testing actions without side effects
- 4. Testing getters with different state
- 5. Mocking interactions between multiple stores
- 6. Testing components that use Pinia stores
- 7. Persisted stores and storage mocking
- 8. Testing plugins and store extensions
- 9. Pinia testing strategies compared
- 10. Summary
- 11. FAQ
1. Why Pinia stores need isolated tests
Pinia stores frequently hold application state shared across many components, such as cart contents, authentication status, or multi step form data. This exact central role makes bugs in a store especially expensive, because they affect every component that consumes the store. Isolated tests for Pinia stores verify the store logic independent of any specific component, which locates bugs earlier and more precisely than a component test that happens to test the store along the way.
A second reason for isolated store tests is reusability across many components. A store like useCartStore is typically used by a dozen different components, from the product page to the mini cart to checkout. Instead of implicitly re-verifying the same store logic in every component test, an isolated store test verifies the business logic once and reliably, while component tests can focus on correctly consuming the store.
This article builds up testing Pinia stores systematically, from the basic setup decision through action and getter tests to cross-store dependencies and plugin tests.
2. Test setup: createTestingPinia vs. createPinia
Pinia offers two fundamentally different approaches for testing: createPinia() creates a fully functional Pinia instance in which actions run for real, while createTestingPinia() from the @pinia/testing package replaces actions with spies by default, so they are not actually executed but their calls remain verifiable. The choice between the two depends on whether a test should verify the store logic itself, or whether a component that merely consumes the store is being tested.
For pure store unit tests that need to verify the actual business logic of an action, createPinia() is the right choice, since the action actually has to run and mutate the state. For component tests that only check whether a component calls an action with the correct arguments, createTestingPinia() is preferable, because the component is tested in isolation from the actual store implementation.
// useCartStore.test.js — real Pinia instance for testing actual store logic
import { describe, it, expect, beforeEach } from "vitest";
import { createPinia, setActivePinia } from "pinia";
import { useCartStore } from "@/stores/cart";
describe("useCartStore (real store logic)", () => {
beforeEach(() => {
setActivePinia(createPinia());
});
it("adds an item and recalculates the total", () => {
const store = useCartStore();
store.addItem({ id: 1, price: 19.9, quantity: 2 });
expect(store.items).toHaveLength(1);
expect(store.total).toBe(39.8);
});
});
// CartWidget.test.js — createTestingPinia to isolate the component from real store logic
import { describe, it, expect } from "vitest";
import { mount } from "@vue/test-utils";
import { createTestingPinia } from "@pinia/testing";
import { vi } from "vitest";
import CartWidget from "./CartWidget.vue";
import { useCartStore } from "@/stores/cart";
describe("CartWidget", () => {
it("calls the store's removeItem action with the correct id", async () => {
const wrapper = mount(CartWidget, {
global: {
plugins: [createTestingPinia({ stubActions: true, createSpy: vi.fn })],
},
});
const store = useCartStore();
await wrapper.find("[data-testid='remove-item-1']").trigger("click");
expect(store.removeItem).toHaveBeenCalledWith(1);
});
});
3. Testing actions without side effects
Actions in Pinia stores can be pure state mutations as well as calls with external side effects, such as HTTP requests. For actions with HTTP calls, it is important to mock the network access itself, not the action as a whole, so that the actual logic of the action, such as error handling and state updates, is genuinely verified. Mocking the entire action too coarsely would hide exactly the part most likely to contain bugs.
For actions that rely on composables like useFetch or an injected HTTP client, the same mocking strategy applies as for composables in general: the HTTP client is mocked, the action logic runs for real. That way, both the success case and error states such as network outages are covered in isolated tests, without a real server dependency.
// useOrdersStore.js — action with a real HTTP call through an injected client
import { defineStore } from "pinia";
export const useOrdersStore = defineStore("orders", {
state: () => ({ orders: [], isLoading: false, error: null }),
actions: {
async fetchOrders(httpClient) {
this.isLoading = true;
this.error = null;
try {
this.orders = await httpClient.get("/api/orders");
} catch (err) {
this.error = err.message;
} finally {
this.isLoading = false;
}
},
},
});
// useOrdersStore.test.js — mocking the HTTP client, running the real action logic
import { describe, it, expect, vi, beforeEach } from "vitest";
import { createPinia, setActivePinia } from "pinia";
import { useOrdersStore } from "@/stores/orders";
describe("useOrdersStore", () => {
beforeEach(() => setActivePinia(createPinia()));
it("populates orders and clears the loading flag on success", async () => {
const store = useOrdersStore();
const fakeClient = { get: vi.fn().mockResolvedValue([{ id: 1, total: 42 }]) };
await store.fetchOrders(fakeClient);
expect(store.orders).toEqual([{ id: 1, total: 42 }]);
expect(store.isLoading).toBe(false);
expect(store.error).toBeNull();
});
it("sets the error message when the request fails", async () => {
const store = useOrdersStore();
const failingClient = { get: vi.fn().mockRejectedValue(new Error("Network down")) };
await store.fetchOrders(failingClient);
expect(store.error).toBe("Network down");
expect(store.isLoading).toBe(false);
});
});
4. Testing getters with different state
Getters in Pinia are pure, derived values based on state, which makes them particularly easy to test in isolation: the test sets a specific state and checks whether the getter returns the expected value. Unlike actions, getters have no side effects, which makes them among the simplest building blocks of a store to test. Nonetheless, getters are frequently underestimated in practice and only implicitly verified through component tests, instead of being targeted for various state combinations.
Testing edge cases in state is especially important, such as an empty cart, a cart with a single discounted item, or state with contradictory values that should not occur in practice but could arise from a bug. A getter that does not crash with a division by zero on an empty array is a typical example of a test that covers a real edge case.
// useCartStore.test.js — testing getters with distinct, explicit state setups
import { describe, it, expect, beforeEach } from "vitest";
import { createPinia, setActivePinia } from "pinia";
import { useCartStore } from "@/stores/cart";
describe("useCartStore getters", () => {
beforeEach(() => setActivePinia(createPinia()));
it("returns zero for the average item price on an empty cart", () => {
const store = useCartStore();
expect(store.averageItemPrice).toBe(0);
});
it("calculates the average item price across multiple items", () => {
const store = useCartStore();
store.items = [
{ id: 1, price: 10, quantity: 1 },
{ id: 2, price: 30, quantity: 1 },
];
expect(store.averageItemPrice).toBe(20);
});
it("applies the discount getter only when a coupon is active", () => {
const store = useCartStore();
store.items = [{ id: 1, price: 100, quantity: 1 }];
store.appliedCoupon = { percent: 10 };
expect(store.discountedTotal).toBe(90);
});
});
5. Mocking interactions between multiple stores
Larger Vue applications often have stores that reference other stores, such as a useCartStore that accesses useAuthStore during checkout to retrieve the user id. When testing useCartStore in isolation, the actual implementation of useAuthStore should not be tested along with it, since that unnecessarily couples the test to a second store implementation.
The clean solution is to initialize the referenced store itself via createTestingPinia with a fixed, controlled state, instead of completely mocking the import of the second store. That way, the actual store structure stays real, only the state of the dependent store is preset in a controlled way for the test.
// useCartStore.test.js — controlling a dependent store's state via createTestingPinia
import { describe, it, expect } from "vitest";
import { createTestingPinia } from "@pinia/testing";
import { setActivePinia } from "pinia";
import { useCartStore } from "@/stores/cart";
import { useAuthStore } from "@/stores/auth";
describe("useCartStore with auth dependency", () => {
it("attaches the current user id to the checkout payload", () => {
const pinia = createTestingPinia({
stubActions: false,
initialState: {
auth: { currentUser: { id: 42, email: "user@example.com" } },
},
});
setActivePinia(pinia);
const cartStore = useCartStore();
cartStore.items = [{ id: 1, price: 19.9, quantity: 1 }];
const payload = cartStore.buildCheckoutPayload();
expect(payload.userId).toBe(42);
});
});
6. Testing components that use Pinia stores
When testing a Vue component that consumes a Pinia store, the primary goal is to check whether the component reads the store correctly and calls it correctly, not to re-verify the store logic itself. With createTestingPinia({ stubActions: true }), all actions are automatically replaced with spies, decoupling component tests from the actual store implementation and enabling fast, focused tests.
For tests that verify how a component reacts to different store states, such as a loading state or an error message, the state can be preset directly via initialState when creating the testing Pinia instance. That avoids the detour of real action calls just to reach a specific state.
// OrderList.test.js — presetting store state via initialState for component tests
import { describe, it, expect } from "vitest";
import { mount } from "@vue/test-utils";
import { createTestingPinia } from "@pinia/testing";
import OrderList from "./OrderList.vue";
describe("OrderList", () => {
it("shows a loading indicator when the store is loading", () => {
const wrapper = mount(OrderList, {
global: {
plugins: [
createTestingPinia({
initialState: { orders: { orders: [], isLoading: true, error: null } },
}),
],
},
});
expect(wrapper.find("[data-testid='loading-spinner']").exists()).toBe(true);
});
it("shows the error banner when the store has an error", () => {
const wrapper = mount(OrderList, {
global: {
plugins: [
createTestingPinia({
initialState: { orders: { orders: [], isLoading: false, error: "Network down" } },
}),
],
},
});
expect(wrapper.find("[data-testid='error-banner']").text()).toContain("Network down");
});
});
7. Persisted stores and storage mocking
Many Pinia stores use plugins such as pinia-plugin-persistedstate to automatically mirror parts of the state into localStorage or sessionStorage. In tests, this real browser storage must not be used unmocked, since it retains state between test runs and thereby makes tests dependent on each other. JSDOM, which Vitest uses by default, does come with a localStorage implementation, but it must be explicitly cleared between tests.
For targeted tests of the persistence logic itself, it is worth clearing localStorage in beforeEach() and, after setting a state value, explicitly checking whether localStorage.getItem() returns the expected serialized value. For all other store tests that do not concern persistence itself, the persistence plugin should be disabled in the test setup to avoid unnecessary coupling to the storage implementation.
8. Testing plugins and store extensions
Custom Pinia plugins that add, say, automatic logging, undo functionality, or cross-store synchronization, deserve their own isolated tests, independent of the stores they are applied to. A plugin test creates a minimal test store for this, applies the plugin via pinia.use(), and checks whether the expected behavior, such as an additional $reset() behavior or a logging call on every mutation, actually occurs.
It is important to keep plugin tests clearly separate from store specific tests. A bug in the plugin should show up in its own test case, independent of which specific store uses the plugin. That prevents a single plugin bug from simultaneously failing dozens of independent store tests, without the actual root cause being immediately apparent.
9. Pinia testing strategies compared
Depending on whether the store logic itself or a consuming component is being tested, different approaches are appropriate.
| Test target | Recommended setup | Actions | Rationale |
|---|---|---|---|
| Store business logic | createPinia() | Run for real | Actual state mutations must be verified |
| Component consuming a store | createTestingPinia() | Stubbed (spies) | Decouple component from store implementation |
| Dependent store (interaction) | createTestingPinia() with initialState | Depends on the case | Controlled state without a second implementation |
| Pinia plugin | Minimal test store + pinia.use() | Real, minimal | Verify plugin behavior independent of real stores |
Choosing the right setup is not a formality, it directly determines whether a test actually verifies the intended thing. Anyone using createTestingPinia for store business logic ends up testing nothing more than their own spies. Anyone using createPinia for a pure component test unnecessarily couples the test tightly to the concrete store implementation.
Mironsoft
Pinia architecture and reliable state management tests for Vue teams
Pinia stores that are reliably and cleanly tested?
We review existing Pinia stores for testability, set up createTestingPinia cleanly for component tests, and separate store business logic from component specific tests.
Store tests
Verifying actions and getters with real business logic in isolation
Component decoupling
Setting up createTestingPinia for fast, focused component tests
Plugin tests
Testing custom Pinia plugins independent of specific stores
10. Summary
Testing Pinia stores in isolation means above all deliberately distinguishing between two fundamentally different test goals: verifying real store logic with createPinia(), and decoupling a component from the store implementation with createTestingPinia(). Actions with external side effects should mock the HTTP client, not the action itself, so error handling and state updates are genuinely verified. Getters deserve targeted tests for edge cases in state, not just implicit verification through component tests.
Cross-store dependencies can be controlled via initialState, without testing a second store implementation along the way. Persistence plugins need explicit storage resets between tests, and custom Pinia plugins deserve their own test cases, independent of specific stores. Anyone who consistently makes these distinctions ends up with a test suite that locates bugs precisely, instead of having to debug through several layers of store, component, and plugin on every failure.
Testing Pinia Stores in Isolation — the essentials at a glance
createPinia vs. createTestingPinia
Real instance for store logic tests, testing Pinia with stubbed actions for component tests.
Actions with side effects
Mock the HTTP client, not the action itself, so error handling is genuinely verified.
Store interactions
Control dependent stores via initialState instead of fully mocking imports.
Plugins
Custom, minimal test stores for plugin behavior, independent of concrete application stores.