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测试的全部内容,如果对您有一点点帮助,就点赞支持支持~

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,284评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,115评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,614评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,671评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,699评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,562评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,309评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,223评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,668评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,859评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,981评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,705评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,310评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,904评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,023评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,146评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,933评论 2 355

推荐阅读更多精彩内容