做者:江敏熙 贝聊前端开发工程师
本文同时发布于我的博客html
当前我在公司里负责的项目,能够分为两类:前端
一类是类似度很高的项目,好比管理后台,这类项目的页面经过各类公共组件搭建而成。公共组件的复用性很高,因此质量尤其重要。若是开发人员在修改了公共组件以后留下了bug,那么将会直接下降了整个项目的质量。我但愿让程序去测试这些公共组件,保证每个公共组件是可用的。vue
另外一类是公司的核心项目,这些项目特色是维护周期长,而且会不断加入新的功能。在项目版本迭代的过程当中,当一些原来经过了测试的旧功能发生了bug,通常只能到了测试阶段才能被测试人员发现。我但愿由程序去保证部分核心功能的正常运做,当核心功能发生了bug能快速的察觉到,而不是到了测试阶段才发现。webpack
为了解决上面的问题,我尝试引入单元测试。ios
下降bug发生概率,快速定位bug,减小重复的手工测试。git
提升代码质量,为项目带来更高的代码可维护性。github
方便项目的交接工做,测试脚本就是最好的需求描述。web
接下来谈谈如何进行单元测试。vue-router
Mocha(发音"摩卡")诞生于2011年,是如今最流行的JavaScript测试框架之一,在浏览器和Node环境均可以使用。chrome
Karma是由Google团队开发的一个测试工具, 它不是一个测试框架, 只是一个跑测试的驱动. 你能够经过karma的配置文件集成你喜欢的框架, 断言库和浏览器.
Vue的官方的单元测试框架,它提供了一系列很是方便的工具,使咱们能够更轻松地为Vue应用编写单元测试。主流的 JavaScript 测试运行器有不少,但 Vue Test Utils 都可以支持。它是测试运行器无关的。
本文选择的测试框架由Karma + Mocha + Chai + Vue Test Utils搭配,本身手动配置过程比较繁琐,在这里强烈推荐你们使用vue-cli,vue-cli有现成的模板能够生成项目,执行vue init webpack [项目名],'Pick a test runner'时选择'Karma + Mocha' 。vue-cli会自动生成Karma + Mocha + Chai的配置,咱们只须要额外安装Vue Test Utils,执行npm install @vue/test-utils。
若是想本身动手配置的同窗,能够参考这篇文章。
配置完成之后,下图是项目目录结构:
test文件夹下是unit文件夹,里面放的是单元测试相关的文件。
specs里存放的是测试脚本,这部分是由开发人员编写的。
coverage文件夹里存放的是测试报告,打开里面的index.html能够直观地看到测试的代码覆盖率。
Karma.conf.js是karma的配置文件。
被测试的组件HelloWorld.vue(path:E:\study\demo\src\components)
代码以下:
<template>
<div class="hello">
<h1>Welcome to Your Vue.js App</h1>
</div>
</template>
复制代码
测试脚本HelloWorld.spec.js(path:E:\study\demo\test\unit\specs)
代码以下:
import HelloWorld from '@/components/HelloWorld';
import { mount, createLocalVue, shallowMount } from '@vue/test-utils'
describe('HelloWorld.vue', () => {
it('should render correct contents', () => {
const wrapper = shallowMount(HelloWorld);
let content = wrapper.vm.$el.querySelector('.hello h1').textContent;
expect(content).to.equal('Welcome to Your Vue.js App');
});
});
复制代码
describe是"测试套件"(test suite),表示一组相关的测试。它是一个函数,第一个参数是测试套件的名称("加法函数的测试"),第二个参数是一个实际执行的函数。
it是"测试用例"(test case),表示一个单独的测试,是测试的最小单位。它也是一个函数,第一个参数是测试用例的名称,第二个参数是一个实际执行的函数。
上面的测试脚本里面,有一句断言:
expect(content).to.equal('Welcome to Your Vue.js App');
复制代码
所谓"断言",就是判断源码的实际执行结果与预期结果是否一致,若是不一致就抛出一个错误。上面这句断言的意思是,变量content应等于'Welcome to Your Vue.js App'。
全部的测试用例(it块)都应该含有一句或多句的断言。它是编写测试用例的关键。
最后运行一下npm run unit,来看结果:
打开coverage下的index.vue查看代码覆盖率:
这就是一个简单的单元测试编写过程,是否是很简单呢?你们都动手本身试试吧。
咱们在给实际项目写单元测试的时候,项目代码会比上面的demo组件复杂不少。若是你要测试的单个组件里使用了vue-router或者Vuex的话,就要使用createLocalVue。 好比,有这样一段代码:
data() {
return {
brandId: this.$route.query.id,
}
}
复制代码
$route对象须要用createLocalVue注入router才能使用,不然执行测试脚本会出错。使用createLocalVue解决这个问题,具体代码:
import { shallowMount, createLocalVue } from '@vue/test-utils'
import VueRouter from 'vue-router'
const localVue = createLocalVue()
localVue.use(VueRouter)
const router = new VueRouter()
shallowMount(Component, {
localVue,
router
})
复制代码
Vuex也是同理,关于createLocalVue详细用法就不作赘述了,你们能够去翻阅官方文档。
若是你须要在本身的测试文件中使用 nextTick,注意任何在其内部被抛出的错误可能都不会被测试运行器捕获,由于其内部使用了 Promise。关于这个问题有两个建议:要么你能够在测试的一开始将 Vue 的全局错误处理器设置为 done 回调,要么你能够在调用 nextTick 时不带参数让其做为一个 Promise 返回:
// 这不会被捕获
it('will time out', (done) => {
Vue.nextTick(() => {
expect(true).toBe(false)
done()
})
})
// 接下来的两项测试都会如预期工做
it('will catch the error using done', (done) => {
Vue.config.errorHandler = done
Vue.nextTick(() => {
expect(true).toBe(false)
done()
})
})
it('will catch the error using a promise', () => {
return Vue.nextTick()
.then(function () {
expect(true).toBe(false)
})
})
复制代码
在下面的项目实战中,有使用到nextTick的例子,你们能够当作参考。
测试在配置文件karma.conf里,browsers默认是'PhantomJS'.
module.exports = function karmaConfig (config) {
config.set({
// browsers: ['PhantomJS'],
browsers: ['Chrome'],
复制代码
但我在使用过程当中发现PhantomJS环境的warning和error提示和平时在浏览器chrome看到的提示不太同样,有点难懂,如图:
Chrome:
browsers设置为'Chrome',获得的报错提示和真实Chrome浏览器上一致,而且可使用console.log(),调试起来和真实开发的体验同样。惟一缺点是每次执行npm run unit都会弹出一个Chrome浏览器,PhantomJS则不会,推荐你们调试测试脚本时候使用Chrome,等脚本都跑通了不须要调试的时候能够换回PhantomJS。
默认下auto-watch是关闭的,每次修改了测试脚本,或者修改了项目代码以后都须要手动执行一次命令才能启动测试,很是麻烦。咱们能够加上--auto-watch,这样在开发的过程当中,若是某个功能没有经过测试用例,开发人员能够马上发现并修复。
"unit": "cross-env BABEL_ENV=test karma start test/unit/karma.conf.js --auto-watch",
复制代码
场景:页面上有一个textarea输入框和提交按钮,点击按钮发送请求。要求点击提交后前端先校验一下内容是否符合json格式,若是不符合则提示不能提交。
测试的目标:校验程序
测试用例:经过条件覆盖,输入数字,字符串,错误的json字符串,'null',正确的json字符串去验证全部的状况是否正常执行,指望只有最后一种状况才是返回结果才是经过的,其余都是不经过。
// form-setting.vue测试校验功能
describe('form-setting.vue测试校验功能', () => {
const wrapper = shallowMount(formSetting, {
localVue
});
let vm = wrapper.vm;
it('test form填入数字是否会不经过', () => {
vm.appType = 'ios'; // 选择系统ios
vm.ios.schemeInfo = 1; // 输入数字
expect(vm.isValid()).to.equal(false);
});
it('test form填入字符串格式是否会不经过', () => {
vm.appType = 'ios';
vm.ios.schemeInfo = '1'; // 输入字符串
expect(vm.isValid()).to.equal(false);
});
it('test form填入错误json格式是否会不经过', () => {
vm.appType = 'ios';
vm.ios.schemeInfo = '{a:{a:}}'; // 输入非法的相似json格式的字符串
expect(vm.isValid()).to.equal(false);
});
it('test form填入空对象是否会不经过', () => {
vm.appType = 'ios';
vm.ios.schemeInfo = 'null'; // 输入null对象字符串
expect(vm.isValid()).to.equal(false);
});
it('test form填入正确JSON格式是否会经过', () => {
vm.appType = 'ios';
vm.ios.schemeInfo = '{"a": 111}'; // 输入正确的json字符串
expect(vm.isValid()).to.equal(true);
});
});
复制代码
场景:团队开发了一个校验插件,其做用是校验输入框是否知足相应规则,若不知足在输入框下会出现一个提示错误的dom节点。
测试用例:经过列举全部的输入操做,而后判断是否存在类名为.error的错误提示节点。
在完成输入操做后,若是内容不经过校验,页面会生成错误提示的dom节点。这个过程是异步的,因此用到了nextTick。具体的用法是
return Vue.nextTick().then(() => {
...断言
}
复制代码
关于这块详细的解释,Vue Test Utils有相关篇幅
import { mount, createLocalVue } from '@vue/test-utils'
import ValidateDemo from '@/components/validate-demo'
import validate from '@/directive/validate/1.0/validate'
import Vue from 'Vue'
const localVue = createLocalVue() // 建立一个Vue实例
localVue.use(validate) // 挂载校验插件
describe('测试validate-demo.vue', () => {
it('没发生输入操做,[不显示error]', () => {
const wrapper = mount(ValidateDemo, {
localVue
})
return Vue.nextTick().then(() => {
expect(wrapper.find('.error').exists()).to.equal(false)
})
})
it('聚焦输入框而后失去焦点,[显示error]', () => {
const wrapper = mount(ValidateDemo, {
localVue
})
let input = wrapper.find('input')
input.trigger('focus') // 聚焦
input.trigger('blur') // 失去焦点
return Vue.nextTick().then(() => {
expect(wrapper.find('.error').exists()).to.equal(true)
})
})
it('发生输入操做,而后清空,[显示error]', () => {
const wrapper = mount(ValidateDemo, {
localVue
})
let vm = wrapper.vm
let input = wrapper.find('input')
input.trigger('focus')
vm.name = '不为空'
vm.name = '' // 清空
input.trigger('blur')
return Vue.nextTick().then(() => {
expect(wrapper.find('.error').exists()).to.equal(true)
})
})
it('输入内容后,[不显示error]', () => {
const wrapper = mount(ValidateDemo, {
localVue
})
let vm = wrapper.vm
vm.name = '不为空' // 输入内容
return Vue.nextTick().then(() => {
expect(wrapper.find('.error').exists()).to.equal(false)
})
})
})
复制代码
单元测试有许多优势,但不表明它就必定适合每一个项目,在我看来它会有如下局限性:
即便你愿意花费开发的几分之一的时间去写单元测试,可是一旦功能有变动,就意味着测试逻辑也须要调整。对于一些常常变动的功能来讲,这会致使很大的单元测试维护量。 因此咱们要权衡好当中的利弊,能够考虑只针对稳定的功能(好比一些公用组件)和核心流程编写单元测试。
若是项目里充斥着颗粒度低,方法间互相耦合的代码,你会发现没法进行单元测试。由于单元测试旨在从代码粒度上实现对应用质量的把握。面对这样的状况,要么重构已有代码,要么放弃单元测试寻求其余测试方法,好比人工测试,e2e测试。
虽然这算是单元测试的一个缺点,但我认为同时也是优势,习惯编写单元测试能够促使工程师提升代码的颗粒度,思惟更加缜密。
前端是一个很是复杂的测试环境,由于每一个浏览器都有差别,须要的数据又依赖于后端。单元测试只能对功能每个单元进行测试,对于一些依赖api的数据通常只能mock,没法真正的模拟用户实际的使用场景。对于这种状况,建议采用其余测试方法,好比人工测试、e2e测试。
经过此次对单元测试的探索,我以为作单元测试最大的阻力是——时间。
手工测试最大的优点在于:当一个功能代码写好之后,只须要手动刷新浏览器去实际操做一下,便能判断程序是否正确。若是为此去编写单元测试则会花费额外的开发时间。
但人不是机器,不管多么简单的事都有可能出错。咱们为系统加入了新功能的以后,通常不会去手动测试之前的旧功能。由于这耗费时间而又无趣,而且咱们总会认为本身写的代码是不会影响旧功能的。
然而咱们能够换个角度去想,若是在开发旧功能的时候写好了相应的单元测试,那么每次进入测试阶段以前,就能够用测试脚本把旧功能都跑一遍。这样既节省了测试旧功能的时间,本身也能够问心无愧:不管怎么样,我都能确保我写的代码是经过测试的。
最后,感谢你们的阅读,本文是我关于对一个Vue项目作的比较浅显的单元测试的探索,属于抛砖引玉,若是有什么不合理的地方或建议,欢迎你们来指正!