You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用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 | });

问题根源

  1. 违反React Hooks规则:在组件外部(beforeEach)直接调用useCounterStore()获取Store实例,打破了Hooks必须在组件函数内部调用的规则。
  2. 模拟逻辑触发循环:自定义的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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.22 12:37:22