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

如何用Jest测试Redux Toolkit的createAsyncThunk内部函数调用

测试失败的核心原因

  1. 低级笔误:测试代码里你dispatch的是onEmailSubmit,但你要验证的是onInfoSubmit的逻辑,自然不会触发目标函数。
  2. 函数引用提前绑定:你定义onInfoSubmit时,已经将当时的真实handlePayload函数引用传给了createAsyncThunk。jest mock同一模块的导出是在模块加载完成后替换导出对象,已经创建好的thunk内部仍然持有原始函数的引用,不会调用你mock的版本。同时handlePayload内部直接调用handleUpdateInfo属于模块内的直接调用,默认也不会走jest替换后的mock函数。

解决方案

方案1(推荐,耦合度最低):拆分独立函数到单独模块

将handlePayload、handleUpdateInfo拆分到独立的工具模块中导出,thunk从工具模块引入函数,jest mock对应工具模块即可生效:

  1. 新建infoHelpers.js存放独立函数:
// infoHelpers.js
export async function handlePayload (data, { dispatch, rejectWithValue, getState }) {
  // 原有逻辑不变
}
export async function handleUpdateInfo(data, thunkApi) {
  // 原有逻辑不变
}
  1. 原slice文件引入工具函数创建thunk:
// InfoSlice.js
import { createAsyncThunk } from '@reduxjs/toolkit'
import { handlePayload, handleUpdateInfo } from './infoHelpers'

export const onInfoSubmit = createAsyncThunk('info/onInfoSubmit', handlePayload)
  1. 测试时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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.01 09:39:01