Jest单元测试环境搭建

Jest单元测试环境搭建

依赖说明

下面列出了一些使用 Jest 测试相关的依赖说明及配置,可以根据不同的需求进行安装。如果你想快速安装运行,也可以直接跳到 快速开始

测试运行器—Jest

官方推荐的其中一种配置简单、集成完善的测试机运行器。

1
npm i --save-dev jest

配置 package.json 设置 scripts 启动,更多常用配置请跳转到Jest 的脚本

1
2
3
4
5
6
7
8
9
{
...
"scripts": {
"test": "jest",
"test:init": "jest --init",
"test:coverage": "jest --coverage"
},
...
}

支持 TS

1
npm i ts-jest @types/jest

配置 jest.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
...
transform: {
transform: {
'^.+\\.(t|j)sx?$': [
'babel-jest', {
presets: [
'@babel/preset-typescript',
],
},
],
},
},
...

Babel 转译

虽然最新版本的 Node 已经支持绝大多数的 ES2015 特性,但是我们还想在测试中使用 ES modules 语法。所以需要需要安装 babel-jest进行语法转义。

1
npm i --save-dev babel-jest       @babel/core @babel/preset-env

配置 babel.config.json

1
2
3
4
5
6
7
8
9
10
11
{
"presets": [
[
"@babel/preset-env", {
"targets": {
"node": "current"
}
}
]
]
}

单元测试实用工具库—vue-test-utils

Vue Test UtilsVue.js 官方的单元测试实用工具库,通过两者结合来测试验证码组件,覆盖各功能测试

1
npm i --save-dev @vue/test-utils

处理单文件组件

告诉 Jest 如何处理 *.vue 文件

1
npm i --save-dev vue-jest

配置 jest.config.js文件

1
2
3
4
5
...
transform: {
'^.+\\.vue$': 'vue-jest',
},
...

使用 Eslint 检测

1
npm i --save-dev eslint-plugin-jest

配置 .eslintrc文件

1
2
3
4
5
...
"extends": [
"plugin:jest/recommended"
],
...

快照格式化

默认快照测试,输出文件包含大量转义符号”/“,需要通过jest-serializer-vue插件处理,可以自定义快照输出目录位置

1
npm i --save-dev jest-serializer-vue

配置 jest.config.js文件

1
2
3
4
5
6
7
8
...
// 处理快照文件转义符
snapshotSerializers: ["<rootDir>/node_modules/jest-serializer-vue"],
// 生成测试报告文件类型
coverageReporters: ['json', 'html'],
// 覆盖率报告的目录,测试报告所存放的位置
coverageDirectory: './coverage-reports',
...

浏览器显示测试结果

执行测试后,会在项目最外层生成test-report.html,点开即可查看结果

1
npm i --save-dev jest-html-reporter

配置 jest.config.js文件

1
2
3
4
5
6
7
8
9
10
...
reporters: [
// 可以自定义报告
["./node_modules/jest-html-reporter", {
"pageTitle": "Test Report"
}]
],
或者
"testResultsProcessor": "./node_modules/jest-html-reporter"
...

Jest 基本配置

以下只列出常用的配置,更多配置请参考https://jestjs.io/docs/configuration

可以通过npm run test:init创建jest.config.json 文件,里面包含全部配置的说明。也可以手动创建,并配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
const path = require('path');

module.exports = {
// 匹配测试用例的文件
testMatch: [
"**/__tests__/**/*.[jt]s?(x)",
"**/?(*.)+(spec|test).[tj]s?(x)"
],
reporters: [
"default",
// 需要安装依赖:jest-html-reporter
["./node_modules/jest-html-reporter", {
"pageTitle": "Test Report"
}]
],
// 激活测试结果通知
notify: true,
// 每次测试前清除模拟调用和实例
clearMocks: true,
// 生成测试报告文件类型
coverageReporters: ['json', 'html'],
// 覆盖率报告的目录,测试报告所存放的位置
coverageDirectory: './coverage-reports',
// 处理快照文件转义符
snapshotSerializers: ["<rootDir>/node_modules/jest-serializer-vue"],
// 转换器 -- ts需要配置该选项
transform: {
'^.+\\.(t|j)sx?$': [
'babel-jest', {
presets: [
'@babel/preset-typescript',
],
},
],
},
};

