4. redux-saga的测试
我发现 redux-saga 官方的测试用例使用的是 tape ,所以我先学会使用这个,然后看怎么使用 jest 来完成。安装就不用说了,直接上代码:
export function* syncAdd() {
yield put({type: ADD});
}
对于这个简单的怎么写测试呢,很简单,我们只要不要想这个是 saga ,而是 js 代码就可以了,像这些 js 框架或者库什么的都当成 js ,其实本来也是 js ,但有时候可能有更好的测试方式,如果没有就当 js 进行:
import test from 'tape'; // 引入这个库
test('测试普通 saga', (t) => {
const generator = syncAdd();
t.deepEqual(generator.next().value, put({type: ADD}), '这是什么玩意');
t.ok(generator.next().done, '所有的执行完成');
t.end();
});
这个测试好像没什么用,一般都是有传值的,于是有下面的:
export function* syncMul(count) {
yield put({type: MUL, count});
}
测试:
test('测试带有参数的 saga', (t) => {
const count = 10;
const generator = syncMul(count);
t.deepEqual(
generator.next().value,
put({type: MUL, count: 10}),
'这是带有参数的值为10',
);
t.ok(generator.next().done, '所有的执行完成了--');
t.end();
});
写了这么多,应该怎么测试这些呢?可以照葫芦画瓢,先装下面的库:
yarn add @babel/node @babel/core cross-env tap-spec --dev
或
npm install @babel/node @babel/core cross-env tap-spec -D
然后在 package.json 文件的 “ scripts ” 下写一个属性,假如叫: tape ,像下面:
{
...
"scripts": {
...
"tape": "cross-env NODE_ENV=test babel-node src/app/sagas/counts.tape.js | tap-spec"
},
...
}
然后执行:
yarn tape
或
npm run tape
这个时候就会看到通过的
除了上面的这种方法还可以直接执行下面的语句:
npx babel-node 你的测试文件
只不过得到的界面有点丑
接下来就是异步了,一般使用 redux-saga 是为了写异步代码,所以下面的可能更重要,只不过使用这个 tape 也很简单:
function delay(time, count) {
return new Promise((resolve, reject) => {
let timer = setTimeout(() => {
time && clearTimeout(timer);
resolve(count + 10);
}, time);
});
}
export async function request(count) {
const tempCount = await delay(1000, count);
return tempCount;
}
export function* asyncAdd() {
const count = yield call(request, 10);
yield put({type: ADD, count});
}
我们可以看到我写的异步函数是定时器模拟的,返回的就是传入的数据 + 10 。
test('测试异步的 saga', (t) => {
const generator = asyncAdd();
let count;
t.deepEqual(
generator.next(count).value,
call(request, 10),
'开始调用异步的请求',
);
t.deepEqual(
generator.next().value,
put({type: ADD, count}),
'将获取得到的值传入 redux 中',
);
t.ok(generator.next().done, '所有的执行完成了--');
t.end();
});
这里注意一下返回值,只需要将要返回的值放到 generator.next(count).value 中,这样就自然拿到值了,于是就可以开开心心的传入的 put({type: ADD, count}) 中了。
如果看到这里还是没明白的,我们来看看 deepEqual 这个函数:
// 先看 tape 层面的
function tapeDeepEqual(a, b, msg, extra) {
this._assert(deepEqual(a, b, { strict: true }), {
message: defined(msg, 'should be equivalent'),
operator: 'deepEqual',
actual: a,
expected: b,
extra: extra
});
}
Test.prototype.deepEqual
= Test.prototype.deepEquals
= Test.prototype.isEquivalent
= Test.prototype.same
= tapeDeepEqual;
// 再看 deep-equal 的
function deepEqual(actual, expected, options) {
var opts = options || {};
// 7.1. All identical values are equivalent, as determined by ===.
if (opts.strict ? is(actual, expected) : actual === expected) {
return true;
}
// 7.3. Other pairs that do not both pass typeof value == 'object', equivalence is determined by ==.
if (!actual || !expected || (typeof actual !== 'object' && typeof expected !== 'object')) {
return opts.strict ? is(actual, expected) : actual == expected;
}
/*
* 7.4. For all other Object pairs, including Array objects, equivalence is
* determined by having the same number of owned properties (as verified
* with Object.prototype.hasOwnProperty.call), the same set of keys
* (although not necessarily the same order), equivalent values for every
* corresponding key, and an identical 'prototype' property. Note: this
* accounts for both named and indexed properties on Arrays.
*/
// eslint-disable-next-line no-use-before-define
return objEquiv(actual, expected, opts);
}
可以看到:
// tape 帮我们传入了 strict 并且值为: true
function tapeDeepEqual(a, b, msg, extra) {
this._assert(deepEqual(a, b, { strict: true }), {
message: defined(msg, 'should be equivalent'),
operator: 'deepEqual',
actual: a,
expected: b,
extra: extra
});
}
// 和 deep-equal 库的关于这段的判断
// 7.1. All identical values are equivalent, as determined by ===.
if (opts.strict ? is(actual, expected) : actual === expected) {
return true;
}
我们就不往下追究了,直接看看官方的定义:
也就是如果传入的是 true ,那么就使用的是递归比较,否值就是值比较。
看到这里应该看到测试的规则,也就是首先获得 generator ,然后就进行比较 generator.next(count).value 和 真正执行的,前一个是实际值,后一个是期望值,如果不相等说明就出现问题了。
下面我们看怎么使用 jest 来完成测试。 这个时候就把 redux-saga 看成是彻头彻尾的 js 函数,那么测试就很好进行了。首先尝试最简单的测试,下面我会把具体的函数实现和测试写到一块方便比较对比:
export function* syncAdd() {
yield put({type: ADD});
}
// 测试
it('普通的 saga 的测试', () => {
const generator = syncAdd();
expect(generator.next().value).toEqual(put({type: ADD}));
});
发现好像更简单了,下面看看带有参数的:
export function* syncMul(count) {
yield put({type: MUL, count});
}
// 测试
it('测试带有参数的 saga 的测试', () => {
const count = 10;
const generator = syncMul(count);
expect(generator.next().value).toEqual(put({type: MUL, count: count}));
expect(generator.next().done).toEqual(true);
});
这里的 toEqual 就是递归比较,所以效果跟上面的相同,具体的差别还需要看 toEqual 和 deepEqual 函数实现的差别。
其他的就先不写了,都差不多。以后实战会把遇到的坑和现在没注意到的地方都记录下来。
5. 组件的测试
对于前端来说,这个才是重头戏,毕竟逻辑什么的一般都是后台的,前端能处理的逻辑也不会太多,最主要的是如果把大量逻辑放到前端,即便逻辑处理和页面渲染分开也会导致页面等情况,只不过小逻辑还是自己处理比较好,跟后台反复通信的代价是昂贵的。由于我负责的是 react-native 的工作,所以我主要先弄这个,其他的后面慢慢补上,有可能没有,如果谁刚好负责那一块也写好了博客,可以告诉我,我把链接放到这里,方面不明白的人能够很容易的找到。
首先到 jest 官网的 react-native 那一部分看看: Testing React Native Apps 。根据上面的操作即可,只不过默认情况下是支持的,如果你的版本太低了就不要考虑了,官方说只支持 0.38 以上的,你可以检查一下你的 package.json 来看看是否支持,这里都有可以看一下。
如果你是混合开发,可能跟我们直接使用 react-native 的不一样,只不过没关系,应该都是大同小异。
先写一个简单的:
const TestFunction = () => {
useEffect(() => {
add(1, 2, 3, 4);
}, []);
const onClick = useCallback(() => {
console.log('似乎就是这样,我修改一下函数的定义');
}, []);
return (
<View style={styles.container}>
<Text>这是学习 jest 测试。</Text>
<Pressable onPress={onClick}>
<Text>按钮</Text>
</Pressable>
</View>
);
};
测试代码:
it('测试简单的组件 TestFunction', () => {
const tree = renderer.create(<TestFunction />).toJSON();
expect(tree).toMatchSnapshot();
});
这个代码很简单,如果你是第一次写,那么会在你的对应文件夹下生成 __snapshots__ 文件夹,下面会生成一个文件 index.test.js.snap 的类似文件,这个文件其实就是快照文件,如果下次修改了,那么就会出现测试失败,此时就需要你检查一下修改是不是无意改动还是有意改动,或者有没有改错什么的。
这里说的修改,指的是样式和组件的修改,不包含函数的修改,比如上面, useEffect 里面的修改并不会影响, 同时我修改了 add 函数也不会影响, 同时修改了 onClick 函数也不会影响。
6. 总结
第一次的学习就此结束,下面会有关于 jest 的更多用法,也就是相关钩子的使用。同时全部完成以后,我会出实践的,实践类的是和项目高度挂钩的,出现的任何问题都会记录,同时也会随着更新迭代,相关的记录也会保存。我之所以做这一整套学习记录,实践记录和迭代记录等,主要是我发现关于测试类的教程真的少,即便有也都是写介绍语法的,没有和项目挂钩,最先开始的时候就看得我云里雾里,真的还不如看官网。如果说的不明白的望看到的人提出来,我会尽可能的写详细,让大家都能看懂,而不是云里雾里。有时候网上的教程之所以出现很多云里雾里的教程,最大程度就是因为,他学会了再看的时候发现很多都觉得没必要讲或者不值得讲,这也是从学习的一刚开始就选择记录,不然我也会出现这样的情况。