如何正确Mock自定义Hook或Spy被Mock模块的方法?
经过两天反复尝试仍毫无头绪,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中正确Mock
useToggleModal,避免「Invalid hook call」错误? - 在尝试2中Mock整个模块后,如何Spy其内部方法?
内容的提问来源于stack exchange,提问作者crotylaldehyde