示例说明

编写测试用例

要测试的代码

1
2
3
4
5
// fun.js
function foo(a, b) {
return a + b;
}
export default foo;

测试用例

这里只是一个简单的测试用例,更多常用方法、匹配器等跳转 Vue Test Utils 常用的 APIJest 常用 API

1
2
3
4
5
// test.js
import foo from './fun.js';
test('add', () => {
expect(foo(1, 2)).toBe(3);
});

运行测试

默认测试命令

1
npm run test
测试结果![image-20210330142642772](/Users/zhangjinxiu/Library/Application Support/typora-user-images/image-20210330142642772.png)
html 显示

安装jest-html-reporter并在jest.config.json中进行配置(查看上述 Jest 基本配置),执行测试后,根目录会生成一个test-report.html文件,打开页面如下图所示:

![image-20210330140206336](/Users/zhangjinxiu/Library/Application Support/typora-user-images/image-20210330140206336.png)

测试并统计覆盖率命令

1
npm run test:coverage
测试结果![image-20210330142829459](/Users/zhangjinxiu/Library/Application Support/typora-user-images/image-20210330142829459.png)

执行测试后,根目录下会生成目录coverage-reports/index.html文件(文件目录可以自定义,参考jest.config.js中的配置),打开页面如下图所示:![image-20210330142327049](/Users/zhangjinxiu/Library/Application Support/typora-user-images/image-20210330142327049.png)

  • 语句覆盖率(statements)是否每个语句都执行了
  • 分支覆盖率(branches)是否每个函数都调用了
  • 函数覆盖率(functions)是否每个 if 代码块都执行了
  • 行覆盖率(lines) 是否每一行都执行了

Jest 的脚本

"test": "jest" :对项目目录下测试文件进行 Jest 测试

"test:help": "jest --help":查看 cli 命令

"test:debug": "jest --debug":debug

"test:verbose": "jest --verbose":以层级方式在控制台展示测试结果

"test:noCache": "jest --no-cache":设置是否缓存,有缓存会快

"test:init": "jest --init":根目录下创建jest.config.js文件

"test:caculator": "jest ./test/caculator.test.js":单文件测试

"test:caculator:watch": "jest ./test/caculator.test.js --watch":单文件监视测试

"test:watchAll": "jest --watchAll":监视所有文件改动并测试

"test:coverage": "jest --coverage":测试覆盖率

Jest 常用 API

全局函数

  • describe(name, fn) 把相关测试组合在一起
  • test(name, fn) 测试方法
  • expect(value) 断言
  • beforeEach(fn) 在每一个测试之前需要做的事情,比如测试之前将某个数据恢复到初始状态
  • afterEach(fn) 在每一个测试用例执行结束之后运行
  • beforeAll(fn) 在所有的测试之前需要做什么
  • afterAll(fn) 在测试用例执行结束之后运行

匹配器

  • toBe 使用 === 来测试完全相等

  • toEqual 递归检查对象或数组的每个字段

  • toContain 判断数组或字符串中是否包含指定值

  • toContainEqual 判断数组中是否包含一个特定对象

  • toBeNull 只匹配 null

  • toBeUndefined 只匹配 undefined

  • toBeDefinedtoBeUndefined 相反

  • toBeTruthy 匹配任何 if 语句为真

  • toBeFalsy 匹配任何 if 语句为假

  • toThrow 特定函数抛出一个错误

  • toMatch 正则匹配

  • toThrow 要测试的特定函数会在调用时抛出一个错误

  • resolves 和 rejects 用来测试 promise

  • toHaveBeenCalled 判断一个函数是否被调用过

  • toHaveBeenCalledTimes 判断函数被调用过几次

  • toBeGreaterThan 大于

  • toBeGreaterThanOrEqual 大于等于

  • toBeLessThan 小于

  • toBeLessThanOrEqual 小于等于

  • toBeCloseTo 浮点数比较

  • toMatchSnapshot() 记录快照

  • 第一次调用的时候,会把 expect 中的值以字符串的形式存储到一个.snap文件中
  • 多次运行快照时,每次都会与上一次的快照文件内容对比,相同则测试通过,不同则测试失败
  • 重新生成快照文件 npm run test -- -u

