Mocha是JavaScript的测试框架, 浏览器和Node端都可以使用。可是Mocha自己并不提供断言的功能, 须要借助例如: Chai, 这样的断言库完成测试的功能。html
Mocha的API快速浏览, 更多细节请参考文档vue
⚠️ 注意:node
describe('unit', function () {
it('example', function () {
return true
})
})
复制代码
Mocha支持Promise, Async, callback的形式webpack
// callback
describe('异步测试Callback', function () {
it('Done用例', function (done) {
setTimeout(() => {
done()
}, 1000)
})
})
// promise
describe('异步测试Promise', function () {
it('Promise用例', function () {
return new Promise((resolve, reject) => {
resolve(true)
})
})
})
// async
describe('异步测试Async', function () {
it('Async用例', async function () {
return await Promise.resolve()
})
})
复制代码
// before, beforeEach, 1, afterEach, beforeEach, 2, afterEach, after
describe('MochaHook', function () {
before(function () {
console.log('before')
})
after(function () {
console.log('after')
})
beforeEach(function () {
console.log('beforeEach')
})
afterEach(function () {
console.log('afterEach')
})
it('example1', function () {
console.log(1)
})
it('example2', function () {
console.log(2)
})
})
复制代码
Mocha Hook能够是异步的函数, 支持done,promise, asyncgit
若是beforeEach在任何descride以外添加, 那么这个beforeEach将被视为root hook。beforeEach将会在任何文件, 任何的测试用例前执行。github
beforeEach(function () {
console.log('root beforeEach')
})
describe('unit1', function () {
//...
})
复制代码
若是须要在任何测试用例前执行异步操做也能够使用(DELAYED ROOT SUITE)。使用"mocha --delay"执行测试脚本。"mocha --delay"会添加一个特殊的函数run()到全局的上下文。当异步操做完成后, 执行run函数能够开始执行测试用例web
function deplay() {
return new Promise((resolve, reject) => {
setTimeout(function () {
resolve()
}, 1000)
})
}
deplay().then(function () {
// 异步操做完成后, 开始执行测试
run()
})
describe('unit', function () {
it('example', function () {
return true
})
})
复制代码
describe, 或者it以后添加skip。可让Mocha忽略测试单元或者测试用例。使用skip, 测试会标记为待处理。vue-router
设置测试失败后, 测试重试的次数chrome
describe('retries', function () {
it('retries', function () {
// 设置测试的重试次数
this.retries(3)
const number = Math.random()
if (number > 0.5) throw new Error()
else return true
})
})
复制代码
对于一些接口的测试, 能够使用动态生产测试用例, 配置请求参数的数组, 循环数组动态生成测试用例vue-cli
describe('动态生成测试用例', function () {
let result = []
for(let i = 0; i < 10; i ++) {
result.push(Math.random())
}
result.forEach((r, i) => {
// 动态生成测试用例
it(`测试用例${i + 1}`, function () {
return r < 1
})
})
})
复制代码
若是测试用例, 运行时间超过了slow设置的时间, 会被标红。
describe('unit', function () {
it('example', function (done) {
this.slow(100)
setTimeout(() => {
done()
}, 200)
})
})
复制代码
设置测试用例的最大超时时间, 若是执行时间超过了最大超时时间,测试结果将为错误
describe('unit', function () {
it('example', function (done) {
this.timeout(100)
setTimeout(() => {
done()
}, 200)
})
})
复制代码
Chai是Node和浏览器的BDD/TDD断言库。下面将介绍, BDD风格的API, expect。should兼容性较差。
Chai的API快速浏览, 更多细节请参考文档
对断言结果取反
it('not', function () {
const foo = 1
expect(foo).to.equal(1)
expect(foo).to.not.equal(2)
})
复制代码
进行断言比较的时候, 将进行深比较
it('deep', function () {
const foo = [1, 2, 3]
const bar = [1, 2, 3]
expect(foo).to.deep.equal(bar)
expect(foo).to.not.equal(bar)
})
复制代码
启用点和括号表示法
it('nested', function () {
const foo = {
a: [
{
a: 1
}
]
}
expect(foo.a[0].a).to.equals(1)
})
复制代码
断言时将会忽略对象prototype上的属性
it('own', function () {
Object.prototype.own = 1
const a = {}
expect(a).to.not.have.own.property('own')
expect(a).to.have.property('own')
})
复制代码
ordered.members比较数组的顺序是否一致, 使用include.ordered.members能够进行部分比较, 配合deep则能够进行深比较
it('order', function () {
const foo = [1, 2, 3]
const bar = [1, 2, 3]
const faz = [1, 2]
const baz = [{a: 1}, {b: 2}]
const fzo = [{a: 1}, {b: 2}]
expect(baz).to.have.deep.ordered.members(fzo)
expect(foo).to.have.ordered.members(bar)
expect(foo).to.have.include.ordered.members(faz)
})
复制代码
要求对象至少包含一个给定的属性
it('any', function () {
const foo = {a: 1, b: 2}
expect(foo).to.have.any.keys('a', 'b', 'c')
})
复制代码
要求对象包含所有给定的属性
it('all', function () {
const foo = {a: 1, b: 2}
expect(foo).to.not.have.all.keys('a', 'c')
})
复制代码
用来断言数据类型。推荐在进行更多的断言操做前, 首先进行类型的判断
it('a', function () {
expect('123').to.a('string')
expect(false).to.a('boolean')
expect({a: 1}).to.a('object')
expect('123').to.an('string')
expect(false).to.an('boolean')
expect({a: 1}).to.an('object')
})
复制代码
include断言是否包含, include能够字符串, 数组以及对象(key: value的形式)。同时能够配合deep进行深度比较。
it('include', function () {
expect('love fangfang').to.an('string').to.have.include('fangfang')
expect(['foo', 'bar']).to.an('array').to.have.include('foo', 'bar')
expect({a: 1, b: 2, c: 3}).to.an('object').to.have.include({a: 1})
expect({a: {b: 1}}).to.an('object').to.deep.have.include({a: {b: 1}})
expect({a: {b: [1, 2]}}).to.an('object').to.nested.deep.have.include({'a.b[0]': 1})
})
复制代码
断言数组, 字符串长度为空。或者对象的可枚举的属性数组长度为0
进行"==="比较的断言
能够不使用deep进行严格类型的比较
范围断言
it('within', function () {
expect(2).to.within(1, 4)
})
复制代码
断言目标是否包含指定的属性
it('property', function () {
expect({a: 1}).to.have.property('a')
// property能够同时对key, value断言
expect({a: { b: 1 }}).to.deep.have.property('a', {b: 1})
expect({a: 1}).to.have.property('a').to.an('number')
})
复制代码
断言数组, 字符串的长度
it('lengthOf', function () {
expect([1, 2, 3]).to.lengthOf(3)
expect('test').to.lengthOf(4)
})
复制代码
断言目标是否具备指定的key
it('keys', function () {
expect({a: 1}).to.have.any.keys('a', 'b')
expect({a: 1, b: 2}).to.have.any.keys({a: 1})
})
复制代码
断言目标是否具备指定的方法
it('respondTo', function () {
class Foo {
getName () {
return 'fangfang'
}
}
expect(new Foo()).to.have.respondsTo('getName')
expect({a: 1, b: function () {}}).to.have.respondsTo('b')
})
复制代码
断言函数的返回值为true, expect的参数是函数的参数
it('satisfy', function () {
function bar (n) {
return n > 0
}
expect(1).to.satisfy(bar)
})
复制代码
断言目标是否为数组的成员
it('oneOf', function () {
expect(1).to.oneOf([2, 3, 1])
})
复制代码
断言函数执行完成后。函数的返回值发生变化
it('change', function () {
let a = 0
function add () {
a += 3
}
function getA () {
return a
}
// 断言getA的返回值发生变化
expect(add).to.change(getA)
// 断言变化的大小
expect(add).to.change(getA).by(3)
})
复制代码
Karma是一个测试工具,能让你的代码在浏览器环境下测试。代码多是设计在浏览器端执行的,在node环境下测试可能有些bug暴露不出来;另外,浏览器有兼容问题,karma提供了手段让你的代码自动在多个浏览器(chrome,firefox,ie等)环境下运行。若是你的代码只会运行在node端,那么你不须要用karma。
// 安装karma
npm install karma --save-dev
复制代码
// 在package.json文件中添加测试命令
scripts: {
test:unit: "karma start"
}
复制代码
经过Karma测试项目, 须要在项目中添加配置karam.conf.js的文件。推荐使用karam init命令生成初始化的配置文件。下面是, karam init 命令的配置项。生成配置文件以后, 就能够经过"npm run test:unit"命令进行单元测试了。
1. Which testing framework do you want to use(使用的测试框架)?
mocha
2. Do you want to use Require.js(是否使用Require)?
no
3. Do you want to capture any browsers automatically(须要测试的浏览器)?
Chrome, IE,
4. What is the location of your source and test files(测试文件的位置)?
test/*.test.js
5. Should any of the files included by the previous patterns be excluded(须要排除的文件) ?
node_modules
6. Do you want Karma to watch all the files and run the tests on change(何时开始测试) ?
change
复制代码
// 安装
npm install --save-dev chai karma-chai
复制代码
若是测试发送在浏览器环境, Karma会将测试文件, 模拟运行在浏览器环境中。因此推荐使用webpack, babel, 对测试文件进行编译操做。Karma中提供了处理文件中间件的配置。ps: 以前因为浏览器环境不支持require, 而我在test文件中使用了require, 而且我没有将测试文件进行编译, 耽误了我半天的时间:(
karma.conf.js配置的更多的细节,能够查看karma文档
// 安装babel
npm install --save-dev karma-webpack webpack babel-core babel-loader babel-preset-env
复制代码
// 对文件添加webpack的配置, 对配置文件使用babel进行处理
module.exports = function(config) {
config.set({
basePath: '',
frameworks: ['mocha', 'chai'],
files: [
'test/*.test.js'
],
exclude: [
'node_modules'
],
preprocessors: {
'test/*.test.js': ['webpack']
},
webpack: {
// webpack4中新增的mode模式
mode: "development",
module: {
rules: [
{ test: /\.js?$/, loader: "babel-loader", options: { presets: ["env"] }, },
]
}
},
reporters: ['progress'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: false,
concurrency: Infinity
})
}
复制代码
// 经过babel, 浏览器能够正常的解析测试文件中的require
const modeA = require('../lib/a')
const expect = require('chai').expect
describe('test', function () {
it('example', function () {
expect(modeA.a).to.equals(1)
})
})
复制代码
与处理浏览器中的require同理, 若是咱们须要对.vue文件进行测试, 则须要经过vue-loader的对.vue文件进行处理。
咱们首先经过vue-cli初始化咱们的项目, 这里我使用的是vue-cli2.x的版本, 3.x的版本vue-cli对webpack的配置做出了抽象, 没有将webpack的配置暴露出来, 咱们会很难理解配置。若是须要使用vue-cli3.x集成karma, 则须要另外的操做。
// 安装karma以及karam的相关插件
npm install karma mocha karma-mocha chai karma-chai karma-webpack --save-dev
// 配置karma.conf.js
// webpack的配置直接使用webpack暴露的配置
const webpackConfig = require('./build/webpack.test.conf')
module.exports = function(config) {
config.set({
basePath: '',
frameworks: ['mocha'],
files: [
'test/*.test.js'
],
exclude: [
],
// 测试文件添加中间件处理
preprocessors: {
'test/*.test.js': ['webpack']
},
webpack: webpackConfig,
reporters: ['progress'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: false,
concurrency: Infinity
})
}
复制代码
⚠️ 注意: 这里依然存在一个问题Can't find variable: webpackJsonp, 咱们能够将webpackConfig文件中的CommonsChunkPlugin插件注释后, karma将会正常的工做。
import { expect } from 'chai'
import { mount } from '@vue/test-utils'
import HelloWorld from '../src/components/HelloWorld.vue'
describe('HelloWorld.vue', function () {
const wrapper = mount(Counter)
it('Welcome to Your Vue.js App', function () {
console.log('Welcome to Your Vue.js App')
expect(wrapper.vm.msg).to.equals('Welcome to Your Vue.js App')
})
})
复制代码
对于vue-cli3我尝试本身添加karma.conf.js的配置, 虽然能够运行,可是存在问题。issue中, 官方建议我在vue-cli3版本中使用vue-cli的karma的插件解决。
对于vue-cli3,能够使用vue-cli-plugin-unit-karma插件, 集成vue-cli3与karma
对于VueTestUtils, 我这里并不想作过多的介绍。由于它拥有详尽和完善的中文文档, 在这里我只会作大体的概述。文档地址, 值得注意的一点文档中部份内容已通过时, 以及不适用与vue-cli3
VueTestUtils是Vue.js官方的单元测试实用工具库, 提供不少便捷的接口, 好比挂载组件, 设置Props, 发送emit事件等操做。咱们首先使用vue-cli3建立项目, 并添加vue-cli-plugin-unit-karma的插件。而vue-cli-plugin-unit-karma插件已经集成了VueTestUtils工具, 无需重复的安装。
VueRouter, 是Vue的全局插件, 而咱们测试的都是单文件组件, 咱们该如何测试VueRouter的呢?, VueTestUtils为咱们提供了localVue的API, 可让咱们在测试单文件组件的时候, 使用VueRouter。(更多内容请参考文档)
import { mount, createLocalVue } from '@vue/test-utils'
import Test from '../../src/views/Test.vue'
import VueRouter from 'vue-router'
it('localVue Router', function () {
const localVue = createLocalVue()
localVue.use(VueRouter)
const router = new VueRouter()
// 挂载组件的同时, 同时挂载VueRouter
const wrapper = mount(Test, {
localVue,
router
})
// 咱们能够在组件的实例中访问$router以及$route
console.log(wrapper.vm.$route)
})
复制代码
对于route, 咱们也能够经过mocks进行伪造, 并注入到组件的实例中
it('mocks', function () {
// 伪造的$route的对象
const $route = {
path: '/',
hash: '',
params: { id: '123' },
query: { q: 'hello' }
}
const wrapper = mount(Test, {
mocks: {
$route
}
})
// 在组件的实例中访问伪造的$route对象
console.log(wrapper.vm.$route)
})
复制代码
对于Vuex的测试, 咱们须要明确一点咱们不关心这个action或者mutation作了什么或者这个store是什么样子的, 咱们只须要测试action将会在适当的时机触发。对于getters咱们也不关心它返回的是什么, 咱们只须要测试这些getters是否被正确的渲染。更多细节请查看文档。
describe('Vuex', function () {
// 应用全局的插件Vuex
const localVue = createLocalVue()
localVue.use(Vuex)
let actions
let store
let getters
let isAction = false
// 在每一个测试用例执行前, 伪造action以及getters
// 每一个测试用例执行前, 都会重置这些数据
beforeEach(function () {
// 是否执行了action
isAction = false
actions = {
actionClick: function () {
isAction = true
},
actionInput: function () {
isAction = true
}
}
getters = {
name: () => '方方'
}
// 生成伪造的store
store = new Vuex.Store({
state: {},
actions,
getters
})
})
// 测试是否触发了actions
it('若是text等于actionClick, 触发了actionClick action', function () {
const wrapper = mount(TestVuex, { store, localVue })
wrapper.vm.text = 'actionClick'
// 若是成功触发actionClick action, isAction将为true
expect(isAction).to.true
})
it('若是text等于actionInput,触发actionInput action', function () {
const wrapper = mount(TestVuex, { store, localVue })
wrapper.vm.text = 'actionInput'
// 若是成功触发actionInput action, isAction将为true
expect(isAction).to.true
})
// 对于getters, 同理action
// 咱们只关注是否正确渲染了getters,并不关心渲染了什么
it('测试getters', function () {
const wrapper = mount(TestVuex, {
store,
localVue
})
// 测试组件中使用了getters的dom, 是否被正确的渲染
expect(wrapper.find('p').text()).to.equals(getters.name())
})
})
复制代码