浅析前端如何做单元测试:jest与mocha对比、如何使用jest进行单元测试及持续监听、如何生成测试覆盖率报告、常用断言方法、如何测试异步函数
一、Jest 和 Mocha 对比选型
至于:前端为什么做单元测试?JavaScript 单元测试的现状,常见单元测试工具:Jest 和 Mocha 的对比,可以看这篇文章了解:https://mp.weixin.qq.com/s/r08NghhRybAwBX9dzcoE1w
从 github starts & issues 以及 npm 下载量角度来看,Jest 的关注度更高、社区更活跃
框架 | 断言 | 异步 | 代码覆盖率 |
---|---|---|---|
Mocha | 不支持(需要其他库支持) | 友好 | 不支持(需要其他库支持) |
Jest | 默认支持 | 友好 | 支持 |
1. Mocha 生态好,但是需要较多的配置来实现高扩展性
2. Jest 开箱即用
无论是受欢迎度和写法上,Jest 都有很大的优势,因此推荐你使用开箱即用的 Jest
二、如何使用 Jest 进行单元测试
1、关于安装依赖的理解:
npm install --save-dev jest
(1)不支持部分 ES6 语法:
原因:nodejs 采用的是 CommonJS 的模块化规范,使用 require 引入模块;而 import 是 ES6 的模块化规范关键字。想要使用 import,必须引入 babel 转义支持,通过 babel 进行编译,使其变成 node 的模块化代码
解决:为了能使用这些新特性,我们就需要使用 babel 把 ES6 转成 ES5 语法
// 安装依赖
npm install --save-dev @babel/core @babel/preset-env
// 根目录加入.babelrc
{ "presets": ["@babel/preset-env"] }
原理:jest 运行时内部先执行( jest-babel ),检测是否安装 babel-core,然后取 .babelrc 中的配置运行测试之前结合 babel 先把测试用例代码转换一遍然后再进行测试
(2)测试 ts 文件:jest 需要借助 .babelrc 去解析 TypeScript 文件再进行测试
// 安装依赖
npm install --save-dev @babel/preset-typescript
// 改写 .babelrc
{ "presets": ["@babel/preset-env", "@babel/preset-typescript"] }
// 为了解决编辑器对 jest 断言方法的类型报错,如 test、expect 的报错,你还需要安装
npm install --save-dev @types/jest
2、总结一下:
(1)安装单元测试必要软件包
npm install -D babel jest @vue/test-utils @babel/preset-env @babel/preset-typescript @types/jest
(2)编写 babel.config.js
文件
module.exports = {
presets: [
["@babel/preset-env", { targets: { node: "current" } }],
"@babel/preset-typescript"
]
}
3、根目录创建tests文件夹:ts 测试文件指定后缀为 .test.ts
或 .spec.ts
4、创建 HelloWorld.test.ts 测试文件:跟着 vue3 的官方单元测试get starting创建hello world测试文件
/**
* @jest-environment jsdom
*/
// 上方注释是指定测试环境,否则报错document is not define
import { mount } from '@vue/test-utils'
const MessageComponent = {
template: '<p>{{ msg }}</p>',
props: ['msg']
}
test('displays message', () => {
const wrapper = mount(MessageComponent, {
props: {
msg: 'Hello world'
}
})
// Assert the rendered text of the component
expect(wrapper.text()).toContain('Hello world')
})
5、在 package.json
文件中添加 script
{
"script":{
"test": "jest"
}
}
6、终端运行:npm run test
7、测试实例:
// /src/utils/sum.ts
export function sum(a: number, b: number) {
return a + b
}
// tests/sum.test.ts
import { sum } from '../src/utils/sum'
test('adds 1 + 2 to equal 3', () => {
expect(sum(1, 2)).toBe(3)
})
test('adds "2" + 2 to equal 22', () => {
expect(sum('2', 2)).toBe('22')
})
可以看到 test 均成功。(下面那个 '2' ts 会给提示,但是不影响结果成功,起初我以为会错误的,原来不是)
那我们试着给个错误试下:它会提示你哪里出错了
三、更多使用
1、持续监听:热更新
为了提高效率,可以通过加启动参数的方式让 jest 持续监听文件的修改,而不需要每次修改完再重新执行测试用例
// 在 package.json 添加
"scripts": {
"test": "jest --watchAll"
},
这样的话,如果测试出现错误,你改了之后,就会自动支持单元测试,而不需要在输入命令 npm run test 进行测试,类似于一个热更新的功能。
2、生成测试覆盖率报告
什么是单元测试覆盖率:单元测试覆盖率是一种软件测试的度量指标,指在所有功能代码中,完成了单元测试的代码所占的比例。有很多自动化测试框架工具可以提供这一统计数据,其中最基础的计算方式为:单元测试覆盖率 = 被测代码行数 / 参测代码总行数 * 100%
如何生成呢?加入 jest.config.js 文件
module.exports = {
collectCoverage: true, // 是否显示覆盖率报告
// 告诉 jest 哪些文件需要经过单元测试测试
collectCoverageFrom: ['/tests/helloWorld.test.ts', 'src/utils/sum.ts'],
}
再次测试就会生成:
参数名 | 含义 | 说明 |
---|---|---|
% stmts | 语句覆盖率 | 是不是每个语句都执行了? |
% Branch | 分支覆盖率 | 是不是每个 if 代码块都执行了? |
% Funcs | 函数覆盖率 | 是不是每个函数都调用了? |
% Lines | 行覆盖率 | 是不是每一行都执行了? |
3、设置单元测试覆盖率阀值
个人认为既然在项目中集成了单元测试,那么非常有必要关注单元测试的质量,而覆盖率则一定程度上客观的反映了单测的质量,同时我们还可以通过设置单元测试阀值的方式提示用户是否达到了预期质量。
// jest.config.js 文件加入
// 修改配置
module.exports = {
collectCoverage: true, // 是否显示覆盖率报告
// 告诉 jest 哪些文件需要经过单元测试测试
collectCoverageFrom: ['/src/views/HelloWorld.vue', 'src/utils/sum.ts'],
coverageThreshold: {
global: {
statements: 90, // 保证每个语句都执行了
functions: 90, // 保证每个函数都调用了
branches: 90, // 保证每个 if 等分支代码都执行了
},
},
};
上述阀值要求我们的测试用例足够充分,如果我们的用例没有足够充分,则下面的报错将会帮助你去完善
export function sum(a: number, b: number) {
return a + b
}
export function add(a: number, b: number) {
return a - b
}
export function multi(a: number, b: number) {
return a * b
}
我又加了 2 个函数,但是单元测试,没加,所以可以看到覆盖率是 33.33%,同时可以看到未覆盖的行数是 5 - 8 行
在 sum.test.ts 中加上另外这 2 个函数,再测试,就可以是 100% 了
import { sum, add, multi } from '../src/utils/sum'
test('adds "2" + 2 to equal 22', () => {
expect(add(4, 2)).toBe(2)
})
test('adds "2" + 2 to equal 22', () => {
expect(multi(3, 2)).toBe(6)
})
4、常用断言方法
断言方法有很多,可以去 Jest 官网 API (https://www.jestjs.cn/docs/expect) 部分查看
// .not 修饰符允许你测试结果不等于某个值的情况
expect(sum(2, 4)).not.toBe(5);
// .toEqual 匹配器会递归的检查对象所有属性和属性值是否相等,常用来检测引用类型
expect(getUserInfo()).toEqual(getUserInfo());
//.toHaveLength 可以很方便的用来测试字符串和数组类型的长度是否满足预期
expect(getIntArray(3)).toHaveLength(3);
// .toMatch 传入一个正则表达式,它允许我们来进行字符串类型的正则匹配
expect(getUserInfo().name).toMatch(/mo/i);
// .toContain 匹配对象中是否包含
expect(names).toContain('jim');
5、如何测试异步函数
// 获取用户信息 ./servers/fetchUser.js
export const fetchUser = () => {
return new Promise((resole) => {
setTimeout(() => {
resole({
name: 'moji',
age: 24,
})
}, 2000)
})
}
// ./test/fetchUser.test.js
import { fetchUser } from '../src/fetchUser';
test('fetchUser() 可以请求到一个用户名字为 moji', async () => {
const data = await fetchUser();
expect(data.name).toBe('moji')
})
这里你可能看到这样一条报错
这是因为 @babel/preset-nev 不支持 async await 导致的。这时候就需要对 babel 配置进行增强, 可以安装这个插件解决
npm install --save-dev @babel/plugin-transform-runtime
// 同时改写 .babelrc
{
"presets": ["@babel/preset-env", "@babel/preset-typescript"],
"plugins": ["@babel/plugin-transform-runtime"]
}
再次运行就不会出现报错了。