Vue Test Utils 常用的 API

挂载组件

  • mout(component)创建一个包含被挂载和渲染的 Vue 组件的 Wrapper
1
2
3
4
5
6
7
const wrapper = mount({
template: `<div>hello word</div>`,
components: {
OtherComponent,
},
});
const wrapper2 = mount(MyComponent);
  • shallowMount 只挂载一个组件不渲染子组件
1
2
3
4
5
6
7
const wrapper = shallowMount({
template: `<div>hello word</div>`,
components: {
OtherComponent,
},
});
const wrapper2 = mount(MyComponent);

参数

  • props 传入组件的参数
1
2
3
4
5
6
7
const mountCom = (template, options) => mount(MyComponent, {
...
props: {
active: 'default',
},
...
};
  • slots 设置组件的插槽
1
2
3
4
5
6
7
8
const mountCom = (template, options) => mount(MyComponent, {
...
slots: {
default: 'Default',
customerName: OtherComponent,
},
...
});
  • data 传入组件数据
1
2
3
4
5
6
7
8
cconst mountCom = (template, options) => mount(MyComponent, {
...
data() {
return {
message: 'world',
};
...
});
  • attrs 设置组件的属性
1
2
3
4
5
6
7
8
ccconst mountCom = (template, options) => mount(MyComponent, {
...
attrs: {
id: 'hello',
disabled: true,
},
...
});

wrapper 的方法

  • attributes 返回DOM节点属性值
1
2
3
4
5
6
test('attributes', () => {
const wrapper = mount(Component);

expect(wrapper.attributes('id')).toBe('foo');
expect(wrapper.attributes('class')).toBe('bar');
});
  • classes 返回元素上class的数组 一般用于测试样式
1
2
3
4
5
6
7
test('classes', () => {
const wrapper = mount(Component);

expect(wrapper.classes()).toContain('my-span');
expect(wrapper.classes('my-span')).toBe(true);
expect(wrapper.classes('not-existing')).toBe(false);
});
  • emitted 返回组件发出的所有事件 一般用于测试派发事件及传输的参数
1
2
3
4
5
6
7
8
test('emitted', () => {
const wrapper = mount(Component);

expect(wrapper.emitted()).toHaveProperty('greet');
expect(wrapper.emitted().greet).toHaveLength(2);
expect(wrapper.emitted().greet[0]).toEqual(['hello']);
expect(wrapper.emitted().greet[1]).toEqual(['goodbye']);
});
  • exists 返回布尔值 验证元素是否存在 一般用于DOM元素是否存在
1
2
3
4
5
6
test('exists', () => {
const wrapper = mount(Component);

expect(wrapper.find('span').exists()).toBe(true);
expect(wrapper.find('p').exists()).toBe(false);
});
  • find 选择器查找DOM元素,返回DOMWrapper 如果未查到不报错
1
2
3
4
5
6
7
test('find', () => {
const wrapper = mount(Component);

wrapper.find('span'); //=> found; returns DOMWrapper
wrapper.find('[data-test="span"]'); //=> found; returns DOMWrapper
wrapper.find('p'); //=> nothing found; returns ErrorWrapper
});
  • getComponent 查找Vue Component实例,返回VueWrapper 或抛出异常
1
2
3
4
5
6
7
8
test('getComponent', () => {
const wrapper = mount(Component);

wrapper.getComponent({ name: 'foo' }); // returns a VueWrapper
wrapper.getComponent(Foo); // returns a VueWrapper

expect(() => wrapper.getComponent('.not-there')).toThrowError();
});
  • get 选择器查找DOM元素,返回DOMWrapper 或抛出异常
1
2
3
4
5
6
7
test('get', () => {
const wrapper = mount(Component);

wrapper.get('span'); //=> found; returns DOMWrapper

expect(() => wrapper.get('.not-there')).toThrowError();
});
  • html 返回元素的HTML 字符串 快照时会使用到
1
2
3
4
5
test('html', () => {
const wrapper = mount(Component);

expect(wrapper.html()).toBe('<div><p>Hello world</p></div>');
});
  • props 返回传递给Vue组件的参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
test('props', () => {
const wrapper = mount(Component, {
global: { stubs: ['Foo'] },
});

const foo = wrapper.getComponent({ name: 'Foo' });

expect(foo.props('truthy')).toBe(true);
expect(foo.props('object')).toEqual({});
expect(foo.props('notExisting')).toEqual(undefined);
expect(foo.props()).toEqual({
truthy: true,
object: {},
string: 'string',
});
});
  • setProps 更新props传参 用于修改参数测试组件是否有变化
1
2
3
4
5
6
7
8
9
10
11
12
13
test('updates prop', async () => {
const wrapper = mount(Component, {
props: {
message: 'hello',
},
});

expect(wrapper.html()).toContain('hello');

await wrapper.setProps({ message: 'goodbye' });

expect(wrapper.html()).toContain('goodbye');
});
  • trigger 触发DOM事件,click submit keyup
1
2
3
4
5
6
7
test('trigger', async () => {
const wrapper = mount(Component);

await wrapper.find('button').trigger('click');

expect(wrapper.find('span').text()).toBe('Count: 1');
});
  • unmount 卸载组件
1
2
3
4
5
6
7
test('unmount', () => {
const wrapper = mount(Component);

wrapper.unmount();
// Component is removed from DOM.
// console.log has been called with 'unmounted!'
});

属性

vm Vue 实例,可以获取所有实例的方法和属性。

1
2
3
4
5
test('unmount', () => {
const wrapper = mount(Component);

expect(wrapper.vm.$el.style.backgroundColor).toBe('red');
});

注意:vm 仅在 VueWrapper 上可用, 并且只能获取到 DOM 元素的内联样式

快速开始

主要目录结构

1
2
3
4
5
6
7
├─ src
└─ __tests__
*.spec.js
├─ package.json
├─ babel.config.json
├─ .eslintrc
├─ jest.config.js

普通 Js 项目

1
npm i jest babel-jest @babel/core @babel/preset-env jest-html-reporter vue-template-compiler

package.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// package.json
{
"name": "test-js",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "jest",
"test:init": "jest --init",
"test:coverage": "jest --coverage"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@babel/core": "^7.13.14",
"@babel/preset-env": "^7.13.12",
"babel-jest": "^26.6.3",
"jest": "^26.6.3",
"jest-html-reporter": "^3.3.0",
"vue-template-compiler": "^2.6.12"
}
}

babel.config.js

1
2
3
4
5
6
7
8
9
10
11
12
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"node": "current"
}
}
]
]
}

