一、JEST简介
Jest是Facebook开源的一套JavaScript测试框架, 它集成了断言、JSDom、覆盖率报告等开发者所需要的所有测试工具。
二、搭建基于 jest 的 vue 单元测试环境
1、安装依赖
npm install babel-jest @vue/test-utils @vue/cli-plugin-unit-jest -D
复制代码
"devDependencies": {
"@vue/cli-plugin-babel": "^3.12.0",
"@vue/cli-plugin-unit-jest": "^3.12.0",
"@vue/cli-service": "^3.12.0",
"@vue/test-utils": "1.0.0-beta.29",
"babel-jest": "^23.6.0",
"babel-preset-stage-2": "^6.24.1",
"sinon": "^11.1.1",
},
复制代码
2、编写 jest 配置文件
//jest.conf.js 与vue.config.js同级
module.exports = {
verbose: true,
moduleFileExtensions: ['js', 'jsx', 'json', 'vue'],
transform: {
'^.+\.vue$': 'vue-jest',
'.+\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$':
'jest-transform-stub',
// 为 Jest 配置 Babel
"^.+\.js$": "<rootDir>/node_modules/babel-jest"
},
transformIgnorePatterns: ['/node_modules/'],
// 别名
moduleNameMapper: {
"^@/(.*)$": "<rootDir>/src/$1"
},
snapshotSerializers: ['jest-serializer-vue'],
testMatch: [
'**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)'
],
testURL: 'http://localhost/',
watchPlugins: [
'jest-watch-typeahead/filename',
'jest-watch-typeahead/testname'
],
coverageDirectory: '<rootDir>/tests/unit/coverage', // 覆盖率报告的目录
// collectCoverage: true, // 代码覆盖率的指标
collectCoverageFrom: [ // 测试报告想要覆盖那些文件,目录,前面加!是避开这些文件
// 'src/components/**/*.(js|vue)',
'src/components/*.(vue)',
'!src/main.js',
'!src/router/index.js',
'!**/node_modules/**'
]
};
复制代码
3、编写启动文件 setup.js
放置一些全局定义和引入
// ./test/setup.js
import Vue from 'vue';
// 将Vue暴露到全局里面
// global.Vue = Vue;
// console.log('--global:',global.hasOwnProperty('Vue'))
Vue.config.productionTip = false;
// 使用elementUi组件
import ElementUI from 'element-ui';
// npm run unit 时要下面引入样式那句注释掉-不知为什么导入会报错。可能因为测试时,不需要css样式
// import 'element-ui/lib/theme-chalk/index.css';
Vue.use(ElementUI);
// 注册全局组件
import localModules from '@/modules'
Object.keys(localModules).map(key => {
Vue.component(key, localModules[key])
});
复制代码
在jest.conf.js文件中加入下一行
module.exports = {
...,
setupFiles: ['<rootDir>/tests/setup']
}
复制代码
4、加入启动 jest 的 npm script
"scripts": {
"test:unit": "vue-cli-service test:unit",
"unitc": "vue-cli-service test:unit --coverage"
},
复制代码
--coverage 生成测试覆盖率
--watch 单文件监视测试
--watchAll 监视所有文件改动,测试相应的测试。
复制代码
三、踩坑集
使用elementui模块, 报错如下:
解决方案:
第一步:新建setup.js文件
//test/unit/setup.js
import Vue from 'vue';
// 将Vue暴露到全局里面
// global.Vue = Vue;
// console.log('--global:',global.hasOwnProperty('Vue'))
Vue.config.productionTip = false;
// 使用elementUi组件
import ElementUI from 'element-ui';
// npm run unit 时要下面引入样式那句注释掉-不知为什么导入会报错。可能因为测试时,不需要css样式
// import 'element-ui/lib/theme-chalk/index.css';
Vue.use(ElementUI);
复制代码
第二步:在 jest.conf.js或jest 配置下新增setupFiles
// jest.conf.js
'setupFiles': ['<rootDir>/tests/setup']
复制代码
无法识别es6语法
// .babelrc配置
{
"plugins": [
"transform-es2015-modules-commonjs"
]
}
复制代码
引用Vuex报错
...mapState解析不出
// 安装 npm install babel-preset-stage-2
// 修改 .babelrc配置文件
{
"plugins": [
"transform-es2015-modules-commonjs","transform-object-rest-spread"
],
}
复制代码
四、使用
1、编写第一个测试文件
// ./src/components/hello-world.vue
<template>
<div>
<h1>{{ msg }}</h1>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
data() {
return {
msg: 'Hello Jest',
};
},
};
</script>
复制代码
对该组件进行测试
// ./test/unit/hello-world.spec.js
import { shallowMount } from '@vue/test-utils';
import HelloWorld from '@/components/hello-world';
describe('<hello-world/>', () => {
it('should render correct contents', () => {
const wrapper = shallowMount(HelloWorld);
expect(wrapper.find('.hello h1').text())
.toEqual('Welcome to Your Vue.js App');
});
});
复制代码
启动测试
npm run test:unit
复制代码
2、测试Dom事件
import {mount, shallowMount, createLocalVue} from "@vue/test-utils";
import networkDiagnosis from "@/views/Manage/device/networkDiagnosis";
import sinon from "sinon";
import Vuex from 'vuex';
const localVue = createLocalVue();
localVue.use(Vuex);
describe("networkDiagnosis网络诊断管理 —— 表单参数", () => { // describe 代表一个作用域
const clickHandler = sinon.stub();
it('测试网络诊断Ping类型下没有ip', async () => {
// 通过 mount 生成了一个包裹器,包括了一个挂载组件或 vnode,以及测试该组件或 vnode 的方法
let wrapper = mount(networkDiagnosis, {
listeners: {clickHandler},
// 加stubs 参数是为了避免这个问题 https://github.com/vuejs/vue-test-utils/issues/958 Cannot read property '$el' of undefined
stubs: {transition: false},
localVue
});
wrapper.setData({
form: {
diagnosisType: 'Ping',
ip: '' // 172.16.16.30
},
});
// .vm 可以获取当前实例对象,相当于拿到了 vue组件里的 this 对象
// const vm = wrapper.vm;
const button = wrapper.find('.blueLongBtn');
await button.trigger('click');
expect(wrapper.findAll('.el-form-item__error').length).toBe(1)
});
})
复制代码
3、使用axios模拟接口
现在按照下列3个步骤:
- 在测试文件中导入需要模拟的模块
- 使用
jest.mock()
方法mock一下模块 - 使用.mockResolvedValue()模拟数据返回
// index.test.js
const getFirstAlbumTitle = require('./index');
const axios = require('axios');
jest.mock('axios');
it('returns the title of the first album', async () => {
axios.get.mockResolvedValue({
data: [
{
userId: 1,
id: 1,
title: 'My First Album'
},
{
userId: 1,
id: 2,
title: 'Album: The Sequel'
}
]
});
const title = await getFirstAlbumTitle();
expect(title).toEqual('My First Album');
});
复制代码
这里面最核心的一句是jest.mock('axios')
,这让jest
用一个空的函数,接管了axios
的所有行为,在没有使用mockResolvedValue
方法前,本文件中的axios
的所有方法都将返回undefined
:
4、mock接口
在接口文件同一目录下,创建__mock__, 然后在__mock__文件夹下mock接口文件
具体如下:
__mocks __/device.js (在这里统一写mock函数)
export const netDiagnosisPing = jest.fn(
(ip) =>
new Promise((resolve, reject) => {
resolve({
code: 0,
message: "ping success",
data: [
"",
"正在 Ping 172.16.16.30 具有 32 字节的数据:",
"来自 172.16.16.30 的回复: 字节=32 时间<1ms TTL=63",
"......"
]
});
}),
);
export const netDiagnosisTelnet = jest.fn(
(ip, port) =>
new Promise((resolve, reject) => {
if (ip === '172.16.16.30' && port === '8080') {
resolve({
code: 0,
message: "telnet success",
data: null
});
} else {
reject({
code: 1,
message: "网络诊断失败!",
data: null
});
}
}),
);
复制代码
diagnosisRequest.spec.js (在spec文件中mock对应路径的模块即可)
import {createLocalVue, mount} from "@vue/test-utils";
import networkDiagnosis from "@/views/Manage/device/networkDiagnosis";
import {netDiagnosisPing, netDiagnosisTelnet} from "@/api/device";
import sinon from "sinon";
const localVue = createLocalVue();
jest.mock('../../../src/api/device.js');
describe("networkDiagnosis网络诊断管理 —— 表单请求", () => {
const clickHandler = sinon.stub();
test('Telnet类型网络诊断', async (done) => {
// let wrapper = mount(networkDiagnosis,{localVue});
// wrapper.setData({
// form: {
// diagnosisType: 'Telnet',
// ip: '172.16.16.30',
// port: '8080'
// },
// });
// const button = wrapper.find('.blueLongBtn');
// await button.trigger('click');
//
const res = await netDiagnosisTelnet('172.16.16.30','8080');
expect(res.code).toEqual(0);
done();
});
复制代码
重点:
五、扩展
mount和shallowMount
区别
- mount会渲染整个组件树而shallowMount在挂载组件之前对所有子组件进行存根。
- shallowMount可以确保你对一个组件进行独立测试,有助于避免测试中因子组件的渲染输出而混乱结果。
使用建议
- mount适合小组件测试
- shallowMount适合多场景测试(能用shallowMount就不用mount)
Sinon.js
Sinon.js是一个用来做独立测试和模拟的JavaScript库。它在单元测试的编写中通常用来模拟HTTP等相关请求。
expect.assertions
expect.assertions(number)验证在测试期间是否调用了一定数量的断言。 这在测试异步代码时通常很有用,以确保实际调用了回调中的断言。
在jest单元测试中模拟接口请求
如果不模拟接口请求,每次跑测试用例,都会真实地请求albums
接口,严重拖慢测试的速度