浅析前端vue项目使用单元测试框架一
测试库:https://www.awesomes.cn/repos/Applications/Testings
/*************************************************************************************分割线************************************************************************************************************************************/
第一部分:配置操作流程
步骤一、通过 vue init webpack-simple test-xxxxx安装的简单工程项目,在webpack-template项目中,需要手动安装 "karma"、"karma-chrome-launcher"、"karma-coverage"、 "karma-mocha","karma-phantomjs-launcher"、"karma-phantomjs-shim","karma-sinon-chai"、"karma-sourcemap-loader"、"karma-spec-reporter"、"karma-webpack"、"mocha"、“chai”、"sinon"、"sinon-chai",可能需要安装“cross-env”
cnpm i xxx --save-dev 即可
步骤二、package.json中script脚本命令中添加"unit": "cross-env BABEL_ENV=test karma start test/unit/karma.conf.js",
步骤三、因为我们并不是用脚手架集成引入测试框架(chai/Mocha等),所以项目目录结构中不存在测试目录,需我们手动创建添加:
1)与src、node_modules等文件夹同级结构中创建test目录
2)test目录下创建unit文件夹
3)unit目录下存在3个文件(.eslintrc、index.js、karma.conf.js)&1个文件夹(specs)
.eslintrc中内容:
{
"env": {
"mocha": true
},
"globals": {
"expect": true,
"sinon": true
}
}
index.js中内容:
import Vue from 'vue'
Vue.config.productionTip = false
// require all test files (files that ends with .spec.js)
const testsContext = require.context('./specs', true, /\.spec$/)
testsContext.keys().forEach(testsContext)
// require all src files except main.js for coverage.
// you can also change this to match only the subset of files that
// you want coverage for.
const srcContext = require.context('../../src', true, /^\.\/(?!main(\.js)?$)/)
srcContext.keys().forEach(srcContext)
karma.conf.js中内容:
// This is a karma config file. For more details see
// http://karma-runner.github.io/0.13/config/configuration-file.html
// we are also using it with karma-webpack
// https://github.com/webpack/karma-webpack
var webpackConfig = require('../../webpack.test.conf')
module.exports = function (config) {
config.set({
// to run in additional browsers:
// 1. install corresponding karma launcher
// http://karma-runner.github.io/0.13/config/browsers.html
// 2. add it to the `browsers` array below.
//browsers: ['PhantomJS'],
browsers: ['Chrome'],
frameworks: ['mocha', 'sinon-chai', 'phantomjs-shim'],
reporters: ['spec', 'coverage'],
files: ['./index.js'],
preprocessors: {
'./index.js': ['webpack', 'sourcemap']
},
webpack: webpackConfig,
webpackMiddleware: {
noInfo: true
},
coverageReporter: {
dir: './coverage',
reporters: [
{ type: 'lcov', subdir: '.' },
{ type: 'text-summary' }
]
}
})
}
/***********end**************/
specs文件夹中存放测试脚本文件,
例如,在src中component中有个HelloWorld.vue/或者untils下HelloFarmFriend.js文件;注,此时在specs目录下对应存在测试脚本文件,如HelloWorld.spec.js 或者HelloFarmFriend.spec.js
正如下文举例所描述那样。
步骤四、需要添加webpack.test.conf.js文件(与src同级),内容:
此时,还需要cnpm i --save-dev webpack-merge,理由是webpack配置需要的
最后,添加config目录(与src同级)
1.dev.env.js中
2.prod.env.js
3.test.env.js
大功告成!试试
步骤五、完成上述步骤后,运行npm run unit命令即可。运行后会在test下unit目录中生成一个coverage目录,此文件为代码测试覆盖率结果,找到并打开index.html即可查看测试报告。
这里先以vue单文件组件为例子
src/components/HelloWorld.vue
<template>
<div class="hello">
<h1>{{ msg }}</h1>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
data () {
return {
msg: 'dachengz Vue.js App'
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h1, h2 {
font-weight: normal;
}
ul {
list-style-type: none;
padding: 0;
}
li {
display: inline-block;
margin: 0 10px;
}
a {
color: #42b983;
}
</style>
此时测试脚本文件为:HelloWorld.spec.js
import Vue from 'vue'
import HelloWorld from '../../../src/components/HelloWorld'
describe('Hello大橙子', () => {
it('should render correct contents', () => {
const Constructor = Vue.extend(HelloWorld)
const vm = new Constructor().$mount()
expect(vm.$el.querySelector('.hello h1').textContent)
.to.equal('dachengz Vue.js App')
})
})
上述自己创建对应文件,保存
/****占位utils.js测试***/
这里先以需要测试js文件为例子
1.创建一个utils目录,添加一个add.js(demo)
function add(x,y){
return x + y
}
module.exports = add
2.同样操作,在test/unit/specs目录中添加add.spec.js文件
// add.test.js
import add from '../../../utils/add'
describe('测试加法',() => {
it('1加1 应该等于2', () => {
expect(add(1,1)).to.be.equal(2);
})
})
上述就是针对纯待测js文件进行单元测试,保存代码,运行npm run unit即可,结果如下图:
/*********************************************************************************分割线*********************************************************************************************************************/
第二部分:针对测试框架说明介绍(偏概念)
应用组件功能测试
组件测试vue-TDD的开发流程 (TDD什么意思?下面有介绍)
{
1.编写组件测试-在单元测试中将设计组件的名称、属性接口、事件接口、用断言工具确定衡量这个组件正确的标准
2.编写组件代码-以单元测试为引导程序,编写组件真实的实现代码,让测试通过
3.运行测试,并看到测试通过
4.重构
}
测试工具:
测试管理工具(测试加载器) Karma
{
Karma-自动化测试程序的入口,执行以下任务
1.为测试程序注入指定依赖包
2.可同时在一个或多个浏览器宿主中执行测试,满足兼容性测试需求
3.执行代码覆盖性测试
4.输出测试报告
5.执行自动化测试
}
测试框架 Mocha (测试Node和浏览器js代码)
vue官网单元测试框架 Vue Test Utils
断言库 chai / sinon
{
expect和should是BDD风格,使用相同链式语言组织断言,不同点在于初始化断言方式:
1.expect使用构造函数来创建断言对象实例
2.should通过为Object.prototype新增方法来实现断言(should不支持IE); expect直接指向chai.expect
而should则是chai.should()
注:在构建vue-cli工程时,添加了单元测试是不需要手动配置chai;
chai和sinon 被Karma通过Karma-sinon-chai插件直接嵌入到单元测试的上下文中,不用import就可以使用
}
那问题来了?sinon是什么?辅助库 sinon-chai
{
sinon: 负责仿真
三个方法: 调用侦测Spy / 接口仿真 Stub / 对象仿真 Mock
}
测试浏览器 chrome (PhantomJs 无界面浏览器,速度比chrome快)
测试覆盖率统计工具 Karma-coverage
test文件夹下unit,单元测试相关文件
specs-中存放测试脚本(所有的测试文件, 都将放specs这个目录下, 并以测试脚本名.spec.js结尾命名)
coverage文件里存放测试报告,html打开的是代码覆盖率
Karma.config.js配置文件
npm run unit 输出
demo:
1.被测试的组件HelloWorld.vue
<template>
<div class="hello">
<h1>Welcome to Your Vue.js App</h1>
</div>
</template>
2.测试脚本HelloWorld.spec.js
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是"测试套件",表示一组相关的测试。它是一个函数,
第一个参数是测试套件的名称("加法函数的测试"),
第二个参数是一个实际执行的函数。
it是"测试用例"(test case),表示一个单独的测试,是测试的最小单位。
它也是一个函数,第一个参数是测试用例的名称,第二个参数是一个实际执行的函数。
所有的测试用例(it块)都应该含有一句或多句的断言
断言(判断源码的实际执行结果与预期结果是否一致),如果不一致, 就会抛出错误
expect(content).to.equal('Welcome to Your Vue.js App')实际为变量content应等于'Welcome to Your Vue.js App'
Vue的脚手架提供的断言库是sino-chai, 是一个基于Chai的断言库
chai官方文档:https://www.chaijs.com/
chai官方文档翻译:https://www.jianshu.com/p/f200a75a15d2
注意:Mocha测试钩子
在describe块中提供了四个钩子: before(), after(), beforeEach(), afterEach(). 它们会在以下时间执行:
describe('钩子说明', function() { // 第一个参数提示要测试什么;第二个匿名函数
before(function() {
// 在所有场景执行之前执行(执行一次)
});
after(function() {
// 在所有场景执行之后执行(执行一次)
});
beforeEach(function() {
// 在每个场景测试执行之前执行
});
afterEach(function() {
// 在每个场景执行完成之后执行
});
});
举个例子:
describe('Array', () => {
let expectTarget = []
beforeEach(() => {
// 在每个场景测试执行之前执行
expectTarget.push(1)
});
afterEach(() => {
// 在每个场景执行完成之后执行
expectTarget = [] // 清空
});
it('应该存在一个为1的整数', () => {
expect(expectTarget[0]).to.eqls(1)
});
it('应该存在多个的期望值检测', () => {
expect(expectTarget[0]).to.eqls(1)
expect(true).to.eqls(true)
});
});
Mocha 官方文档:https://mochajs.org/
Mocha 官方文档翻译:https://www.jianshu.com/p/9c78548caffa
阮一峰 - 测试框架 Mocha 实例教程:http://www.ruanyifeng.com/blog/2015/12/a-mocha-tutorial-of-examples.html
vue-test-utils官方文档:https://vue-test-utils.vuejs.org/zh/api/#render
vue-test-utils常用api
find(): 返回匹配选择器的第一个DOM节点或Vue组件的wrapper, 可以使用任何有效的选择器
text(): 返回wrapper的文本内容
html(): 返回wrapper DOM的HTML字符串
trigger(): 在该 wrapper DOM 节点上触发一个事件
setData(): 设置data的属性并强制更新
it('find()/text()/html()方法', () => {
const wrapper = mount(Counter);
const h3 = wrapper.find('h3');
expect(h3.text()).to.equal('Counter.vue');
expect(h3.html()).to.equal('<h3>Counter.vue</h3>');
})
it('trigger()方法', () => {
const wrapper = mount(Counter);
const buttonOfSync = wrapper.find('.sync-button');
buttonOfSync.trigger('click');
buttonOfSync.trigger('click');
const count = Number(wrapper.find('.num').text());
expect(count).to.equal(2);
})
it('setData()方法',() => {
const wrapper = mount(Counter);
wrapper.setData({foo: 'bar'});
expect(wrapper.vm.foo).to.equal('bar');
})
遇到vue-异步测试情况
Mocha异步测试在it内加一个done函数,在所有的断言执行完成后调用done()就可以释放测试用例并告知Mocha测试的执行结果
例子:一个User对象,具有一个save方法,这个方法通过ajax将数据保存到服务器端,若服务端没有返回错误,即认为这个save方法是成功的
describe('User', () => {
describe('#save()方法', () => {
it('应该成功保存到服务器端且不会返回任何错误信息', done => {
const user = new User('纳尼')
user.save( err => {
if (err) done(err) // 如果返回错误码直接将错误码输出到控制台
else done()
})
})
})
})
优化,将done作为回调参数使用
describe('User', () => {
describe('#save()方法', () => {
it('应该成功保存到服务器端且不会返回任何错误信息', done => {
const user = new User('纳尼')
user.save(done)
})
})
})
使用promise的另一种替代方案就是将chai断言作为it的返回值,将chai断言作为一个promise对象返回
让Mocha进行链式处理,需要借助chai-as-promised库支持(https:www.npmjs.com/package/chai-as-promised)
在Mocha v3.0.0之后的版本中,如果直接构造ES6上的Promise对象则会被Mocha认为是非法的
it('应该完成此测试', done => {
return new Promise( resolve => {
assert.ok(true)
resolve()
}).then(done)
})
会提示异常信息
扩展:基于Nightwatch的端到端测试环境
端到端测试,简称e2e (End to End test):
***侧重于检测界面的交互效果与操作逻辑是否正确
{
1.单元测试侧重:检验函数的输出结果
2.e2e侧重:从用户视角,对真实系统的访问行为进行仿真
}
简单理解:
1.单元测试的功能只能确保单个组件的质量,无法测试具体的业务流程是否运作正常
2.e2e是面对 组件与组件之间、用户与真实环境之间的一种集成性测试
demo: https://github.com/Lee-Tanghui/Vue-Testing-Demo/issues
组件单元测试:https://blog.csdn.net/hsany330/article/details/73650020
***更多请参考官网例子:https://cn.vuejs.org/v2/guide/unit-testing.html
补充下:具体去google一下
Test-Driven Development(TDD)即测试驱动开发,一种测试先于编写代码的思想用于指导软件开发
Behavior Driven Development(BDD)行为驱动开发是一种敏捷软件开发的技术
单元测试(白盒测试),主要用于测试开发人员编写的代码是否正确,这部分工作都是开发人员自己来做的;通常而言,一个单元测试是用于判断某个特定条件(或者场景)下某个特定函数的行为
BDD(灰盒测试、黑盒测试)