使用Jest模拟Zustand模块测试组件时触发调用栈溢出
解决Zustand测试组件时的调用栈溢出问题
我用Zustand官方文档提供的Jest模拟模块测试单独的Store时正常,但测试Counter组件时Jest抛出RangeError: Maximum call stack size exceeded错误。相关代码及报错信息如下:
相关代码
counter-store.ts
import { create } from 'zustand'; export interface CounterState { count: number; increment: () => void; decrement: () => void; } export const useCounterStore = create<CounterState>((set) => ({ count: 0, increment: () => set((state) => ({ count: state.count + 1 })), decrement: () => set((state) => ({ count: state.count - 1 })), }));
counter.tsx
import { useCounterStore } from 'shared/stores'; export const Counter = () => { const count = useCounterStore((state) => state.count); return ( <h1>Count: {count}</h1> ); }
counter.spec.tsx
jest.mock('zustand', () => { return jest.requireActual('../mocks/zustand'); }); import { act, render, screen } from '@testing-library/react'; import { Counter, type CounterState, useCounterStore } from 'shared'; describe('Counter Component', () => { let store: CounterState; beforeEach(() => { store = useCounterStore(); }); it('should render with pre-set count state', () => { act(() => { store.increment(); }); render(<Counter />); expect(screen.getByText('Count: 1')).toBeInTheDocument(); }); });
报错信息
RangeError: Maximum call stack size exceeded 21 | // Suppresses console.error output from ReactDOMTestUtils.act deprecation warning due to third party library 22 | if (typeof args[0] === 'string' && !args[0].includes('ReactDOMTestUtils.act')) { > 23 | console.error(...args); | ^ 24 | } 25 | }); 26 | });
问题根源
- 违反React Hooks规则:在组件外部(
beforeEach)直接调用useCounterStore()获取Store实例,打破了Hooks必须在组件函数内部调用的规则。 - 模拟逻辑触发循环:自定义的Zustand模拟在状态更新时,没有正确处理订阅机制,导致无限重渲染循环,最终撑爆调用栈。
修复方案
1. 调整测试代码,遵循Hooks规则
改用Zustand提供的getState/setState静态方法操作状态,不要在组件外部调用useCounterStore():
jest.mock('zustand', () => { return jest.requireActual('../mocks/zustand'); }); import { act, render, screen } from '@testing-library/react'; import { Counter, useCounterStore } from 'shared'; describe('Counter Component', () => { beforeEach(() => { // 重置初始状态 useCounterStore.setState({ count: 0 }); }); it('should render with pre-set count state', () => { // 渲染前通过setState预设状态 act(() => { useCounterStore.setState({ count: 1 }); }); render(<Counter />); expect(screen.getByText('Count: 1')).toBeInTheDocument(); }); it('should update count when increment is called', () => { render(<Counter />); act(() => { // 通过getState获取方法并调用 useCounterStore.getState().increment(); }); expect(screen.getByText('Count: 1')).toBeInTheDocument(); }); });
2. 修正Zustand模拟文件(关键)
如果自定义的../mocks/zustand.ts存在逻辑漏洞,替换为官方推荐的模拟方案,确保正确处理状态更新和订阅:
// ../mocks/zustand.ts import * as zustand from 'zustand'; import { act } from '@testing-library/react'; const actualCreate = zustand.create; export const create = jest.fn((createState) => { const store = actualCreate(createState); const originalGetState = store.getState; // 包装方法,自动处理act包裹状态更新 store.getState = () => { const state = originalGetState(); return new Proxy(state, { get(target, prop) { if (typeof target[prop] === 'function') { return (...args) => { let result; act(() => { result = target[prop](...args); }); return result; }; } return target[prop]; }, }); }; return store; }); export * from 'zustand';
内容的提问来源于stack exchange,提问作者Neil
相关产品推荐
相关产品推荐

