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

如何正确Mock自定义Hook或Spy被Mock模块的方法?

React组件测试中Mock自定义Hook的问题

经过两天反复尝试仍毫无头绪,Stack Overflow上仅找到一个相关案例,但无法从中获取可行方案。目前正在测试一个简单React组件TableIconMenu,Mock自定义HookuseToggleModal()时遇到问题,两种尝试均失败。

相关文件

待测试组件TableIconMenu

import React from 'react';
import { useDispatch } from 'react-redux';
import { IconMenu } from '@common/components';
import { MODALS } from '@utils/consts';
import useToggleModal from '@hooks/useToggleModal';
import { getHistoryDetails } from '@redux/historyDetails/actions';

export const TableIconMenu = ({data, handleDetails}) => {
  const { showErrorModal, showModal } = useToggleModal();
  const dispatch = useDispatch();

  const handleHistoryPromise = id => {
    return dispatch(getHistoryDetails(id));
  };

  const showModalData = async (id, dataHandler, modalName) => {
    showModal(MODALS.INFO, { infoMessage: { isLoading: true } });

    const result = await dataHandler(id);

    if (result.error) {
      showErrorModal(result.error?.message);
    } else {
      showModal(modalName, { details: result.payload?.data });
    }
  };

  return (
    <>
      <IconMenu.Item
        onClick={() =>
          showModalData(
            data.id,
            handleHistoryPromise,
            MODALS.HISTORY
          )
        }
      >
        History
      </IconMenu.Item>
      <IconMenu.Item
        onClick={() => handleDetails(data.id)}
      >
        Refresh details
      </IconMenu.Item>
      <IconMenu.Item
        onClick={() => showModalData(
            data.id,
            handleDetails,
            MODALS.EDIT
          )
        }
      >
        Edit
      </IconMenu.Item>
    </>
  );
};
export default TableIconMenu;

useToggleModal Hook

import { useDispatch } from 'react-redux';
import { INFO_MODAL_TYPES, MODALS } from '@utils/consts';
import { closeModal, openModal } from '@redux/common/modals/actions';

const useToggleModal = () => {
  const dispatch = useDispatch(); // 尝试1中的问题根源

  const showModal = (modal, data) => {
    dispatch(openModal({ data, modal }));
  };

  const hideModal = () => {
    dispatch(closeModal());
  };

  const showErrorModal = message =>
    showModal(MODALS.INFO, {
      infoMessage: {
        errorMessage: message,
        errors: true,
        loading: false,
        type: INFO_MODAL_TYPES.ERROR
      }
    });

  const showSuccessModal = message =>
    showModal(MODALS.INFO, {
      infoMessage: {
        isLoading: false,
        message,
        type: INFO_MODAL_TYPES.SUCCESS
      }
    });

  return {
    hideModal,
    showErrorModal,
    showModal,
    showSuccessModal
  };
};

export default useToggleModal;

尝试1

最初通过import * as useToggleModal from '@hooks/useToggleModal';导入模块,然后对showModal方法设置Spy,但因useToggleModal内部使用react-redux的dispatch,出现以下错误:

"Invalid hook call. Hooks can only be called inside of the body of a function component. This could happen for one of the following reasons:..."

如果导入import * as ReactRedux from 'react-redux';并MockuseDispatch方法jest.spyOn(ReactRedux, 'useDispatch').mockReturnValue(jest.fn());,则会与configureStore冲突,破坏expect(dispatchSpy).toHaveBeenCalledWith(details);断言。

完整测试代码如下:

import React from 'react';
import { Provider } from 'react-redux';
import configureStore from 'redux-mock-store';
import * as useToggleModal from '@hooks/useToggleModal';
import * as actions from '@redux/historyDetails/actions';
import { fireEvent, render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import TableIconMenu from './TableIconMenu';

describe('TableIconMenu', () => {
  let mockStore;
  let store;
  let data;
  beforeEach(() => {
    mockStore = configureStore();

    data = {
      id: 123
    };
  });
  afterEach(() => {
    jest.clearAllMocks();
  });

  it('should be able to click button', () => {
    store = mockStore({});
    const details = { test: 'test' };

    const spyShowModal = jest.spyOn(useToggleModal.default(), 'showModal');
    const handleHistorySpy = jest.spyOn(actions, 'getHistoryDetails').mockReturnValue(details);
    const dispatchSpy = jest.spyOn(store, 'dispatch').mockReturnValue(Promise.resolve({ payload: { data: 'hello there' } }));

    render(
      <Provider store={store}>
        <TableIconMenu data={data} handleDetails={jest.fn()} />
      </Provider>
    );

    const button = screen.getByText('History');
    expect(button).toBeTruthy();
    fireEvent.click(button);
    expect(handleHistorySpy).toHaveBeenCalled();
    expect(dispatchSpy).toHaveBeenCalledWith(details);
    expect(spyShowModal).toHaveBeenCalled();
  });
});

尝试2

随后尝试Mock整个useToggleModal模块,但不知道如何Spy其内部方法。控制台能打印正确的args对象(如预期调用两次),但expect(spyShowModal).toHaveBeenCalled()显示调用次数为0。

完整测试代码如下:

import React from 'react';
import { Provider } from 'react-redux';
import configureStore from 'redux-mock-store';
import * as useToggleModal from '@hooks/useToggleModal';
import * as actions from '@redux/historyDetails/actions';
import { fireEvent, render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import TableIconMenu from './TableIconMenu';

jest.mock('@hooks/useToggleModal', () => ({
  __esModule: true,
  default: () => ({
    showErrorModal: jest.fn(),
    showModal: (...args) => {
      console.log('show modal args: ', args);
      return jest.fn();
    }
  })
}));

describe('TableIconMenu', () => {
  let mockStore;
  let store;
  let data;
  beforeEach(() => {
    mockStore = configureStore();

    data = {
      id: 123
    };
  });
  afterEach(() => {
    jest.clearAllMocks();
  });

  it('should be able to click button', () => {
    store = mockStore({});
    const details = { test: 'test' };

    const spyShowModal = jest.spyOn(useToggleModal.default(), 'showModal');
    const handleHistorySpy = jest.spyOn(actions, 'getHistoryDetails').mockReturnValue(details);
    const dispatchSpy = jest.spyOn(store, 'dispatch').mockReturnValue(Promise.resolve({ payload: { data: 'hello there' } }));

    render(
      <Provider store={store}>
        <TableIconMenu data={data} handleDetails={jest.fn()} />
      </Provider>
    );

    const button = screen.getByText('History');
    expect(button).toBeTruthy();
    fireEvent.click(button);
    expect(handleHistorySpy).toHaveBeenCalled();
    expect(dispatchSpy).toHaveBeenCalledWith(details);
    expect(spyShowModal).toHaveBeenCalled(); // 不生效,spyShowModal未被调用
  });
});

我的问题

  1. 如何在尝试1中正确MockuseToggleModal,避免「Invalid hook call」错误?
  2. 在尝试2中Mock整个模块后,如何Spy其内部方法?

内容的提问来源于stack exchange,提问作者crotylaldehyde

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.12 18:47:02