使用 React 和 Redux 进行测试驱动开发:Thunk、Slices 和请求模拟

2025-05-25

使用 React 和 Redux 进行测试驱动开发:Thunk、Slices 和请求模拟

如果您想阅读更多这些文章,请随时订阅我的时事通讯。😁

用 Redux 编写测试听起来可能有点违反直觉。如果你用的是 Redux,可能会觉得更复杂。🥶

但是,在添加功能之前编写测试有助于编写更好的代码,因为您可以提前考虑设计模式、架构和将要使用的变量名称。🚀

项目

我们正在构建一个用户管理仪表盘。基本上,我们使用 Redux 和 Thinks 来执行 CRUD 操作。
然后,用户可以:

  • 创建用户。
  • 更新用户。
  • 删除用户。
  • 获取用户或用户列表。

这个小项目中的用户将具有四个属性:

  • 一个 ID
  • 一个名字
  • 用户名
  • 一封电子邮件

为了简单起见,我们不会编写 UI 代码。我们将主要专注于创建测试环境、编写测试,并确保切片和 thunk 能够处理我们想要的内容。

设置项目

首先,创建一个简单的 React 项目。



yarn create react-app react-redux-test-driven-development


Enter fullscreen mode Exit fullscreen mode

项目创建完成后,通过运行项目确保一切正常。



cd react-redux-test-driven-development
yarn start


Enter fullscreen mode Exit fullscreen mode

您将在http://localhost:3000上运行类似的程序。

启动 React 应用程序

接下来,我们要安装 redux 包以及一个模拟适配器。模拟适配器将帮助我们模拟服务器上的请求。



# Yarn
yarn add @reduxjs/toolkit axios-mock-adapter axios


Enter fullscreen mode Exit fullscreen mode

太棒了!安装完成后,我们先来为测试编写模拟数据。🍔

模拟测试数据

在 src 目录中,创建一个名为 的新目录utils。然后,创建一个名为 的文件tests.data.js

该文件将包含以下方法和变量:

  • mockNetWorkResponse:在默认实例上创建模拟适配器,并模拟对所需端点的任何 GET 或 POST 请求
  • getCreateUserResponse:返回 POST 请求的响应/user/
  • getUserListResponse:返回 GET 请求的响应/user/

让我们编写这些方法。



import axios from "axios";
import MockAdapter from "axios-mock-adapter";

const getCreateUserResponse = {
  id: 3,
  name: "Clementine Bauch",
  username: "Samantha",
  email: "Nathan@yesenia.net"
};

const getUserListResponse = [
  {
    id: 1,
    name: "Leanne Graham",
    username: "Bret",
    email: "Sincere@april.biz"
  },
  {
    id: 2,
    name: "Ervin Howell",
    username: "Antonette",
    email: "ervin@april.biz"
  },
];

// Adding mock network response that is used in tests

const mockNetWorkResponse = () => {
  const mock = new MockAdapter(axios);

  mock.onGet(`/users/`).reply(200, getUserListResponse);
  mock.onPost(`/users/`).reply(200, getCreateUserResponse);
};

export {
  mockNetWorkResponse,
  getCreateUserResponse,
  getUserListResponse,
};


Enter fullscreen mode Exit fullscreen mode

太棒了!模拟适配器准备好后,我们可以专注于初始化存储并为切片编写测试了。

编写测试

这是最有趣的部分。让我们开始 TDD 吧!🔥
首先,让我们创建并配置 store。在 src 目录中,创建一个名为 的新目录index.js。在此文件中,初始化 store。



import { configureStore } from "@reduxjs/toolkit";
import { combineReducers } from "redux";

const rootReducer = combineReducers({
  // Adding the reducers
});

export const store = configureStore({
  reducer: rootReducer,
});


Enter fullscreen mode Exit fullscreen mode

编写 userSlice

“切片”是应用中单个功能的 Redux Reducer 逻辑和操作的集合,通常一起定义在一个文件中。切片userSlice将包含用于执行 CRUD 操作的操作和 Reducer。
切片的默认状态应该是一个空数组,毕竟我们处理的是users
让我们通过编写测试并使其失败来深入研究它。在src/store名为 的新目录下创建一个名为 的新目录slices
在此目录中,添加一个名为 的文件user.test.js。此文件将包含我们将为 编写的测试userSlice

第一个测试是确保 store 为空或未定义。初始状态可能如下所示。



const initialState = {
  users: [],
  loading: false,
  error: null
};


Enter fullscreen mode Exit fullscreen mode

让我们写第一个测试。

测试初始状态

user.test.js文件中,添加以下测试:



import reducer, {
    initialState,
  } from "./user";
  /**
   * Testing the initial state
   */

  test("Should return initial state", () => {
    expect(
      reducer(undefined, {
        type: undefined,
      })
    ).toEqual(initialState);
  });


Enter fullscreen mode Exit fullscreen mode

现在运行yarn test命令。测试会失败。❌
完全正常。我们还没有定义userSlice、reducer 和初始状态。

在切片目录中,创建一个名为 user.js 的文件。



export const initialState = {
  users: [],
  loading: false,
  error: null
};

export const userSlice = createSlice({
  name: "users",
  initialState: initialState,
  extraReducers: () => {
  },
});

export default userSlice.reducer;


Enter fullscreen mode Exit fullscreen mode

并且,在存储中注册切片减速器store/index.js



import { configureStore } from "@reduxjs/toolkit";
import { combineReducers } from "redux";
import { userSlice } from "./slices/user";

const rootReducer = combineReducers({
  users: userSlice.reducer,
});

