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
-
这里只用了单元测试 reducer 的部分
-
下面的 reference 列举了 Modifiers 和 Matchers