如何用Jest测试Redux Toolkit的createAsyncThunk内部函数调用
测试失败的核心原因
- 低级笔误:测试代码里你dispatch的是
onEmailSubmit,但你要验证的是onInfoSubmit的逻辑,自然不会触发目标函数。 - 函数引用提前绑定:你定义
onInfoSubmit时,已经将当时的真实handlePayload函数引用传给了createAsyncThunk。jest mock同一模块的导出是在模块加载完成后替换导出对象,已经创建好的thunk内部仍然持有原始函数的引用,不会调用你mock的版本。同时handlePayload内部直接调用handleUpdateInfo属于模块内的直接调用,默认也不会走jest替换后的mock函数。
解决方案
方案1(推荐,耦合度最低):拆分独立函数到单独模块
将handlePayload、handleUpdateInfo拆分到独立的工具模块中导出,thunk从工具模块引入函数,jest mock对应工具模块即可生效:
- 新建
infoHelpers.js存放独立函数:
// infoHelpers.js export async function handlePayload (data, { dispatch, rejectWithValue, getState }) { // 原有逻辑不变 } export async function handleUpdateInfo(data, thunkApi) { // 原有逻辑不变 }
- 原slice文件引入工具函数创建thunk:
// InfoSlice.js import { createAsyncThunk } from '@reduxjs/toolkit' import { handlePayload, handleUpdateInfo } from './infoHelpers' export const onInfoSubmit = createAsyncThunk('info/onInfoSubmit', handlePayload)
- 测试时mock工具模块即可:
import configureStore from 'redux-mock-store' import thunk from 'redux-thunk' import { onInfoSubmit } from './InfoSlice' // 引入工具模块的函数 import { handlePayload, handleUpdateInfo } from './infoHelpers' // 你用到的其他外部依赖也要mock import { isFeatureEnabled } from './featureConfig' import { thirdPartyApiCall } from './api' const middlewares = [thunk] const mockStore = configureStore(middlewares) // mock工具模块和所有外部依赖 jest.mock('./infoHelpers.js', () => ({ handlePayload: jest.fn().mockResolvedValue({}), handleUpdateInfo: jest.fn().mockResolvedValue({}) })) jest.mock('./featureConfig', () => ({ isFeatureEnabled: jest.fn() })) jest.mock('./api', () => ({ thirdPartyApiCall: jest.fn().mockResolvedValue('mockThirdPartyRes') })) describe('InfoSlice unit tests', () => { beforeEach(() => { jest.clearAllMocks() Object.defineProperty(window, 'sessionStorage', { value: { getItem: jest.fn(() => null), setItem: jest.fn(() => null), removeItem: jest.fn(() => null) }, writable: true }) }) const initialState = { loginInfo: { userName: 'test@test.com', email: 'test@test.com' }, verifyEmailResponse: {}, email: { loginInfo: { email: 'test@test.com' } } } it('onInfoSubmit 会正常调用 handlePayload', async () => { const store = mockStore(initialState) const testData = { loginInfo: { userName: 'new@test.com', email: 'new@test.com', } } // 修正之前的笔误,dispatch目标thunk await store.dispatch(onInfoSubmit(testData)) expect(handlePayload).toHaveBeenCalledTimes(1) expect(handlePayload).toHaveBeenCalledWith(testData, expect.objectContaining({ dispatch: expect.any(Function), rejectWithValue: expect.any(Function), getState: expect.any(Function) })) }) it('功能开关关闭时直接调用 handleUpdateInfo', async () => { isFeatureEnabled.mockReturnValue(false) const store = mockStore(initialState) const testData = { loginInfo: { userName: 'new@test.com', email: 'new@test.com' } } await store.dispatch(onInfoSubmit(testData)) expect(thirdPartyApiCall).not.toHaveBeenCalled() expect(handleUpdateInfo).toHaveBeenCalledTimes(1) }) it('功能开关开启时先调用第三方接口再调用 handleUpdateInfo', async () => { isFeatureEnabled.mockReturnValue(true) const store = mockStore(initialState) const testData = { loginInfo: { userName: 'new@test.com', email: 'new@test.com' } } await store.dispatch(onInfoSubmit(testData)) expect(thirdPartyApiCall).toHaveBeenCalledTimes(1) expect(handleUpdateInfo).toHaveBeenCalledTimes(1) expect(handleUpdateInfo).toHaveBeenCalledWith(expect.objectContaining({ ...testData, thirdPartyResponse: 'mockThirdPartyRes' }), expect.any(Object)) }) })
方案2(不拆模块,改内部调用方式)
如果不想拆分模块,可以把模块内的函数调用改为通过exports对象调用,让jest的mock能生效:
// InfoSlice.js 中修改handlePayload的调用逻辑 export async function handlePayload (data, { dispatch, rejectWithValue, getState }) { dispatch(updateInfo({ isLoadingInfoRequest: true })) if (!isFeatureEnabled(FEATURE_NAME)) { // 改为调用exports上的函数 return exports.handleUpdateInfo(data, { dispatch, rejectWithValue, getState }) } return thirdPartyApiCall() .then((response) => { data.thirdPartyResponse = response return exports.handleUpdateInfo(data, { dispatch, rejectWithValue, getState }) }) }
这种方式可以验证handleUpdateInfo的调用,但是handlePayload的调用还是无法通过mock验证,因为createAsyncThunk的引用绑定问题仍然存在,更建议采用方案1。
内容的提问来源于stack exchange,提问作者CarlosFastLab
相关产品推荐
相关产品推荐

