Skip to content
测试

测试

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)