jest.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
module.exports = {
// 匹配测试用例的文件
testMatch: [
"**/__tests__/**/*.[jt]s?(x)",
"**/?(*.)+(spec|test).[tj]s?(x)"
],
reporters: [
"default",
// 需要安装依赖:jest-html-reporter
["./node_modules/jest-html-reporter", {
"pageTitle": "Test Report"
}]
],
// 激活测试结果通知
notify: true,
// 每次测试前清除模拟调用和实例
clearMocks: true,
// 生成测试报告文件类型
coverageReporters: ['json', 'html'],
// 覆盖率报告的目录,测试报告所存放的位置
coverageDirectory: './coverage-reports',
snapshotSerializers: ['./node_modules/jest-serializer-vue'],
};

Ts 项目

1
npm i jest babel-jest @babel/core @babel/preset-env ts-jest @types/jest jest-html-reporter vue-template-compiler

package.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
{
"name": "test-ts",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "jest",
"test:init": "jest --init",
"test:coverage": "jest --coverage"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@babel/core": "^7.13.14",
"@babel/preset-env": "^7.13.12",
"@types/jest": "^26.0.22",
"babel-jest": "^26.6.3",
"jest": "^26.6.3",
"jest-html-reporter": "^3.3.0",
"ts-jest": "^26.5.4",
"vue-template-compiler": "^2.6.12"
}
}

babel.config.js

1
2
3
4
5
6
7
8
9
10
11
12
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"node": "current"
}
}
]
]
}

