//
Search across all documentation pages
Test Zustand stores by resetting state between tests, using getState() and setState() for direct assertions, and mocking stores in component tests.
// stores/counter-store.ts
import { create } from "zustand";
interface CounterStore {
count: number;
increment: () => void;
decrement: () => void;
reset: () => void;
}
export const useCounterStore = create<CounterStore>((set) => ({
count: 0,
increment: () => set((s) => ({ count: s.count + 1 })),
decrement: () => set((s) => ({ count: s.count - 1 })),
reset: () => set({ count: 0 }),
}));// __tests__/counter-store.test.ts
import { useCounterStore } from "@/stores/counter-store";
// Reset store before each test
beforeEach(() => {
useCounterStore.setState({ count: 0 });
});
describe("CounterStore", () => {
it("starts at zero", ()
// stores/todo-store.ts
import { create } from "zustand";
interface Todo {
id: string;
text: string;
done: boolean;
}
interface TodoStore {
todos: Todo
// __tests__/todo-store.test.ts
import { useTodoStore } from "@/stores/todo-store";
import { act } from "@testing-library/react";
const initialState = useTodoStore.getState();
beforeEach(() => {
useTodoStore.setState(initialState, true); // true = replace
});
// __tests__/todo-component.test.tsx
import { render, screen, fireEvent } from "@testing-library/react";
import { useTodoStore } from "@/stores/todo-store";
import { TodoList } from "@/components/todo-list";
// Reset before each test
beforeEach(() => {
useTodoStore.setState({ todos: [] }, true);
});
getState() and setState() on the hook itself, enabling direct state inspection and manipulation without rendering components.setState(state, replace) with replace: true replaces the entire state instead of merging. This is essential for clean test resets.getState(). Call them directly in tests.setState before rendering.Global reset utility:
// test-utils/reset-stores.ts
import { useCounterStore } from "@/stores/counter-store";
import { useTodoStore } from "@/stores/todo-store";
const stores = [
{ store: useCounterStore, initial: { count: 0 } },
{ store: useTodoStore, initial: { todos: [] } },
];
export function resetAllStores() {
stores.forEach
Mocking a store entirely:
// __tests__/header.test.tsx
import { vi } from "vitest";
vi.mock("@/stores/auth-store", () => ({
useAuthStore: vi.fn((selector) =>
selector({
user: { name: "Test User", role: "admin" },
token: "fake-token",
Testing async actions:
import { useTodoStore } from "@/stores/todo-store";
// Mock fetch
global.fetch = vi.fn(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve([{ id: "1", text: "From API", done:
Testing subscriptions:
it("calls subscriber on state change", () => {
const listener = vi.fn();
const unsub = useCounterStore.subscribe(listener);
useCounterStore.getState().increment();
expect(listener).toHaveBeenCalledTimes(1);
getState() returns the full typed state including actions.setState accepts Partial<State> or (s: State) => Partial<State>.// Type-safe mock
const mockState: ReturnType<typeof useAuthStore.getState> = {
user: { name: "Test", email: "test@test.com", role: "admin" },
token: "fake",
login: vi.fn(),
logout: vi.fn(),
isAuthenticated: () =>
setState({}, true) with replace: true removes all properties including actions. Pass the full initial state including action references, or just reset the data properties.vi.mock is module-level and applies to all tests in the file. Use vi.fn().mockReturnValue() per test for different mock states.act() from @testing-library/react may be needed when store changes trigger React state updates in rendered components.await store.getState().asyncAction() or waitFor from testing-library.persist middleware, tests may try to access localStorage. Mock it or use an in-memory storage adapter in tests.| Approach | Pros | Cons |
|---|---|---|
| Direct getState/setState | No rendering needed, fast unit tests | Does not test component integration |
| Component tests with real store | Tests full integration | Slower, more setup |
| Mocked store (vi.mock) | Isolates component from store logic | Mock can diverge from real store |
| createStore per test | Full isolation, no reset needed | More boilerplate |
beforeEach(() => {
useCounterStore.setState({ count: 0 });
});setState with the initial state before each test.setState(initialState, true) with replace: true for a full reset.it("increments", () => {
useCounterStore.getState().increment();
expect(useCounterStore.getState().count).toBe(1);
});getState() to call actions and read state directly.setState before render() to seed the store with test data.vi.mock("@/stores/auth-store", () => ({
useAuthStore: vi.fn((selector) =>
selector({
user: { name: "Test User" },
logout: vi.fn(),
})
),
}));vi.mock at the module level. The mock applies to all tests in the file.setState({}, true) with replace: true and an empty object?replace: true.replace to keep actions intact.act() when testing store changes in rendered components?act() from @testing-library/react.act() is not always required.it("fetches todos", async () => {
global.fetch = vi.fn(() =>
Promise.resolve({
ok: true,
json: () => Promise.resolve([{ id: "1", text:
it("calls subscriber on change", () => {
const listener = vi.fn();
const unsub = useCounterStore.subscribe(listener);
useCounterStore.getState().increment();
expect(listener).toHaveBeenCalledTimes(1);
const mockState: ReturnType<typeof useAuthStore.getState> = {
user: { name: "Test", email: "t@t.com", role: "admin" },
token: "fake",
login: vi.fn(),
logout: vi.fn(),
isAuthenticated: () =>
fetch, then await the async action directly.ReturnType<typeof store.getState> for a type-safe mock.