Jest 单元测试环境搭建
# Jest 单元测试环境搭建
# 一. 依赖说明
下面列出了一些使用 Jest 测试相关的依赖说明及配置,可以根据不同的需求进行安装。如果你想快速安装运行,也可以直接跳到 快速开始。
# 1. 测试运行器 — Jest
官方推荐的其中一种配置简单、集成完善的测试机运行器。
npm i --save-dev jest
配置 package.json
设置 scripts
启动,更多常用配置请跳转到 Jest 的脚本
{
...
"scripts": {
"test": "jest",
"test:init": "jest --init",
"test:coverage": "jest --coverage"
},
...
}
2
3
4
5
6
7
8
9
# 2. 支持 TS
npm i ts-jest @types/jest
配置 jest.config.js
...
transform: {
transform: {
'^.+\\.(t|j)sx?$': [
'babel-jest', {
presets: [
'@babel/preset-typescript',
],
},
],
},
},
...
2
3
4
5
6
7
8
9
10
11
12
13
# 3. Babel 转译
虽然最新版本的 Node
已经支持绝大多数的 ES2015
特性,但是我们还想在测试中使用 ES modules
语法。所以需要需要安装 babel-jest
进行语法转义。
npm i --save-dev babel-jest @babel/core @babel/preset-env
配置 babel.config.json
{
"presets": [
[
"@babel/preset-env", {
"targets": {
"node": "current"
}
}
]
]
}
2
3
4
5
6
7
8
9
10
11
# 4. 单元测试实用工具库—vue-test-utils
Vue Test Utils
是 Vue.js
官方的单元测试实用工具库,通过两者结合来测试验证码组件,覆盖各功能测试
npm i --save-dev @vue/test-utils
# 5. 处理单文件组件
告诉 Jest
如何处理 *.vue
文件
npm i --save-dev vue-jest
配置 jest.config.js
文件
...
transform: {
'^.+\\.vue$': 'vue-jest',
},
...
2
3
4
5
# 6. 使用 Eslint 检测
npm i --save-dev eslint-plugin-jest
配置 .eslintrc
文件
...
"extends": [
"plugin:jest/recommended"
],
...
2
3
4
5
# 7. 快照格式化
默认快照测试,输出文件包含大量转义符号”/“,需要通过 jest-serializer-vue
插件处理,可以自定义快照输出目录位置
npm i --save-dev jest-serializer-vue
配置 jest.config.js
文件
...
// 处理快照文件转义符
snapshotSerializers: ["<rootDir>/node_modules/jest-serializer-vue"],
// 生成测试报告文件类型
coverageReporters: ['json', 'html'],
// 覆盖率报告的目录,测试报告所存放的位置
coverageDirectory: './coverage-reports',
...
2
3
4
5
6
7
8
# 8. 浏览器显示测试结果
执行测试后,会在项目最外层生成 test-report.html
,点开即可查看结果
npm i --save-dev jest-html-reporter
配置 jest.config.js
文件
...
reporters: [
// 可以自定义报告
["./node_modules/jest-html-reporter", {
"pageTitle": "Test Report"
}]
],
或者
"testResultsProcessor": "./node_modules/jest-html-reporter"
...
2
3
4
5
6
7
8
9
10
# 二. Jest 基本配置
以下只列出常用的配置,更多配置请参考 https://jestjs.io/zh-Hans/docs/configuration (opens new window)。
可以通过 npm run test:init
创建 jest.config.json
文件,里面包含全部配置的说明。也可以手动创建,并配置。
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',
],
},
],
},
};
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
# 三. 示例说明
# 1. 编写测试用例
# (1). 要测试的代码
// fun.js
function foo(a, b) {
return a + b;
}
export default foo;
2
3
4
5
# (2). 测试用例
这里只是一个简单的测试用例,更多常用方法、匹配器等跳转 Vue Test Utils 常用的 API 、 Jest 常用 API 。
// test.js
import foo from './fun.js';
test('add', () => {
expect(foo(1, 2)).toBe(3);
});
2
3
4
5
# 2. 运行测试
# (1). 默认测试命令
npm run test
执行测试后,根目录下会生成目录 coverage-reports/index.html
文件(文件目录可以自定义,参考 jest.config.js
中的配置)
- 语句覆盖率(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
# 1. 全局函数
describe(name, fn)
把相关测试组合在一起test(name, fn)
测试方法expect(value)
断言beforeEach(fn)
在每一个测试之前需要做的事情,比如测试之前将某个数据恢复到初始状态afterEach(fn)
在每一个测试用例执行结束之后运行beforeAll(fn)
在所有的测试之前需要做什么afterAll(fn)
在测试用例执行结束之后运行
# 2. 匹配器
toBe
使用===
来测试完全相等toEqual
递归检查对象或数组的每个字段toContain
判断数组或字符串中是否包含指定值toContainEqual
判断数组中是否包含一个特定对象toBeNull
只匹配null
toBeUndefined
只匹配undefined
toBeDefined
与toBeUndefined
相反toBeTruthy
匹配任何if
语句为真toBeFalsy
匹配任何if
语句为假toThrow
特定函数抛出一个错误toMatch
正则匹配toThrow
要测试的特定函数会在调用时抛出一个错误resolves
和 rejects 用来测试 promisetoHaveBeenCalled
判断一个函数是否被调用过toHaveBeenCalledTimes
判断函数被调用过几次toBeGreaterThan
大于toBeGreaterThanOrEqual
大于等于toBeLessThan
小于toBeLessThanOrEqual
小于等于toBeCloseTo
浮点数比较toMatchSnapshot()
记录快照
第一次调用的时候,会把
expect
中的值以字符串的形式存储到一个.snap
文件中 多次运行快照时,每次都会与上一次的快照文件内容对比,相同则测试通过,不同则测试失败 重新生成快照文件npm run test -- -u
# 六. Vue Test Utils 常用的 API
# 1. 挂载组件
mout(component)
创建一个包含被挂载和渲染的Vue
组件的Wrapper
const wrapper = mount({
template: `<div>hello word</div>`,
components: {
OtherComponent,
},
});
const wrapper2 = mount(MyComponent);
2
3
4
5
6
7
shallowMount
只挂载一个组件不渲染子组件
const wrapper = shallowMount({
template: `<div>hello word</div>`,
components: {
OtherComponent,
},
});
const wrapper2 = mount(MyComponent);
2
3
4
5
6
7
# 2. 参数
props
传入组件的参数
const mountCom = (template, options) => mount(MyComponent, {
...
props: {
active: 'default',
},
...
};
2
3
4
5
6
7
slots
设置组件的插槽
const mountCom = (template, options) => mount(MyComponent, {
...
slots: {
default: 'Default',
customerName: OtherComponent,
},
...
});
2
3
4
5
6
7
8
data
传入组件数据
cconst mountCom = (template, options) => mount(MyComponent, {
...
data() {
return {
message: 'world',
};
...
});
2
3
4
5
6
7
8
attrs
设置组件的属性
ccconst mountCom = (template, options) => mount(MyComponent, {
...
attrs: {
id: 'hello',
disabled: true,
},
...
});
2
3
4
5
6
7
8
# 3. wrapper 的方法
attributes
返回DOM
节点属性值
test('attributes', () => {
const wrapper = mount(Component);
expect(wrapper.attributes('id')).toBe('foo');
expect(wrapper.attributes('class')).toBe('bar');
});
2
3
4
5
6
classes
返回元素上class
的数组 一般用于测试样式
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);
});
2
3
4
5
6
7
emitted
返回组件发出的所有事件 一般用于测试派发事件及传输的参数
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']);
});
2
3
4
5
6
7
8
exists
返回布尔值 验证元素是否存在 一般用于DOM
元素是否存在
test('exists', () => {
const wrapper = mount(Component);
expect(wrapper.find('span').exists()).toBe(true);
expect(wrapper.find('p').exists()).toBe(false);
});
2
3
4
5
6
find
选择器查找DOM
元素,返回DOMWrapper
如果未查到不报错
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
});
2
3
4
5
6
7
getComponent
查找Vue Component
实例,返回VueWrapper
或抛出异常
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();
});
2
3
4
5
6
7
8
get
选择器查找DOM
元素,返回DOMWrapper
或抛出异常
test('get', () => {
const wrapper = mount(Component);
wrapper.get('span'); //=> found; returns DOMWrapper
expect(() => wrapper.get('.not-there')).toThrowError();
});
2
3
4
5
6
7
html
返回元素的HTML
字符串 快照时会使用到
test('html', () => {
const wrapper = mount(Component);
expect(wrapper.html()).toBe('<div><p>Hello world</p></div>');
});
2
3
4
5
props
返回传递给Vue
组件的参数
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',
});
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
setProps
更新props
传参 用于修改参数测试组件是否有变化
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');
});
2
3
4
5
6
7
8
9
10
11
12
13
trigger
触发DOM
事件,click submit keyup
test('trigger', async () => {
const wrapper = mount(Component);
await wrapper.find('button').trigger('click');
expect(wrapper.find('span').text()).toBe('Count: 1');
});
2
3
4
5
6
7
unmount
卸载组件
test('unmount', () => {
const wrapper = mount(Component);
wrapper.unmount();
// Component is removed from DOM.
// console.log has been called with 'unmounted!'
});
2
3
4
5
6
7
# 4. 属性
vm
Vue 实例,可以获取所有实例的方法和属性。
test('unmount', () => {
const wrapper = mount(Component);
expect(wrapper.vm.$el.style.backgroundColor).toBe('red');
});
2
3
4
5
注意:vm 仅在 VueWrapper 上可用, 并且只能获取到 DOM 元素的内联样式
# 七. 快速开始
# 1. 主要目录结构
├─ src
└─ __tests__
*.spec.js
├─ package.json
├─ babel.config.json
├─ .eslintrc
├─ jest.config.js
2
3
4
5
6
7
# 2. 普通 Js 项目
npm i jest babel-jest @babel/core @babel/preset-env jest-html-reporter vue-template-compiler
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"
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
babel.config.js
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"node": "current"
}
}
]
]
}
2
3
4
5
6
7
8
9
10
11
12
jest.config.js
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'],
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 3. Ts 项目
npm i jest babel-jest @babel/core @babel/preset-env ts-jest @types/jest jest-html-reporter vue-template-compiler
package.json
{
"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"
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
babel.config.js
{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"node": "current"
}
}
]
]
}
2
3
4
5
6
7
8
9
10
11
12
jest.config.js
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'],
};
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
# 4. vue-test-utils + ts + jest 项目
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
{
"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"
}
}
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
jest.config.js
组件库的配置
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'],
};
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