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

如何使用Jest测试基于fetch的复杂异步Redux Reducer

如何捕获Redux异步Thunk中分发的ACTION?

场景还原

我有一个基于fetch API实现的Redux Thunk Action Creator:

export const fetchRelatedFamilies = () => {
  return (dispatch, getState) => {
    if (isEmpty(getState().relatedFamiliesById)) {
      dispatch({ type: REQUEST_RELATED_FAMILIES_BY_ID })
      new HttpRequestHelper('/api/related_families',
        (responseJson) => {
          dispatch({ type: RECEIVE_RELATED_FAMILIES_BY_ID, updates: responseJson.relatedFamiliesById })
        },  
        e => dispatch({ type: RECEIVE_RELATED_FAMILIES_BY_ID, error: e.message, updates: {} }), 
      ).get()
    }   
  }
}

对应的测试代码无法捕获异步分发的RECEIVE_RELATED_FAMILIES_BY_ID action:

import configureStore from 'redux-mock-store'
import fetchMock from 'fetch-mock'
import thunk from 'redux-thunk'
import { cloneDeep } from 'lodash'
import { fetchRelatedFamilies, REQUEST_RELATED_FAMILIES_BY_ID, RECEIVE_RELATED_FAMILIES_BY_ID } from 'redux/rootReducer'

import { STATE1 } from '/shared/components/panel/fixtures.js'

describe('fetchRelatedFamilies', () => {
  const middlewares = [thunk]
  const testActionsDispatch = async (currstate, expectedActions) => {
    const store = configureStore(middlewares)(currstate)
    store.dispatch(fetchRelatedFamilies())

    // 尝试过用setTimeout等待,但要么捕获不到,要么测试无意义通过
    //await new Promise((r) => setTimeout(r, 200));
    expect(store.getActions()).toEqual(expectedActions)
  }

  afterEach(() => {
    fetchMock.reset()
    fetchMock.restore()
  })  

  it('Dispatches correct actions when data - relatedFamiliesById - is absent in state', () => {
    const relatedFamiliesById = cloneDeep(STATE1.relatedFamiliesById)
    fetchMock
      .getOnce('/api/related_families', { body: relatedFamiliesById, headers: { 'content-type': 'application/json' } })

    STATE1.relatedFamiliesById = {}
    const expectedActions = [ 
      { type: REQUEST_RELATED_FAMILIES_BY_ID },
      { type: RECEIVE_RELATED_FAMILIES_BY_ID, updates: relatedFamiliesById }
    ]   
    testActionsDispatch(STATE1, expectedActions)
  })  
})

遇到的问题:

  • 测试只能捕获到同步的REQUEST_RELATED_FAMILIES_BY_ID,拿不到异步返回后的RECEIVE_RELATED_FAMILIES_BY_ID
  • 用setTimeout模拟等待会导致测试无论预期是否正确都通过,断言逻辑被忽略
  • 无法用store.dispatch(...).then(),因为当前Thunk没有返回Promise
  • 项目版本限制,不能安装waitFor这类库

解决方法

方法1:改造Thunk,让它返回Promise

修改fetchRelatedFamilies,把HTTP请求的过程包装成Promise,这样测试里就能用await等待异步完成:

export const fetchRelatedFamilies = () => {
  return (dispatch, getState) => {
    // 如果数据已存在,直接返回空Promise
    if (!isEmpty(getState().relatedFamiliesById)) {
      return Promise.resolve();
    }

    dispatch({ type: REQUEST_RELATED_FAMILIES_BY_ID })
    
    // 把HttpRequestHelper的调用包装成Promise
    return new Promise((resolve, reject) => {
      new HttpRequestHelper('/api/related_families',
        (responseJson) => {
          const action = { type: RECEIVE_RELATED_FAMILIES_BY_ID, updates: responseJson.relatedFamiliesById };
          dispatch(action);
          resolve(action); // 成功时resolve
        },  
        e => {
          const action = { type: RECEIVE_RELATED_FAMILIES_BY_ID, error: e.message, updates: {} };
          dispatch(action);
          reject(action); // 失败时reject
        }, 
      ).get()
    });
  }
}

然后修改测试代码,等待dispatch完成:

const testActionsDispatch = async (currstate, expectedActions) => {
  const store = configureStore(middlewares)(currstate)
  // 等待Thunk的Promise完成
  await store.dispatch(fetchRelatedFamilies())
  expect(store.getActions()).toEqual(expectedActions)
}

// 测试用例也要改成async/await
it('Dispatches correct actions when data is absent', async () => {
  const relatedFamiliesById = cloneDeep(STATE1.relatedFamiliesById)
  fetchMock
    .getOnce('/api/related_families', { body: { relatedFamiliesById }, headers: { 'content-type': 'application/json' } })

  STATE1.relatedFamiliesById = {}
  const expectedActions = [ 
    { type: REQUEST_RELATED_FAMILIES_BY_ID },
    { type: RECEIVE_RELATED_FAMILIES_BY_ID, updates: relatedFamiliesById }
  ]   
  await testActionsDispatch(STATE1, expectedActions)
})

方法2:不修改原代码,用fetchMock的flush方法同步完成请求

fetchMock本身提供了flush方法,可以立即触发所有挂起的请求回调,不用等待定时器:

const testActionsDispatch = async (currstate, expectedActions) => {
  const store = configureStore(middlewares)(currstate)
  store.dispatch(fetchRelatedFamilies())
  
  // 立即刷新所有fetchMock的挂起请求,触发回调
  await fetchMock.flush()
  
  expect(store.getActions()).toEqual(expectedActions)
}

it('Dispatches correct actions when data is absent', async () => {
  const relatedFamiliesById = cloneDeep(STATE1.relatedFamiliesById)
  fetchMock
    .getOnce('/api/related_families', { body: { relatedFamiliesById }, headers: { 'content-type': 'application/json' } })

  STATE1.relatedFamiliesById = {}
  const expectedActions = [ 
    { type: REQUEST_RELATED_FAMILIES_BY_ID },
    { type: RECEIVE_RELATED_FAMILIES_BY_ID, updates: relatedFamiliesById }
  ]   
  await testActionsDispatch(STATE1, expectedActions)
})

注意:原测试代码的fetchMock返回格式有误——Thunk期望responseJson包含relatedFamiliesById字段,所以返回的body应该是{ relatedFamiliesById },而非直接返回该字段的值,这也是之前捕获不到正确action的原因之一。

方法3:手动模拟事件循环

如果不想修改代码也不用fetchMock的flush,可以用await Promise.resolve()触发事件循环的微任务,让异步回调执行:

const testActionsDispatch = async (currstate, expectedActions) => {
  const store = configureStore(middlewares)(currstate)
  store.dispatch(fetchRelatedFamilies())
  
  // 等待微任务队列执行,触发fetch的回调
  await Promise.resolve()
  
  expect(store.getActions()).toEqual(expectedActions)
}

这个方法依赖于fetchMock用微任务模拟fetch行为,大部分场景下都能生效。


关键问题总结

  1. 原测试代码的fetchMock返回格式错误,导致Thunk拿到的responseJson结构不符合预期
  2. 异步Thunk未返回Promise,无法直接用await等待异步流程完成
  3. setTimeout是宏任务,会打乱异步回调的执行顺序,导致断言逻辑失效

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.20 03:50:26