依赖说明
下面列出了一些使用 Jest 测试相关的依赖说明及配置,可以根据不同的需求进行安装。如果你想快速安装运行,也可以直接跳到 快速开始。
测试运行器—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 Utils
是 Vue.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", ["./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"], transform: { '^.+\\.(t|j)sx?$': [ 'babel-jest', { presets: [ '@babel/preset-typescript', ], }, ], }, };
|
示例说明
编写测试用例
要测试的代码
1 2 3 4 5
| function foo(a, b) { return a + b; } export default foo;
|
测试用例
这里只是一个简单的测试用例,更多常用方法、匹配器等跳转 Vue Test Utils 常用的 API 、 Jest 常用 API 。
1 2 3 4 5
| import foo from './fun.js'; test('add', () => { expect(foo(1, 2)).toBe(3); });
|
运行测试
默认测试命令
测试结果![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)
测试并统计覆盖率命令
测试结果![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) 是否每一行都执行了
"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"
:测试覆盖率
全局函数
describe(name, fn)
把相关测试组合在一起
test(name, fn)
测试方法
expect(value)
断言
beforeEach(fn)
在每一个测试之前需要做的事情,比如测试之前将某个数据恢复到初始状态
afterEach(fn)
在每一个测试用例执行结束之后运行
beforeAll(fn)
在所有的测试之前需要做什么
afterAll(fn)
在测试用例执行结束之后运行
匹配器
toBe
使用 ===
来测试完全相等
toEqual
递归检查对象或数组的每个字段
toContain
判断数组或字符串中是否包含指定值
toContainEqual
判断数组中是否包含一个特定对象
toBeNull
只匹配 null
toBeUndefined
只匹配 undefined
toBeDefined
与 toBeUndefined
相反
toBeTruthy
匹配任何 if
语句为真
toBeFalsy
匹配任何 if
语句为假
toThrow
特定函数抛出一个错误
toMatch
正则匹配
toThrow
要测试的特定函数会在调用时抛出一个错误
resolves
和 rejects 用来测试 promise
toHaveBeenCalled
判断一个函数是否被调用过
toHaveBeenCalledTimes
判断函数被调用过几次
toBeGreaterThan
大于
toBeGreaterThanOrEqual
大于等于
toBeLessThan
小于
toBeLessThanOrEqual
小于等于
toBeCloseTo
浮点数比较
toMatchSnapshot()
记录快照
- 第一次调用的时候,会把
expect
中的值以字符串的形式存储到一个.snap
文件中
- 多次运行快照时,每次都会与上一次的快照文件内容对比,相同则测试通过,不同则测试失败
- 重新生成快照文件
npm run test -- -u
挂载组件
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);
|
参数
1 2 3 4 5 6 7
| const mountCom = (template, options) => mount(MyComponent, { ... props: { active: 'default', }, ... };
|
1 2 3 4 5 6 7 8
| const mountCom = (template, options) => mount(MyComponent, { ... slots: { default: 'Default', customerName: OtherComponent, }, ... });
|
1 2 3 4 5 6 7 8
| cconst mountCom = (template, options) => mount(MyComponent, { ... data() { return { message: 'world', }; ... });
|
1 2 3 4 5 6 7 8
| ccconst mountCom = (template, options) => mount(MyComponent, { ... attrs: { id: 'hello', disabled: true, }, ... });
|
wrapper 的方法
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'); wrapper.find('[data-test="span"]'); wrapper.find('p'); });
|
getComponent
查找Vue Component
实例,返回VueWrapper
或抛出异常
1 2 3 4 5 6 7 8
| test('getComponent', () => { const wrapper = mount(Component);
wrapper.getComponent({ name: 'foo' }); wrapper.getComponent(Foo);
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');
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>'); });
|
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'); });
|
1 2 3 4 5 6 7
| test('unmount', () => { const wrapper = mount(Component);
wrapper.unmount(); });
|
属性
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
| { "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", ["./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", ["./node_modules/jest-html-reporter", { "pageTitle": "Test Report" }] ], notify: true, clearMocks: true, coverageReporters: ['json', 'html'], coverageDirectory: './coverage-reports', 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: { }, notify: true, clearMocks: true, snapshotSerializers: ['<rootDir>/node_modules/jest-serializer-vue'], };
|
参考文档
Jest 中文文档
Vue Test Utils for Vue3 文档
Vue Test Utils for Vue3API
Jest 配置