jest的redux测试

本节内容导图

  • 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';

模拟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测试覆盖率

好了,以上便是redux测试的全部内容,如果对您有一点点帮助,就点赞支持支持~

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容