React 测试笔记 03 - 测试 Redux 中 Reducer 状态变化

React 测试笔记 03 - 测试 Redux 中 Reducer 状态变化

这段时间都在重构代码,把本来奇奇怪怪(singleton)的实现改成用 redux 的实现,然后就突然想到......即然 redux 的改变不涉及到 UI 的改变,那么是不是说可以单独写 redux 的测试......?

找了一下资料,发现比想象中的简单很多,所以就稍微记一下

要求简述

这里的案例主要就是通过用户权限,然后 map 对应的页面的读写(全套为增删改查)权限,基本逻辑是这样的:

  • 权限分为 read_only、full、A、B、C(A/B/C 为抽象的权限,基本上说对应一下用户可以获得不同分类的权限)

  • 每个权限对应着每个页面的读写操作

    如 full 代表着用户对所有页面都有读写操作,read_only 相反

    A/B/C 给予用户 页面 A/页面 B/页面 C 的读写权限

  • 用户可以有不同的权限

  • 更高的权限将会复写较低的权限

    理论上来说这是不可能发生的事情,不过假设用户同时有 read_only 和 full,那么 full 将会复写 read_only 的权限

    比较通常发生的是用户可能会有 A/B/C 这样的权限,但是其中对某些页面会有覆盖操作,如权限 B 给予 B 页面读写操作,权限 C 可能对 B 页面有改的权限,但是没有增删

  • 当前 redux 状态只负责授予权限,具体权限的处理则是在 路由/component 处进行处理

实现简述

具体的实现就是 dispatch event 实现,权限的 map 则是通过 {page: permission} 的方式进行存储,至于要求说是取最高权限,因此 permission 的实现方式采用数字,在 redux 中使用 Math.max() 的方式取最大值

需要注意的是,这里在实现的时候可能会出现 type casting 的异常,如 0 | 1 | 2 | 3 is not compatible with number 之类的,我的实现方式是用 Math.max() as (typeof T)[keyof typeof T] 的方式进行一个转型

当然,cv 太多的话还是建议抽一个函数取实现

这样的话通过 roles.filter(role => role in SOME_CONST) 中可以获取需要 map 的权限,再遍历数组更新权限即可

测试实现

首先我先 mock 了一下整个 app component:

ts 复制代码
jest.mock('../../App', () => ({}));

这一步是假设 app component 已经渲染完毕了,因为其他的 redux 对一些 util------有可能是异步的操作------有一些依赖的关系,然后目前 jest 没有找到这些 util,因此就会抛出找不到 util 的异常,所以这里先 mock 一下虚构的 app,让 jest 知道内部的 util 实现不重要,最终结果就是所有的组件已经正常渲染完毕

其主要原因也是我们的项目有一些 context wrapper 了:

tsx 复制代码
// redux
<Provider store={store}>
  {/* async code,从 API 处获得权限,再传到 redux 中 */}
  {/* 如果不 mock,从这里就会报错 */}
  <AccessContext>
    <OtherContext>
      <App />
    </OtherContext>
  </AccessContext>
</Provider>

之后的操作非常的简单,jest 已经默认 app 可以正确渲染,因此这里只需要出发 RTK 的状态变化,并且检查状态变化即可,如:

ts 复制代码
import {
  ISliceType,
  sliceReducer,
  exportedAction,
} from '../../store/slices/state/keycloak';

jest.mock('../../App', () => ({}));

// 这里可以实现一些状态用来方便 test,而不用手写一堆代码
const initialState: ISliceType = {}; // AKA readOnly
const fullAccess: ISliceType = {};
const aAccess: Partial<ISliceType> = {};
const bAccess: Partial<ISliceType> = {};

const validationHelper = (
  grantedPerm: ISliceType,
  checkAccess: Partial<ISliceType>
): boolean => {
  // do some check
  // 也许可以遍历 checkAccess 的 key,保证 grantedPerm[key] >= checkAccess[key] 这种

  return true;
};

describe('test user has readOnly access', () => {
  test('initial state is set to readOnly', () => {
    expect(sliceReducer(undefined, { type: undefined })).toEqual(initialState);
  });

  test('user only has readOnly access', () => {
    const payload = { role: ['readonly'] };
    expect(sliceReducer(initialState, exportedAction(payload))).toEqual(
      initialState
    );
  });

  test('user has multiple role', () => {
    const payload = { role: ['readonly', 'aAccess'] };

    const grantedPerm = sliceReducer(
      initialState,
      exportedAction(payload)
    ).permission;

    expect(validationHelper(grantedPerm, initialState)).toBeTruthy();
    expect(validationHelper(grantedPerm, aAccess)).toBeTruthy();
    expect(validationHelper(grantedPerm, bAccess)).toBeFalsy();
  });
});

这里检查了三种状态:

  • undefined 相当于触发 Reducer 的初始化,因此返回初始状态

    因为初始状态直接声明了,所以可以直接用 toEqual 去测试

  • 测试只读状态

    这里假设返回的状态依旧比较简单,因此仍旧可以使用 toEqual 去和整个状态测试

  • 测试更复杂的状态

    这个就是用的第三个方法,主要是用 toBeTruthy()toBeFalsy() 测试

    假设说页面有十几二十个,然后有四五组权限,全都手写的话会引入更多的 human error,这个时候就可以考虑将一些常量抽出来

    比如说 a 权限对应的 {page: access} 可以单独抽出来做一个变量------这个部分是可以实现、测试公用的,然后再写一个 helper function------这里可以用于测试给予的权限是否大于等于变量中的权限,而没有赋值的权限是否小于等于当前权限,这个根据具体需求具体实现

    我这里的 helper 直接返回了 boolean,使用上面列举的两个方式去测试,不过实际上还可以搭配其他的 Modifiers 和 Matchers 去测试,不仅仅是用 truthy/falsy

    这个不是必须的,因为测试有些情况下就是会 cv 一些代码,而且一些复杂的情况下,仅返回 boolean 的 helper 也许不是这么的好用,那么最差情况下就是得一遍遍手写,然后通过 npm test 去运行测试结果

reference

  • Writing Tests

    这里只用了单元测试 reducer 的部分

  • Expect

    下面的 reference 列举了 Modifiers 和 Matchers

相关推荐
GISer_Jing19 分钟前
[总结篇]个人网站
前端·javascript
疯狂的沙粒41 分钟前
在web-view 加载的本地及远程HTML中调用uniapp的API及网页和vue页面是如何通讯的?
前端·uni-app·html
小妖6661 小时前
html 滚动条滚动过快会留下边框线
前端·html
heroboyluck1 小时前
Svelte 核心语法详解:Vue/React 开发者如何快速上手?
前端·svelte
海的诗篇_1 小时前
前端开发面试题总结-JavaScript篇(二)
开发语言·前端·javascript·typescript
琹箐1 小时前
ant-design4.xx实现数字输入框; 某些输入法数字需要连续输入两次才显示
前端·javascript·anti-design-vue
程序员-小李1 小时前
VuePress完美整合Toast消息提示
前端·javascript·vue.js
Uyker2 小时前
从零开始制作小程序简单概述
前端·微信小程序·小程序
EndingCoder6 小时前
React从基础入门到高级实战:React 实战项目 - 项目三:实时聊天应用
前端·react.js·架构·前端框架
阿阳微客7 小时前
Steam 搬砖项目深度拆解:从抵触到真香的转型之路
前端·笔记·学习·游戏