本节内容导图
- redux测试
- redux准备工作
- 同步测试
- 异步测试
redux测试
对于静态变量,jest会直接覆盖,因此,我们主要是测试里面包含的纯函数,以及reducer中的actions,而actions又包括了同步和异步。整个项目我已经放到了gitee: https://gitee.com/xifeng-canyang/jest-copy-file-and-video,大家可以直接克隆下来:
git clone https://gitee.com/xifeng-canyang/jest-copy-file-and-video.git
准备
安装依赖
npm i redux react-redux @reduxjs/toolkit -S
// reduxjs/toolkit
// 以消除手写 Redux 逻辑中的“样板文件”,防止常见错误,并提供简化标准 Redux 任务的 API
新建文件src/store/index.ts
import { configureStore } from "@reduxjs/toolkit";
import { globalPrompts } from "./global/global";
import { commonSlice } from "./common";
const store = configureStore({
reducer: {
globalPrompt: globalPrompts.reducer,
common: commonSlice.reducer
}
})
export default store;
新建文件src/store/common.tsx
import { createSlice } from "@reduxjs/toolkit";
interface Person {
username: string,
age: string,
href: string,
avatar: string
}
type Per = Record<string,string>
interface CommonState {
person: Per
}
type Per2 = Partial<Per>;
const commonState: CommonState = {
person: {
username: '',
age: '',
href: '',
avatar: ''
}
}
export const commonSlice = createSlice({
name: 'common_slice',
initialState: commonState,
reducers: {
updatePerson: (state,action) => {
state.person = action.payload
}
}
})
export const { updatePerson } = commonSlice.actions;
新建文件store/global/global.tsx
import type { PayloadAction } from "@reduxjs/toolkit";
import { createSlice } from "@reduxjs/toolkit";
interface GlobalType {
permission: boolean
}
const initialState: GlobalType = {
permission: false
}
export const globalPrompts = createSlice({
name: 'globalPrompt',
initialState: initialState,
reducers: {
setPermission: ( state, action: PayloadAction<boolean>) => {
state.permission = action.payload;
}
}
})
export const { setPermission } = globalPrompts.actions;
修改入口文件main.tsx
...
import store from './store';
import { Provider } from 'react-redux';
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<Provider store={store}>
...
</Provider>
</React.StrictMode>,
)
页面中使用
import store from 'src/store';
import { setPermission } from 'src/store/global/global';
- 获取数据使用: store.getState();
- 数据更新使用store.subscribe()
官网:https://cn.redux.js.org/introduction/getting-started
模拟redux我们需要用到第三方插件:redux-mock-store
github地址:https://github.com/reduxjs/redux-mock-store
npm install redux-mock-store --save-dev
npm i --save-dev @types/redux-mock-store
npm i --save-dev sinon
npm i --save-dev @types/sinon
以上,便是测试redux的准备工作,并且不难看出,只是简单的给项目配置上了store状态管理,接下来便是测试,测试分为同步测试和异步测试
同步测试
我们新建测试文件 __tests__/react/redux/sync.test.tsx
import { setPermission,globalPrompts } from 'src/store/global/global';
describe('test redux',() => {
it('test sync2',() => {
const currentState = globalPrompts.reducer(
{
permission: false
},
setPermission(true)
)
console.log(currentState,'current state')
})
})
reducer 本身也是纯函数,它的作用就是改变数据状态,所以这里我们在第一个参数传入当前状态,在第二个参数传入 action, 最后 expect 一下返回的新状态 currentState 就完成测试了.
reducer函数
export type Reducer<S = any, A extends Action = AnyAction> = (
state: S | undefined,
action: A
) => S
异步测试
先来一个异步请求数据的接口,因此修改express/index.ts
...
app.get('/home/user',(req,res) => {
setTimeout(() => {
res.send({
id: '#123',
name: "jack"
})
},2000)
})
...
新建文件store/user/api_user.ts
import axios from 'axios';
// src/apis/user.ts
// 获取用户列表
export interface FetchUserRes {
id: string;
name: string;
}
export const fetchUser = async () => {
const res = await axios.get<FetchUserRes,any>("/api/home/user");
return res;
};
新建reducer,store/user/getUser.tsx
import { createSlice } from "@reduxjs/toolkit";
import { fetchUserThunk } from "./thunks";
interface State {
id: string,
name: string,
status: string
}
const initialState: State = {
id: '',
name: '',
status: "",
}
export const userSlice = createSlice({
name: "user",
initialState,
reducers: {
updateUsername: ( state, action) => {
state.name = action.payload.name;
}
},
extraReducers: (builder) => {
builder.addCase(fetchUserThunk.pending, (state) => {
state.status = "loading";
});
builder.addCase(fetchUserThunk.fulfilled, (state, action) => {
state.status = "complete";
state.name = action.payload.name;
state.id = action.payload.id;
});
builder.addCase(fetchUserThunk.rejected, (state) => {
state.status = "error";
});
},
})
export const { updateUsername } = userSlice.actions;
这个reducer 是官方推荐的新写法:
https://cn.redux.js.org/tutorials/fundamentals/part-8-modern-redux
新建文件store/user/chunks.ts
import { createAsyncThunk } from "@reduxjs/toolkit";
import { fetchUser } from "./api_user";
export const fetchUserThunk = createAsyncThunk(
"user/fetchUserThunk",
async () => {
const response = await fetchUser();
return response.data;
}
);
更新store/index.ts
...
import { userSlice } from "./user/getUser";
const store = configureStore({
reducer: {
...
user: userSlice.reducer
}
})
export default store;
新建profile子组件page/profile/Com/user.tsx
这个组件主要是主要是从store中拿到user的数据,然后进行展示,同时模拟点击按钮发起dispath的一个过程
import React, { FC, useEffect, useState } from "react";
import { Button } from "@mui/material";
import store from "src/store";
import { updateUsername } from "src/store/user/getUser";
import { userSlice } from "src/store/user/getUser";
import { fetchUserThunk } from "src/store/user/thunks";
type User = ReturnType<typeof store.getState >;
const User: FC = () => {
const user = store.getState().user;
console.log(user,'user');
const [uu,setUU] = useState(user)
useEffect(() => {
store.subscribe(() => {
const user = store.getState().user;
console.log(user,'user');
setUU({
...uu,
id: user.id,
name: user.name,
status: user.status
})
})
})
const onClick = async () => {
await store.dispatch(fetchUserThunk());
};
return (
<div>
<h2>用户信息</h2>
{user.status === "loading" && <p>加载中...</p>}
{user.id ? (
<div>
<p>ID:{user.id}</p>
<p>姓名:{user.name}</p>
</div>
) : (
<p>无用户信息</p>
)}
<Button onClick={onClick}>
加载用户按钮
</Button>
</div>
);
};
export default User;
最后新建测试文件 __tests__/react/redux/async-user.test.tsx
import { setPermission,globalPrompts } from 'src/store/global/global';
import { userSlice, updateUsername } from 'src/store/user/getUser';
import configureStore from "redux-mock-store";
import thunk from "redux-thunk";
import { fetchUserThunk } from 'src/store/user/thunks';
import ProfileChild2 from 'src/page/profile/child/child2/p-child2';
import { screen,render,fireEvent,waitFor } from '@testing-library/react';
import * as userApi from 'src/store/user/api_user';
const mockUser = jest.spyOn(userApi,'fetchUser');
mockUser.mockImplementation(
() => {
return new Promise((resolve) => {
resolve({
data: {
id: "#4433",
name: "mikee"
}
})
})
}
)
jest.mock('src/store/user/api_user');
describe('test redux', () => {
it('test async',async () => {
const Com = (
<ProfileChild2 />
)
const container = render(Com);
const loadBtn = screen.getByText('加载用户按钮');
await waitFor(() => {
fireEvent.click(loadBtn);
expect(container).toMatchSnapshot();
})
})
})
最后我们为redux这个测试文件夹单独弄一个配置,这样我们输出测试就只有这一部分文件 __tests__/react/redux/config/redux.config.ts
import type { Config } from '@jest/types'
import jestConfig from '../../../../../jest.config';
export default {
...jestConfig,
rootDir: '../../../../../',
collectCoverageFrom: [
...(jestConfig.collectCoverageFrom as Array<string>),
'!**/*.(ts|tsx)',
'**/__tests__/react/redux/*.test.tsx',
'**/page/*.tsx',
'**/page/**/*.tsx',
'**/store/*.(ts|tsx)',
'**/store/**/*.(ts|tsx)',
],
testMatch: [
'**/src/__tests__/react/redux/*.test.tsx',
'**/src/__tests__/react/redux/**/*.test.tsx',
'!**/src/__tests__/react/redux/async.test.tsx',
]
} as Config.InitialOptions
// 跑通命令 -u --coverage可选
// jest --config='./src/__tests__/react/redux/config/redux.config.ts' -u
注意事项:
- 异步测试我们需要使用createAsyncThunk创建chunk,然后dispatch去触发它,而当它触发的时候,会触发对应reducer中的extraReducers,我们通过判断它是否fulfiled
- axios的get请求需要传两个类型进去,一个是成功响应,一个是错误响应:const res = await axios.get("/api/home/user")
- 因为我们是渲染页面,而页面引入了redux,那么redux自然会被覆盖进去。但是当我们点击的时候是发起了一个异步请求,并且是真实
至此store/user我们综合就覆盖到了80%以上
好了,以上便是redux测试的全部内容,如果对您有一点点帮助,就点赞支持支持~