export const store = configureStore({
  reducer: rootReducer,
});


Enter fullscreen mode Exit fullscreen mode

然后再次运行测试。✅

测试用户创建

为此,我们需要编写一个 thunk。thunk 是一个函数,它以 store 的 dispatch 方法为参数,并在 API 或副作用完成后用于调度同步操作。

首先,我们来为这个功能编写测试。



import reducer, {
    initialState,
    addUser
  } from "./user";
  import {
    mockNetWorkResponse,
    getCreateUserResponse,
  } from "../../utils/tests.data";

 /**
   * Testing the createUser thunk
   */

  describe("Create a new user", () => {
    beforeAll(() => {
      mockNetWorkResponse();
    });

    it("Should be able to create a new user", async () => {
      // Saving previous state
      const previousState = store.getState().users;

      const previousUsers = [...previousState.users];
      previousUsers.push(getCreateUserResponse);

      // Dispatching the action

      const result = await store.dispatch(addUser(getCreateUserResponse));

      const user = result.payload;

      expect(result.type).toBe("users/addUser/fulfilled");
      expect(user).toEqual(getCreateUserResponse);

      const state = store.getState().users;

      expect(state.users).toEqual(previousUsers);
    });


Enter fullscreen mode Exit fullscreen mode

在本次测试中,我们:

  • 在进行更新之前,保存先前的状态并将users属性修改为预期状态。这将有助于我们比较下一个状态。
  • 调度一个动作并确保它已完成,并且我们比较预期状态和实际状态。

再次,测试将失败。让我们为创建用户功能添加 thunk 和 reducer。



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

const addUser = createAsyncThunk("users/addUser", async (user) => {
  const res = await axios.post(`/users/`, user);
  return res.data;
});

export const initialState = {
  users: [],
  loading: false,
  error: null
};

export const userSlice = createSlice({
  name: "users",
  initialState: initialState,
  extraReducers: () => {
    /*
     * addUser Cases
     */

    builder.addCase(addUser.pending, (state) => {
      state.loading = true;
    });
    builder.addCase(addUser.rejected, (state, action) => {
      state.loading = false;
      state.error = action.error.message || "Something went wrong";
    });
    builder.addCase(addUser.fulfilled, (state, action) => {
      state.loading = true;
      state.users.push(action.payload);
    });
  },
});

export default userSlice.reducer;
export { addUser };


Enter fullscreen mode Exit fullscreen mode

然后再次运行测试,它应该会通过。✅

编写测试以获取用户列表

首先,我们来为这个功能编写测试。



import reducer, {
    initialState,
    addUser,
    fetchUsers
  } from "./user";
  import {
    mockNetWorkResponse,
    getCreateUserResponse,
    getUserListResponse
  } from "../../utils/tests.data";

...
  /**
   * Testing the fetchUsers thunk
   */

  describe("List all users", () => {
    beforeAll(() => {
      mockNetWorkResponse();
    });

    it("Shoudl be able to fetch the user list", async () => {
      const result = await store.dispatch(fetchUsers());

      const users = result.payload;

      expect(result.type).toBe("users/fetchUsers/fulfilled");
      expect(users).toEqual(getUserListResponse);

      const state = store.getState().users;

      expect(state.users).toEqual(getUserListResponse);
    });
  });


Enter fullscreen mode Exit fullscreen mode

并确保测试失败。
让我们添加 Reducer 和 thunk。



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

const fetchUsers = createAsyncThunk(
  "users/fetchUsers",
  async () => {
    const response = await axios.get(`/users/`);
    return response.data;
  }
);

const addUser = createAsyncThunk("users/addUser", async (user) => {
  const res = await axios.post(`/users/`, user);
  return res.data;
});

export const initialState = {
  users: [],
  loading: false,
  error: null
};

export const userSlice = createSlice({
  name: "users",
  initialState: initialState,
  extraReducers: () => {
    /*
     * addUser Cases
     */

    builder.addCase(addUser.pending, (state) => {
      state.loading = true;
    });
    builder.addCase(addUser.rejected, (state, action) => {
      state.loading = false;
      state.error = action.error.message || "Something went wrong";
    });
    builder.addCase(addUser.fulfilled, (state, action) => {
      state.loading = true;
      state.users.push(action.payload);
    });

    /*
     * fetchUsers Cases
     */

    builder.addCase(fetchUsers.pending, (state) => {
      state.loading = true;
    });
    builder.addCase(fetchUsers.fulfilled, (state, action) => {
      state.loading = false;
      state.users = action.payload;
    });
    builder.addCase(fetchUsers.rejected, (state) => {
      state.loading = false;
    });
  },
});

export default userSlice.reducer;
export { addUser, fetchUsers };


Enter fullscreen mode Exit fullscreen mode

并且测试应该通过。✅

太棒了!我们刚刚用 Redux、thunk 和 axios mock 写了一些测试。🤩

对你来说有点挑战?添加诸如删除用户、修改用户以及检索用户等功能。

您可以在这里找到具有所有这些功能的代码

结论

本文简要介绍了 Redux 的 TDD。如果你想使用 TDD 编写 React 组件,可以参考我写的这篇文章。

由于每篇文章都可以变得更好,因此欢迎在评论部分提出您的建议或问题。

本文使用bloggu.io发布。免费试用。

文章来源:https://dev.to/koladev/test-driven-development-with-react-redux-thunk-slices-requests-mocking-585i
PREV
提高软件工程师工作效率的工具
NEXT
作为开发人员,如何建立你的在线形象