我是这样在 React 中实践 TDD 编程的
在Redux中编写测试听起来肯定有悖直觉。如果你使用了Redux,它可能看起来更加复杂。
然而,在添加功能之前编写测试有助于编写更好的代码,因为你预先考虑了将使用的设计模式、体系结构和变量的名称。
我们正在构建一个用户管理仪表板。基本上,使用Redux,我们想执行CRUD操作。
用户可以:
创建用户
更新用户
删除用户
获取用户或用户列表
这个小项目中的用户将有四个属性:
id\name\username\email
为了简单起见,我们不编写UI代码。我们将主要关注于创建一个测试环境,编写测试,并确保我们能够处理我们想要的内容。
开始
首先,创建一个简单的React项目。
yarn create react-app react-redux-test-driven-development
一旦创建了项目,通过运行项目来确保一切正常。
cd react-redux-test-driven-development
yarn start
接下来,我们希望安装redux
包和一个mock
适配器。mock适配器将帮助我们模拟服务器上的请求。
yarn add @reduxjs/toolkit axios-mock-adapter axios
测试 mock 数据
在src
目录中,创建一个名为utils
的新目录。然后,创建一个名为tests.data.js
的文件。
该文件将包含以下方法和变量:
mockNetWorkResponse
:在默认实例上创建mock
适配器,并模拟到所需端点的任何GET或POST请求;getCreateUserResponse
:返回/user/
上POST请求的响应;getUserListResponse
: 返回对/user/
的GET请求的响应。
让我们来写这些方法:
import axios from "axios";
import MockAdapter from "axios-mock-adapter";
const getCreateUserResponse = {
id: 3,
name: "Clementine Bauch",
username: "Samantha",
email: "[email protected]"
};
const getUserListResponse = [
{
id: 1,
name: "Leanne Graham",
username: "Bret",
email: "[email protected]"
},
{
id: 2,
name: "Ervin Howell",
username: "Antonette",
email: "[email protected]"
},
];
// 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,
};
太棒了!准备好mock
适配器后,我们就可以专注于初始化存储和并编写测试了。
编写测试
这是最有趣的部分。让我们开始TDD
。
首先,让我们创建并配置存储。在src
目录中,创建一个名为index.js
的新目录。在这个文件中,初始化存储。
import { configureStore } from "@reduxjs/toolkit";
import { combineReducers } from "redux";
const rootReducer = combineReducers({
// Adding the reducers
});
export const store = configureStore({
reducer: rootReducer,
});
编写 userSlice
“slice”是应用程序中单个特性的Redux reducer
逻辑和动作的集合,通常定义在单个文件中。userSlice
将有actions
和reducer
来执行CRUD操作。
slice
的默认状态应该是一个空数组,毕竟,我们处理的是用户。
让我们通过编写一个测试:
在src/store
中创建一个名为slices
的新目录。
在这个目录中,添加一个名为user.test.js
的文件。这个文件将包含我们将为userSlice
编写的测试。
第一个测试是确保存储是空的或未定义的。初始状态可能是这样的:
const initialState = {
users: [],
loading: false,
error: null
};
让我们尝试写一下这个测试:
测试初始 State。在user.test.js文件中,添加以下测试:
import reducer, {
initialState,
} from "./user";
/**
* Testing the initial state
*/
test("Should return initial state", () => {
expect(
reducer(undefined, {
type: undefined,
})
).toEqual(initialState);
});
现在运行yarn test
命令。测试将失败❌
完全正常。我们还没有定义userSlice、reducer
和初始状态
。
在slice
目录中,创建一个名为user.js
的文件。
export const initialState = {
users: [],
loading: false,
error: null
};
export const userSlice = createSlice({
name: "users",
initialState: initialState,
extraReducers: () => {
},
});
export default userSlice.reducer;
另外,在store/index.js
中注册 slice reducer
。
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,
});
然后再次运行测试✅
测试用户创建:为此,我们需要编写一个
thunk
。thunk
是一个函数,它以store
的dispatch
方法作为参数,然后在API或副作用
完成后使用它来dispatch
同步操作。
首先,让我们为这个特性编写测试。
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);
});
在这个测试中,我们是:
在进行更新之前,保存以前的状态并将
users
属性修改为预期状态。这将有助于我们比较下一个状态。dispatch
一个action
,并确保它已完成,并比较预期状态和实际状态。
同样,测试将失败。让我们为创建用户特性添加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 };
并再次运行测试,它应该会通过。✅
编写测试获取用户列表
首先,让我们为这个特性编写测试。
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);
});
});
这样测试会失败。
让我们加上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 };
测试通过。✅
太棒了!我们刚刚使用Redux、thunk和axios mock
编写了一些测试🤩
对你来说有点挑战吗?添加诸如删除用户、修改以及检索用户等功能。
结论
在本文中,我们快速介绍了使用Redux
的TDD
。如果你希望使用TDD编写React组件,你可以查看我写的这篇文章。
如果你发现这个指南很有用,请不要忘记
点赞👍🏻和在看哦!
我每天都会发这样的帖子,
所以请关注我,了解更多信息。❤️