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

Redux AsyncThunk测试异常:状态未更新致UI测试失败

问题:Redux状态未被MSW模拟的API数据更新,组件测试失败

我正在学习React与Redux,尝试对一个会分发action从外部API获取数据的React组件进行集成测试。使用MSW模拟fetch请求,同时采用Redux官方文档中的可复用测试渲染函数。自定义渲染组件后,预期API返回的模拟数据会更新到Redux状态并在MissionsTable组件中渲染,但目前store并未被API返回的模拟数据更新。已确认MSW的API模拟能返回设定的测试数据,求指点解决方向。


测试文件

import { screen } from '@testing-library/react';
import renderWithProvider from '../utils/test-util';
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import MissionsPage from '../components/missions/MissionsPage';

const missions = [
  {
    id: 'testmission1',
    name: 'test1',
    descriptions: 'test mission',
    isReserved: false,
    wiki: '',
  },
];

export const handlers = [
  rest.get('https://api.spacexdata.com/v3/missions', (req, res, ctx) => {
    return res(ctx.json(missions));
  }),
];

const server = setupServer(...handlers);

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

test('testing Mission page UI', async () => {
  const { store } = renderWithProvider(<MissionsPage />);
  console.log(store.getState().missions);
  const text = screen.getByText('/test mission/i');
  expect(await text.toBeInTheDocument());
});

待渲染组件(MissionsPage)

import { useDispatch, useSelector } from 'react-redux';
import { useEffect } from 'react';
import MissionsTable from './MissionsTable';
import { fetchMissions } from '../../redux/missions/missionSlice';

const MissionsPage = () => {
  const dispatch = useDispatch();
  const status = useSelector((state) => state.missions.toFetch);
  useEffect(() => {
    if (status) {
      dispatch(fetchMissions());
    }
  }, [status, dispatch]);
  return (
    <MissionsTable />
  );
};

export default MissionsPage;

可复用测试渲染函数

import React from 'react';
import { render } from '@testing-library/react';
import { configureStore } from '@reduxjs/toolkit';
import { Provider } from 'react-redux';
import missionSliceReducer from '../redux/missions/missionSlice';

function renderWithProviders(
  ui,
  {
    preloadedState = {},
    store = configureStore({
      reducer: { missions: missionSliceReducer },
      preloadedState,
    }),
    ...renderOptions
  } = {}
) {
  function Wrapper({ children }) {
    return <Provider store={store}>{children}</Provider>;
  }

  return { store, ...render(ui, { wrapper: Wrapper, ...renderOptions }) };
}

export default renderWithProviders;

定义AsyncThunk的Slice

import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';

const initialState = { missions: [], toFetch: 'true' };

export const fetchMissions = createAsyncThunk('missions/fetchMissions', async () => {
  const response = await fetch('https://api.spacexdata.com/v3/missions');
  return response.json();
});

const missionSlice = createSlice({
  name: 'missions',
  initialState,
  reducers: {
    bookMission: (state, { payload }) => {
      const newArr = state.missions.map((misObj) => {
        if (misObj.id === payload) {
          misObj.isReserved = !misObj.isReserved;
        }
        return misObj;
      });
      state.missions = newArr;
    },
  },
  extraReducers(builder) {
    builder.addCase(fetchMissions.fulfilled, (state, action) => {
      const missionList = action.payload.map(misObj => ({
        id: misObj.mission_id,
        name: misObj.mission_name,
        description: misObj.description,
        wiki: misObj.wikipedia,
        isReserved: false,
      }));
      state.missions = missionList;
      state.toFetch = false;
    });
  },
});

export const { bookMission } = missionSlice.actions;
export default missionSlice.reducer;

报错信息

console.log
    { missions: [], toFetch: 'true' }

      at Object.<anonymous> (src/__tests__/missionUI.test.js:31:11)

 FAIL  src/__tests__/missionUI.test.js
  ✕ testing Mission page UI (85 ms)

  ● testing Mission page UI

    TestingLibraryElementError: Unable to find an element with the text: /test mission/i.

    <body>
      <div>
        <div>
          <div class="table-responsive-sm">
            <table class="missionTable table table-striped table-bordered table-hover">
              <thead>
                <tr>
                  <th>Mission</th>
                  <th>Description</th>
                  <th>Status</th>
                  <th></th>
                </tr>
              </thead>
              <tbody />
            </table>
          </div>
        </div>
      </div>
    </body>

      30 |   const { store } = renderWithProvider(<MissionsPage />);
      31 |   console.log(store.getState().missions);
    > 32 |   const text = screen.getByText('/test mission/i');
         |                       ^
      33 |   expect(await text.toBeInTheDocument());
      34 | });

关键问题及修复方向

  • 初始状态类型不一致:Slice中toFetch初始值是字符串'true',但后续更新为布尔值false,类型混乱可能导致逻辑异常。统一改为布尔值:const initialState = { missions: [], toFetch: true };
  • MSW返回数据与Slice处理不匹配:测试中MSW返回的字段是descriptions,但Slice里取的是description;且Slice依赖真实API的mission_id、mission_name字段,测试数据里没有这些键,导致映射后生成空数组。要么调整MSW返回数据结构适配Slice,要么修改Slice映射逻辑适配测试数据。
  • 未等待异步操作完成:测试中直接用同步的getByText查找元素,此时异步请求还未完成。改用异步的findByText并等待:const text = await screen.findByText(/test mission/i);
  • 正则表达式写法错误:screen.getByText('/test mission/i')是把字符串当文本查找,正确写法是去掉引号:screen.getByText(/test mission/i)
  • 断言写法错误:expect(await text.toBeInTheDocument())中text已经是await后的结果,直接断言即可:expect(text).toBeInTheDocument();

修复后测试代码示例

test('testing Mission page UI', async () => {
  const { store } = renderWithProvider(<MissionsPage />);
  const text = await screen.findByText(/test mission/i);
  expect(text).toBeInTheDocument();
  expect(store.getState().missions).toHaveLength(1);
});

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.28 02:50:19