很长一段时间以来,单元测试并非前端工程师应具有的一项技能,但随着前端工程化的发展,项目日渐复杂化及代码追求高复用性等,促使单元测试愈发重要,决定整个项目质量的关键因素之一html
简单来讲就是TDD先写测试模块,再写主功能代码,而后能让测试模块经过测试,而BDD是先写主功能模块,再写测试模块前端
断言指的是一些布尔表达式,在程序中的某个特定点该表达式值为真,判断代码的实际执行结果与预期结果是否一致,而断言库则是讲经常使用的方法封装起来vue
主流的断言库有node
assert("mike" == user.name); 复制代码
expect(foo).to.be("aa"); 复制代码
foo.should.be("aa"); //should 复制代码
Jest 是 Facebook 开源的一款 JS 单元测试框架,它也是 React 目前使用的单元测试框架,目前vue官方也把它看成为单元测试框架官方推荐 。 目前除了 Facebook 外,Twitter、Airbnb 也在使用 Jest。Jest 除了基本的断言和 Mock 功能外,还有快照测试、实时监控模式、覆盖度报告等实用功能。 同时 Jest 几乎不须要作任何配置即可使用。ios
我在项目开发使用jest做为单元测试框架,结合vue官方的测试工具vue-util-testchrome
npm install --save-dev jest
npm install -g jest
复制代码
(1)添加方式vue-cli
npx jest --init
复制代码
而后会有一些选择,根据本身的实际状况选择npm
const path = require('path'); module.exports = { verbose: true, rootDir: path.resolve(__dirname, '../../../'), moduleFileExtensions: [ 'js', 'json', 'vue', ], testMatch: [ '<rootDir>/src/test/unit/specs/*.spec.js', ], transform: { '^.+\\.js$': 'babel-jest', '.*\\.(vue)$': 'vue-jest', }, moduleNameMapper: { '^@/(.*)$': '<rootDir>/src/$1', }, transformIgnorePatterns: ['/node_modules/'], collectCoverage: false, coverageReporters: ['json', 'html'], coverageDirectory: '<rootDir>/src/test/unit/coverage', collectCoverageFrom: [ 'src/components/**/*.(js|vue)', '!src/main.js', '!src/router/index.js', '!**/node_modules/**', ], }; 复制代码
配置解析:json
vue-jest
处理 *.vue
文件,用babel-jest
处理 *.js
文件@
-> src
别名(2)jest命令行工具axios
{ "name": "test", "version": "1.0.0", "scripts": { "unit": "jest --config src/test/unit/jest.conf.js --coverage", }, dependencies": { "vue-jest": "^3.0.5", }, "devDependencies":{ "@vue/test-utils": "^1.0.0-beta.13", "babel-core": "^7.0.0-bridge.0", "babel-jest": "^21.2.0", "jest": "^21.2.1", } } 复制代码
config - 配置jest配置文件路径
coverage - 生成测试覆盖率报告
coverage是jest提供的生成测试覆盖率报告的命令,须要生成覆盖率报告的在package.json添加--coverage参数
(3) 单元测试文件命名
以spec.js结尾命名,spec是sepcification的缩写。就测试而言,Specification指的是给定特性或者必须知足的应用的技术细节
(4)单元测试报告覆盖率指标
执行: npm run unit
配置后执行该命令会直接生成coverage文件并在终端显示各个指标的覆盖率概览
在网页中打开coverage目录下的index.html就能够看到具体每一个组件的测试报告
当咱们完成单元测试覆盖率达不到100%,不用慌,不用过分追求100%的覆盖率,把核心的功能模块测通便可,固然若是你要设置最低的覆盖率检测,能够在配置中加入以下,若是覆盖率低于你所设置的阈值(80%),则测试结果失败不经过
//jest.config.js coverageThreshold: { "global": { "branches": 80, "functions": 80, "lines": 80, "statements": 80 } }, 复制代码
🚀官方文档
expect(1+1).toBe(2)//判断两个值是否相等,toBe不能判断对象,须要判断对象要使用toEqual expect({a: 1}).toEqual({a: 1});//会递归检查对象的每一个字段 expect(1).not.toBe(2)//判断不等 expect(n).toBeNull(); //判断是否为null expect(n).toBeTruthy(); //判断结果为true expect(n).toBeFalsy(); //判断结果为false expect(value).toBeCloseTo(0.3); // 浮点数判断相等 expect(compileAndroidCode).toThrow(ConfigError); //判断抛出异常 复制代码
Vue Test Utils 是 Vue.js 官方的单元测试实用工具库,经过二者结合来测试验证码组件,覆盖各功能测试
//kAuthCode <template> <p class="kauthcode"> <span class="kauthcode_btn" v-if="vertification" @click="handleCode"> 获取验证码</span> <span v-else>{{timer}} 秒从新获取</span> </p> </template> <script> export default { name: 'KAuthCode', props: { phone: { type: String, require: true, }, type: { type: String, default: '1', require: true, validator(t) { return ['1', '2'].includes(t);// 1手机 2邮箱 }, }, validateType: { type: String, default: '1', validator(t) { return ['1', '2', '3'].includes(t);// 1 消息 2 表单 3自定义 }, }, }, data() { return { timer: 60, vertification: true, }; }, methods: { handleCode() { if (!this.phone) { switch (this.type) { case '1': this.$Message.warning('手机号码不能为空'); break; case '2': this.$refs.formRef.validateField('code'); break; default: break; } return; } this.getCode(); }, getCode() { let response; switch (this.type) { case '1': response = this.$api.login.getPhoneCode({ mobileNumber: this.phone }); break; case '2': response = this.$api.login.getEmailCode({ email: this.phone }); break; default: break; } response.then(() => { this.$Message.success('验证码发送成功'); this.vertification = false; const codeTimer = setInterval(() => { this.timer -= 1; if (this.timer <= 0) { this.vertification = true; this.timer = 60; clearInterval(codeTimer); } }, 1000); }); }, }, }; </script> <style lang="less" scoped> .kauthcode { span { display: inline-block; width: 100%; } } </style> 复制代码
测试文件
// kAuthCode.spec.js import {createLocalVue, mount, shallowMount} from '@vue/test-utils'; import KAuthCode from '@/components/common/KAuthCode.vue'; import login from '@/service/modules/login.js'; import iviewUI from 'view-design'; const localVue = createLocalVue(); localVue.use(iviewUI); const testPhone = '18898538706'; jest.mock('@/service/modules/login.js', () => ({ getPhoneCode: () => Promise.resolve({ data: { answer: 'mock_yes', image: 'mock.png', } }) })) describe('KAuthCode.vue', () => { const option = { propsData: { // phone: testPhone, type: '2' }, mocks: { $api: { login }, }, }; beforeEach(() => { jest.useFakeTimers(); }); afterEach(() => { jest.clearAllTimers(); }); const wrapper = mount(KAuthCode, option); it('设置手机号码', () => { const getCode = jest.fn(); option.methods = {getCode}; wrapper.find('.kauthcode_btn').trigger('click'); expect(wrapper.vm.phone).toBe(testPhone); }); it('没有设置手机号码应报错', () => { wrapper.setData({type:'2'}); const status = wrapper.find('.kauthcode_btn').trigger('click'); expect(status).toBeFalsy(); }); }); 复制代码
一个 Wrapper 是一个包括了一个挂载组件或 vnode,以及测试该组件或 vnode 的方法, 经过用mount(component,option)来挂载组件,获得wrapper包裹器,可经过
wrapper.vm
访问实际的 Vue 实例wrapper.setData
修改实例wrapper.find
找到相应的dom并触发事件`wrapper.find('.kauthcode_btn').trigger('click');propsData
- 组件被挂载时对props的设置import {createLocalVue, mount, shallowMount} from '@vue/test-utils'; import KAuthCode from '@/components/common/KAuthCode.vue'; const option = { propsData: { // phone: testPhone, type: '2' }, mocks: { $api: { login }, }, }; const wrapper = mount(KAuthCode, option); 复制代码
ps: 也能够经过shallowMount来挂载组件,区别在于shallowMount不会渲染子组件,详细区别,能够经过shallowMount和mount两个方法分别挂载同组件并进行快照测试后查看所生成文件内容
返回一个 Vue 的类供你添加组件、混入和安装插件而不会污染全局的 Vue 类
import {createLocalVue, mount} from '@vue/test-utils'; import iviewUI from 'view-design'; const localVue = createLocalVue(); localVue.use(iviewUI); 复制代码
beforeEach和afterEach - 在同一个describe描述中,beforeAll和afterAll会在多个it做用域内执行,适合作一次性设置
调用顺序: beforeAll => beforeEach => afterAll => afterEach
beforeEach(() => {
jest.useFakeTimers();
});
afterEach(() => {
jest.clearAllTimers();
});
复制代码
三个与 Mock 函数相关的API,分别是jest.fn()、jest.spyOn()、jest.mock()
// 断言mockFn执行后返回值为name it('jest.fn()返回值', () => { let mockFn = jest.fn().mockReturnValue('name'); expect(mockFn()).toBe('name'); }) //定义jest.fn()的内部实现并断言其结果 it('jest.fn()的内部实现', () => { let mockFn = jest.fn((a, b) => { return a + b; }) expect(mockFn(2, 2)).toBe(4); }) //jest.fn()返回Promise对象 it('jest.fn()返回Promise', async () => { let mockFn = jest.fn().mockResolvedValue('name'); let result = await mockFn(); // 断言mockFn经过await关键字执行后返回值为name expect(result).toBe('name'); // 断言mockFn调用后返回的是Promise对象 expect(Object.prototype.toString.call(mockFn())).toBe("[object Promise]"); }) 复制代码
// kAuthCode.spec.js jest.mock('@/service/modules/login.js', () => ({ getPhoneCode: () => Promise.resolve({ data: { answer: 'mock_yes', image: 'mock.png', } }) })) it('设置手机号码', () => { const getCode = jest.fn(); option.methods = {getCode}; wrapper.find('.kauthcode_btn').trigger('click'); expect(getCode).toHaveBeenCalled() expect(wrapper.vm.phone).toBe(testPhone); }); 复制代码
须要用mock掉整个axios的请求,使用toHaveBeenCalled判断这个方法是否被调用就能够了
这个例子里面,咱们只需关注getCode方法,其余能够忽略。为了测试这个方法,咱们应该作到:
注:有时候会存在一种状况,在同个组件中调用同个方法,只是返回值不一样,咱们可能要对它进行屡次不一样的mock,这时候须要在beforeEach使用restoreAllMocks方法重置状态
mock的目的:
1.触发事件 - 假设组件库使用的是iview中对<Checkbox>提供的@change事件,可是当咱们进行 wrapper.trigger('change')时,是触发不了的。<Button>的@click()和<button>的@click也是有区别的。 2。渲染问题 - 组件库提供的组件渲染后的html,须要经过wrapper.html()来看,可能会与你从控 制台看到的html有所区别,为避免测试结果出错,还应console.log一下wrapper.html()看一下实际的渲染结果 复制代码