jest.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
module.exports = {
// 匹配测试用例的文件
testMatch: [
"**/__tests__/**/*.[jt]s?(x)",
"**/?(*.)+(spec|test).[tj]s?(x)"
],
reporters: [
"default",
// 需要安装依赖:jest-html-reporter
["./node_modules/jest-html-reporter", {
"pageTitle": "Test Report"
}]
],
// 激活测试结果通知
notify: true,
// 每次测试前清除模拟调用和实例
clearMocks: true,
// 生成测试报告文件类型
coverageReporters: ['json', 'html'],
// 覆盖率报告的目录,测试报告所存放的位置
coverageDirectory: './coverage-reports',
// 转换器 -- ts需要配置该选项
transform: {
'^.+\\.(t|j)sx?$': [
'babel-jest', {
presets: [
'@babel/preset-typescript',
],
},
],
},
snapshotSerializers: ['./node_modules/jest-serializer-vue'],
};

vue-test-utils + ts + jest 项目

1
npm i jest babel-jest @babel/core @babel/preset-env ts-jest @types/jest vue-jest @vue/test-utils slint-plugin-jest jest-html-reporter vue-template-compiler

package.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
{
"name": "test-ts",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "jest",
"test:init": "jest --init",
"test:coverage": "jest --coverage"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@babel/core": "^7.13.14",
"@babel/preset-env": "^7.13.12",
"@types/jest": "^26.0.22",
"@vue/test-utils": "^2.0.0-rc.4",
"babel-jest": "^26.6.3",
"eslint-plugin-jest": "^24.3.2",
"jest": "^26.6.3",
"jest-html-reporter": "^3.3.0",
"ts-jest": "^26.5.4",
"vite": "^2.0.1",
"vue-jest": "^5.0.0-alpha.8",
"vue-template-compiler": "^2.6.12"
}
}

jest.config.js 组件库的配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
const path = require('path');

module.exports = {
// 运行时是否输出测试信息
verbose: true,
// 根目录
rootDir: path.resolve(__dirname),
preset: 'ts-jest',
// 测试环境
testEnvironment: 'jsdom',
// 转换器
transform: {
'^.+\\.vue$': 'vue-jest',
'^.+\\.(t|j)sx?$': [
'babel-jest', {
presets: [
[
'@babel/preset-env',
{
targets: {
node: true,
},
},
],
'@babel/preset-typescript',
],
},
],
},
// 指定文件扩展名
moduleFileExtensions: ['vue', 'js', 'json', 'jsx', 'ts', 'tsx', 'node'],
// 匹配测试用例的文件
testMatch: [
'<rootDir>/packages/**/__tests__/*.spec.js',
],
// 是否收集测试时的覆盖率信息
collectCoverage: true,
// 生成测试报告文件类型
coverageReporters: ['json', 'html'],
// 覆盖率报告的目录,测试报告所存放的位置
coverageDirectory: '<rootDir>/testReports/',
// 测试报告想要覆盖那些文件,目录,前面加!是避开这些文件
collectCoverageFrom: [
'!site/components/**/src/*.(js|vue|ts)',
'packages/**/*.(js|vue)',
'packages/testReports/*.(js|vue)',
'!site/main.ts',
'!site/router/index.ts',
'!**/node_modules/**',
],
// 测试覆盖效果阈值 当覆盖率达到预设值返回成功 小于预设值则返回失败 但不影响报告输出
coverageThreshold: {
// 全部文件
// global: {
// branches: 60,
// functions: 60,
// lines: 60,
// statements: 60,
// },
// 单独文件
// 'packages/header-base/src/index.vue': {
// branches: 60,
// statements: 60,
// },
},
// 激活测试结果通知
notify: true,
// 每次测试前清除模拟调用和实例
clearMocks: true,
snapshotSerializers: ['<rootDir>/node_modules/jest-serializer-vue'],
};

参考文档

Jest 中文文档

Vue Test Utils for Vue3 文档

Vue Test Utils for Vue3API

Jest 配置

作者

张金秀

发布于

2021-04-06

更新于

2023-08-05

许可协议

CC BY-NC-SA 4.0

评论