测试
Zustand Store 是纯 JavaScript——不依赖 React 渲染——因此测试非常简单。
测试 Store 本身(单元测试)
import { create, StoreApi, UseBoundStore } from 'zustand';
// 被测 Store
interface BearState {
bears: number;
increase: () => void;
reset: () => void;
}
const createBearStore = () =>
create<BearState>((set) => ({
bears: 0,
increase: () => set((s) => ({ bears: s.bears + 1 })),
reset: () => set({ bears: 0 }),
}));用 Vitest 测试
import { describe, it, expect, beforeEach } from 'vitest';
describe('BearStore', () => {
let useBearStore: ReturnType<typeof createBearStore>;
beforeEach(() => {
// 每个测试前创建全新的 Store
useBearStore = createBearStore();
});
it('should initialize with 0 bears', () => {
expect(useBearStore.getState().bears).toBe(0);
});
it('should increase bears', () => {
useBearStore.getState().increase();
expect(useBearStore.getState().bears).toBe(1);
useBearStore.getState().increase();
expect(useBearStore.getState().bears).toBe(2);
});
it('should reset bears to 0', () => {
useBearStore.getState().increase();
useBearStore.getState().increase();
useBearStore.getState().reset();
expect(useBearStore.getState().bears).toBe(0);
});
});测试异步 Action
interface UserState {
user: { id: number; name: string } | null;
loading: boolean;
error: string | null;
fetchUser: (id: number) => Promise<void>;
}
const createUserStore = () =>
create<UserState>((set) => ({
user: null,
loading: false,
error: null,
fetchUser: async (id: number) => {
set({ loading: true, error: null });
try {
const res = await fetch(`/api/users/${id}`);
const user = await res.json();
set({ user, loading: false });
} catch (err) {
set({ error: (err as Error).message, loading: false });
}
},
}));
describe('UserStore - async', () => {
it('should fetch user successfully', async () => {
// Mock fetch
const mockUser = { id: 1, name: 'Alice' };
globalThis.fetch = vi.fn().mockResolvedValue({
ok: true,
json: () => Promise.resolve(mockUser),
});
const useUserStore = createUserStore();
expect(useUserStore.getState().loading).toBe(false);
const promise = useUserStore.getState().fetchUser(1);
// fetchUser 内部 set({ loading: true }) 是同步的
expect(useUserStore.getState().loading).toBe(true);
await promise;
expect(useUserStore.getState().user).toEqual(mockUser);
expect(useUserStore.getState().loading).toBe(false);
});
it('should handle fetch error', async () => {
globalThis.fetch = vi.fn().mockRejectedValue(new Error('Network error'));
const useUserStore = createUserStore();
await useUserStore.getState().fetchUser(1);
expect(useUserStore.getState().error).toBe('Network error');
expect(useUserStore.getState().loading).toBe(false);
});
});测试使用了 Store 的组件
import { render, screen, fireEvent } from '@testing-library/react';
import { createBearStore } from './useBearStore';
// 组件
function BearCounter() {
const bears = useBearStore((s) => s.bears);
const increase = useBearStore((s) => s.increase);
return (
<div>
<span data-testid="bears">{bears}</span>
<button onClick={increase}>Add</button>
</div>
);
}方案 1:在每个测试中重置 Store(推荐)
describe('BearCounter', () => {
beforeEach(() => {
// 重置 Store 到初始状态
useBearStore.setState({ bears: 0 });
});
it('should display bear count', () => {
render(<BearCounter />);
expect(screen.getByTestId('bears').textContent).toBe('0');
});
it('should increase on click', () => {
render(<BearCounter />);
fireEvent.click(screen.getByText('Add'));
expect(screen.getByTestId('bears').textContent).toBe('1');
});
it('should render with pre-set state', () => {
useBearStore.setState({ bears: 5 });
render(<BearCounter />);
expect(screen.getByTestId('bears').textContent).toBe('5');
});
});方案 2:Mock 整个 Store
import { vi } from 'vitest';
// 仅 mock 返回值
vi.mock('./useBearStore', () => ({
useBearStore: vi.fn(),
}));
import { useBearStore } from './useBearStore';
describe('BearCounter - mocked', () => {
it('should display mocked value', () => {
// 用 mockReturnValue 给选择器返回值
(useBearStore as any).mockReturnValue(3);
render(<BearCounter />);
expect(screen.getByTestId('bears').textContent).toBe('3');
});
});💡 最佳实践:优先使用方案 1(setState 重置)——最轻量、最灵活、不需要 mock 基础设施。方案 2 只在需要完全隔离 Store 实现时使用。
测试中间件组合的 Store
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
const createPersistedStore = () =>
create(
persist(
(set) => ({
theme: 'light' as const,
setTheme: (theme: 'light' | 'dark') => set({ theme }),
}),
{ name: 'test-settings' }
)
);
describe('PersistedStore', () => {
beforeEach(() => {
localStorage.clear();
});
it('should persist to localStorage', () => {
const store = createPersistedStore();
store.getState().setTheme('dark');
const stored = JSON.parse(localStorage.getItem('test-settings')!);
expect(stored.state.theme).toBe('dark');
});
it('should clear persisted data', () => {
const store = createPersistedStore();
store.getState().setTheme('dark');
store.persist.clearStorage();
expect(localStorage.getItem('test-settings')).toBeNull();
});
});测试 subscribe
import { vi } from 'vitest';
describe('Store subscriptions', () => {
it('should call subscriber on state change', () => {
const useStore = createBearStore();
const listener = vi.fn();
const unsub = useStore.subscribe(listener);
useStore.getState().increase();
expect(listener).toHaveBeenCalledTimes(1);
expect(listener).toHaveBeenCalledWith(
expect.objectContaining({ bears: 1 }), // new state
expect.objectContaining({ bears: 0 }), // prev state
);
unsub();
useStore.getState().increase();
expect(listener).toHaveBeenCalledTimes(1); // 不再触发
});
});测试辅助:setState 的灵活用法
// 测试中直接设置任意状态,跳过 action
useStore.setState({ bears: 10 });
// 函数式更新
useStore.setState((prev) => ({ bears: prev.bears + 1 }));
// 重置为初始状态(replace = true)
const initialState = { bears: 0 };
useStore.setState(initialState, true);💡 最佳实践:测试中用
setState直接设置状态,可以快速构造各种测试场景,无需通过一连串 action 调用来达到目标状态。正式业务代码中应通过 action 修改状态。
测试要点速查
| 测试目标 | 方法 | 关键 API |
|---|---|---|
| Store 初始状态 | 直接断言 | getState() |
| Action 修改状态 | 调用 action 后断言 | getState().action() |
| 异步 action | mock fetch + await | getState().asyncAction() |
| 订阅 | 注册 listener,验证调用 | subscribe() |
| 持久化 | 检查 localStorage | persist.clearStorage() |
| 组件 + Store | 渲染前 setState 预设 | setState(...) |
| 每个测试独立 | beforeEach 中重置 | setState(initialState, true) |