Actions, Getter und Store-Interaktionen sauber prüfen
Pinia Stores isoliert zu testen erfordert mehr als nur einen Store zu instanziieren und eine Action aufzurufen. Zwischen createPinia und createTestingPinia, echten und gemockten Actions sowie Store-übergreifenden Abhängigkeiten liegen Entscheidungen, die über Aussagekraft und Wartbarkeit einer gesamten Testsuite entscheiden.
Inhaltsverzeichnis
- 1. Warum Pinia Stores isolierte Tests brauchen
- 2. Test-Setup: createTestingPinia vs. createPinia
- 3. Actions testen ohne Seiteneffekte
- 4. Getter testen mit unterschiedlichem State
- 5. Store-Interaktionen zwischen mehreren Stores mocken
- 6. Komponenten testen, die Pinia Stores nutzen
- 7. Persistierte Stores und Storage-Mocking
- 8. Plugins und Store-Erweiterungen testen
- 9. Testing-Strategien für Pinia im Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Warum Pinia Stores isolierte Tests brauchen
Pinia Stores halten häufig Anwendungszustand, der über viele Komponenten hinweg geteilt wird, etwa Warenkorb-Inhalte, Authentifizierungsstatus oder Formulardaten über mehrere Schritte. Genau diese zentrale Rolle macht Fehler in einem Store besonders teuer, weil sie sich auf jede Komponente auswirken, die den Store konsumiert. Isolierte Tests für Pinia Stores prüfen die Store-Logik unabhängig von jeder konkreten Komponente, was Fehler früher und präziser lokalisiert als ein Komponententest, der zufällig auch den Store mittestet.
Ein zweiter Grund für isolierte Store-Tests ist die Wiederverwendbarkeit über mehrere Komponenten hinweg. Ein Store wie useCartStore wird typischerweise von einem Dutzend verschiedener Komponenten genutzt, von der Produktseite über die Mini-Cart bis zum Checkout. Statt dieselbe Store-Logik implizit in jedem Komponententest erneut zu prüfen, verifiziert ein isolierter Store-Test die Geschäftslogik einmalig und zuverlässig, während Komponententests sich auf das korrekte Konsumieren des Stores konzentrieren können.
Dieser Artikel baut das Testen von Pinia Stores von der grundlegenden Setup-Entscheidung über Action- und Getter-Tests bis zu Store-übergreifenden Abhängigkeiten und Plugin-Tests systematisch auf.
2. Test-Setup: createTestingPinia vs. createPinia
Pinia bietet zwei grundlegend unterschiedliche Ansätze für Tests: createPinia() erzeugt eine vollständig funktionsfähige Pinia-Instanz, in der Actions echt ausgeführt werden, während createTestingPinia() aus dem Paket @pinia/testing Actions standardmäßig durch Spies ersetzt, sodass sie nicht wirklich ausgeführt werden, aber ihre Aufrufe überprüfbar bleiben. Die Wahl zwischen beiden hängt davon ab, ob ein Test die Store-Logik selbst prüfen soll, oder ob eine Komponente getestet wird, die den Store nur konsumiert.
Für reine Store-Unit-Tests, die die tatsächliche Business-Logik einer Action verifizieren sollen, ist createPinia() die richtige Wahl, da die Action tatsächlich laufen und den State verändern muss. Für Komponententests, die nur prüfen, ob eine Komponente eine Action mit den richtigen Argumenten aufruft, ist createTestingPinia() vorzuziehen, weil die Komponente isoliert von der tatsächlichen Store-Implementierung getestet wird.
// 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. Actions testen ohne Seiteneffekte
Actions in Pinia Stores können sowohl reine State-Mutationen als auch Aufrufe mit externen Seiteneffekten sein, etwa HTTP-Requests. Für Actions mit HTTP-Aufrufen ist es wichtig, den Netzwerkzugriff selbst zu mocken, nicht die Action als Ganzes, damit die eigentliche Logik der Action, etwa Fehlerbehandlung und State-Updates, tatsächlich geprüft wird. Ein zu grobes Mocking der gesamten Action würde genau den Teil verstecken, der am ehesten Bugs enthält.
Für Actions, die auf Composables wie useFetch oder einen injizierten HTTP-Client zurückgreifen, gilt dieselbe Mocking-Strategie wie bei Composables generell: Der HTTP-Client wird gemockt, die Action-Logik läuft real. So werden sowohl der Erfolgsfall als auch Fehlerzustände wie Netzwerkausfälle in isolierten Tests abgedeckt, ohne echte Server-Abhängigkeit.
// 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. Getter testen mit unterschiedlichem State
Getter in Pinia sind reine, abgeleitete Werte aus dem State und lassen sich deshalb besonders leicht isoliert testen: Der Test setzt einen bestimmten State und prüft, ob der Getter den erwarteten Wert zurückgibt. Anders als Actions haben Getter keine Seiteneffekte, was sie zu den am einfachsten zu testenden Bausteinen eines Stores macht. Trotzdem werden Getter in der Praxis häufig unterschätzt und nur implizit durch Komponententests mitgeprüft, statt gezielt für verschiedene State-Kombinationen.
Besonders wichtig ist das Testen von Grenzfällen im State, etwa ein leerer Warenkorb, ein Warenkorb mit einem einzigen reduzierten Artikel, oder ein State mit widersprüchlichen Werten, die in der Praxis nicht vorkommen sollten, aber durch einen Bug entstehen könnten. Ein Getter, der bei leerem Array nicht mit einer Division durch null abstürzt, ist ein typisches Beispiel für einen Test, der einen echten Edge Case abdeckt.
// 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. Store-Interaktionen zwischen mehreren Stores mocken
Größere Vue-Anwendungen haben häufig Stores, die andere Stores referenzieren, etwa ein useCartStore, der beim Checkout auf useAuthStore zugreift, um die Nutzer-ID abzurufen. Beim isolierten Testen von useCartStore soll dabei die tatsächliche Implementierung von useAuthStore nicht mitgetestet werden, da das den Test unnötig an eine zweite Store-Implementierung koppelt.
Die saubere Lösung ist, den referenzierten Store selbst über createTestingPinia mit einem festen, kontrollierten State zu initialisieren, statt den Import des zweiten Stores komplett zu mocken. Damit bleibt die eigentliche Store-Struktur real, nur der State des abhängigen Stores wird für den Test kontrolliert vorbelegt.
// 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. Komponenten testen, die Pinia Stores nutzen
Beim Testen einer Vue-Komponente, die einen Pinia Store konsumiert, geht es primär darum zu prüfen, ob die Komponente den Store korrekt liest und korrekt aufruft, nicht die Store-Logik selbst erneut zu verifizieren. Mit createTestingPinia({ stubActions: true }) werden alle Actions automatisch durch Spies ersetzt, was Komponententests von der tatsächlichen Store-Implementierung entkoppelt und schnelle, fokussierte Tests ermöglicht.
Für Tests, die prüfen, wie eine Komponente auf unterschiedliche Store-Zustände reagiert, etwa einen Ladezustand oder eine Fehlermeldung, lässt sich der State direkt über initialState bei der Erstellung der Testing-Pinia-Instanz vorbelegen. Das vermeidet den Umweg über echte Action-Aufrufe, nur um einen bestimmten State-Zustand zu erreichen.
// 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. Persistierte Stores und Storage-Mocking
Viele Pinia Stores nutzen Plugins wie pinia-plugin-persistedstate, um Teile des States automatisch in localStorage oder sessionStorage zu spiegeln. In Tests darf dieser echte Browser-Storage nicht ungemockt verwendet werden, da er zwischen Testläufen Zustand behält und Tests dadurch voneinander abhängig macht. JSDOM, das Vitest standardmäßig nutzt, bringt zwar eine localStorage-Implementierung mit, diese muss aber zwischen Tests explizit geleert werden.
Für gezielte Tests der Persistenz-Logik selbst empfiehlt sich, localStorage in beforeEach() zu leeren und nach dem Setzen eines State-Werts explizit zu prüfen, ob localStorage.getItem() den erwarteten serialisierten Wert liefert. Für alle anderen Store-Tests, die nicht die Persistenz selbst betreffen, sollte das Persistierungs-Plugin im Test-Setup deaktiviert werden, um unnötige Kopplung an die Storage-Implementierung zu vermeiden.
8. Plugins und Store-Erweiterungen testen
Eigene Pinia-Plugins, die etwa automatisches Logging, Undo-Funktionalität oder Store-übergreifende Synchronisation hinzufügen, verdienen eigene, isolierte Tests, unabhängig von den Stores, auf die sie angewendet werden. Ein Plugin-Test erstellt dafür einen minimalen Test-Store, wendet das Plugin über pinia.use() an und prüft, ob das erwartete Verhalten, etwa ein zusätzliches $reset()-Verhalten oder ein Logging-Aufruf bei jeder Mutation, tatsächlich eintritt.
Wichtig ist, Plugin-Tests von Store-spezifischen Tests klar zu trennen. Ein Fehler im Plugin sollte sich in einem eigenen Testfall zeigen, der unabhängig davon ist, welcher konkrete Store das Plugin verwendet. Das verhindert, dass ein einzelner Bug im Plugin gleichzeitig Dutzende unabhängige Store-Tests fehlschlagen lässt, ohne dass die eigentliche Fehlerursache sofort erkennbar wird.
9. Testing-Strategien für Pinia im Vergleich
Je nachdem, ob eine Store-Logik selbst oder eine konsumierende Komponente getestet wird, eignen sich unterschiedliche Ansätze.
| Testziel | Empfohlenes Setup | Actions | Begründung |
|---|---|---|---|
| Store-Business-Logik | createPinia() | Real ausgeführt | Actual state mutations must be verified |
| Komponente, die Store konsumiert | createTestingPinia() | Gestubbt (Spies) | Komponente von Store-Implementierung entkoppeln |
| Abhängiger Store (Interaktion) | createTestingPinia() mit initialState | Je nach Testfall | Kontrollierter State ohne zweite Implementierung |
| Pinia-Plugin | Minimaler Test-Store + pinia.use() | Real, minimal | Plugin-Verhalten unabhängig von echten Stores prüfen |
Die Wahl des richtigen Setups ist keine Formalität, sondern entscheidet direkt darüber, ob ein Test tatsächlich die beabsichtigte Sache prüft. Wer createTestingPinia für Store-Business-Logik verwendet, testet am Ende nichts weiter als die eigenen Spies. Wer createPinia für einen reinen Komponententest nutzt, koppelt den Test unnötig eng an die konkrete Store-Implementierung.
Mironsoft
Pinia-Architektur und belastbare State-Management-Tests für Vue-Teams
Pinia Stores, die zuverlässig und isoliert getestet sind?
Wir überprüfen bestehende Pinia Stores auf Testbarkeit, richten createTestingPinia sauber für Komponententests ein und trennen Store-Business-Logik von komponentenspezifischen Tests.
Store-Tests
Actions und Getter mit echter Business-Logik isoliert prüfen
Komponenten-Entkopplung
createTestingPinia für schnelle, fokussierte Komponententests einrichten
Plugin-Tests
Eigene Pinia-Plugins unabhängig von konkreten Stores testen
10. Zusammenfassung
Pinia Stores isoliert testen bedeutet vor allem, bewusst zwischen zwei grundverschiedenen Testzielen zu unterscheiden: der Verifikation echter Store-Logik mit createPinia() und der Entkopplung einer Komponente von der Store-Implementierung mit createTestingPinia(). Actions mit externen Seiteneffekten sollten den HTTP-Client mocken, nicht die Action selbst, damit Fehlerbehandlung und State-Updates tatsächlich geprüft werden. Getter verdienen gezielte Tests für Grenzfälle im State, nicht nur implizite Prüfung über Komponententests.
Store-übergreifende Abhängigkeiten lassen sich über initialState kontrollieren, ohne eine zweite Store-Implementierung mitzutesten. Persistenz-Plugins brauchen expliziten Storage-Reset zwischen Tests, und eigene Pinia-Plugins verdienen eigene, von konkreten Stores unabhängige Testfälle. Wer diese Unterscheidungen konsequent trifft, bekommt eine Testsuite, die Fehler präzise lokalisiert, statt bei jedem Fehlschlag durch mehrere Ebenen von Store, Komponente und Plugin debuggen zu müssen.
Pinia Stores isoliert testen — Das Wichtigste auf einen Blick
createPinia vs. createTestingPinia
Echte Instanz für Store-Logik-Tests, Testing-Pinia mit gestubbten Actions für Komponententests.
Actions mit Seiteneffekten
HTTP-Client mocken, nicht die Action selbst, damit Fehlerbehandlung real geprüft wird.
Store-Interaktionen
Abhängige Stores über initialState kontrollieren, statt Imports komplett zu mocken.
Plugins
Eigene, minimale Test-Stores für Plugin-Verhalten, unabhängig von konkreten Anwendungs-Stores.