Vue单元测试入门

503 阅读5分钟

一、简介

Karma

Karma是一个基于Node.js的JavaScript测试执行过程管理工具。该工具在Vue中的主要作用是将项目运行在各种主流Web浏览器进行测试。

换句话说,它是一个测试工具,能让你的代码在浏览器环境下测试。需要它的原因在于,你的代码可能是设计在浏览器端执行的,在node环境下测试可能有些bug暴露不出来;另外,浏览器有兼容问题,karma提供了手段让你的代码自动在多个浏览器环境下运行。如果你的代码只运行在node端,则不需要karma。

安装和配置

// 安装 Karma
npm install karma --save-dev// 下载项目所需的依赖以及插件
npm install 
karma-chrome-launcher,
karma-firefox-launcher,
karma-ie-launcher,
karma-coverage,
karma-mocha,
karma-webpack,
...
--save-dev

初始化配置文件

karma init karma.config.js

init命令会提示用户进行选项选择,按向导完成配置

配置简单介绍:

image.png

启动Karma

karma start karma.config.js

启动后会根据配置文件启动服务server并在目标浏览器里启动测试容器页(client html page):http://localhost:9876

如果浏览器选择ChromeHeadless,则不会启动浏览器。

要在不同浏览器测试,需要安装对应的launcher插件:

karma-chrome-launcher karma-firefox-launcher karma-safari-launcher

工作原理

  1. 基于配置文件和插件,启动server
  1. 启动目标浏览器,并打开karam client page。
  1. server监控测试代码文件变动,并通过socket通知client page
  1. client page通知context page运行测试代码,收集测试结果,返回给server
  1. server端的reporter负责处理测试结果:打印到console,保存到文件,发送给其他service等

说明:

  • client page包含socket和iframe,分别用于与server通信、运行测试代码
  • server和client page之间通过websocket通信, context page与client page通过postMessage通信

流程图:

img

Mocha + Chai

Mocha是一个测试框架,在vue-cli中配合chai断言库实现单元测试。推荐看阮一峰老师的测试框架 Mocha 实例教程

Chai是一个测试断言库,提供了更好的断言语法。所谓断言,就是对组件做一些操作,并预言产生的结果。如果测试结果与断言相同则测试通过。可以看Chai.js断言库API中文文档

举个🌰

// 导入Vue用于生成Vue实例
import Vue from 'vue';
// 导入组件
import Hello from '@/component/Hello';
// 测试脚本里面应该包括一个或多个describe块,称为测试套件(test suite)
describe('Hello.vue', () => {
// 没个describe块应该包括一个或多个it块,称为测试用例(test case)
it('should render correct contents', () => {
// 获取Hello组件实例
const Constructor = Vue.extend(Hello);
// 将组件挂到DOM上
const vm = new Constructor.$mount();
// 断言:DOM中class为hello的元素中的h1元素的文本内容为Welcome to Your Vue.js App
expect(vm.$el.querySelector('.hello h1').textContent).to.be.equal('Welcome to Your Vue.js App');
})
})

⚠️注意:

  • 测试脚本都要放在test/unit/specs/目录下
  • 脚本命名方式为[组件名].spec.js
  • 所谓断言,就是对组件做一些操作,并预言产生的结果,如果测试结果与断言相同则测试通过
  • 单元测试默认测试src目录下除了main.js之外的所有文件,可在test/unit/index.js文件中修改
  • Chai断言库中,to\be\been\is\that\which\and\has\have\with\at\of\same这些语言链是没有意义的,只是便于理解而已。
  • 测试脚本由多个describe组成,每个describe由多个it组成

异步测试

对于异步操作,it块执行的时候,需要传入一个回调函数,通常该函数被命名为done。当测试结束的时候,必须显式调用这个函数,告诉Mocha测试结束了。否则,Mocha就无法知道,测试是否结束,会一直等到超时报错。

it('异步测试应该返回一个对象', done => {
request.get('https://api.github.com').end(function(err, res) => {
expect(res).to.be.an('object');
done();
})
})

describe的生命周期

describe('hooks', ()=>{
before(function(){
// 在本区块的所有测试用例执行之前执行
});

after(function(){
// 在本区块的所有测试用例执行之后执行
});

beforeEach(function(){
// 在本区块的每个测试用例执行之前执行
});

afterEach(function(){
// 在本区块的每个测试用例执行之后执行
});

// test case
})

实战

从Vue官方的demo可以看出,对于Vue的单元测试我们需要将组件实例化为一个Vue实例,有时还需要挂载到DOM上。

const Constructor = Vue.extend(Hello);
const vm = new Constructor.$mount();

以上写法只是简单的获取组件,有时候我们需要传递props属性、自定义方法等,还有可能我们需要用到第三方UI框架。所以以上写法非常麻烦。所以我们可以将Vue单元测试中常用的方法封装成util.js,方便调用。以下为参照Element的单元测试工具脚本Util.js编写的demo

/**
* 回收 vm,一般在每个测试脚本测试完成后执行回收vm。
* @param {Object} vm
*/
exports.destroyVM = function (vm) {}
​
/**
* 创建一个 Vue 的实例对象
* @param {Object|String} Compo     - 组件配置,可直接传 template
* @param {Boolean=false} mounted   - 是否添加到 DOM 上
* @return {Object} vm
*/
exports.createVue = function (Compo, mounted = false) {}
​
/**
* 创建一个测试组件实例
* @param {Object} Compo         - 组件对象
* @param {Object} propsData     - props 数据
* @param {Boolean=false} mounted - 是否添加到 DOM 上
* @return {Object} vm
*/
exports.createTest = function (Compo, propsData = {}, mounted = false) {}
​
/**
* 触发一个事件
* 注: 一般在触发事件后使用 vm.$nextTick 方法确定事件触发完成。
* mouseenter, mouseleave, mouseover, keyup, change, click 等
* @param {Element} elm     - 元素
* @param {String} name     - 事件名称
* @param {*} opts           - 配置项
*/
exports.triggerEvent = function (elm, name, ...opts) {}
​
/**
* 触发 “mouseup” 和 “mousedown” 事件,既触发点击事件。
* @param {Element} elm     - 元素
* @param {*} opts         - 配置选项
*/
exports.triggerClick = function (elm, ...opts) {}

示例一: props

Hello.vue

<template>
 <div class="hello">
   <h1 class="hello-title">{{ msg }}</h1>
   <h2 class="hello-content">{{ content }}</h2>
 </div>
</template><script>
export default {
 name'hello',
 props: {
   contentString
},
 data () {
   return {
     msg'Welcome!'
  }
}
}
</script>

Hello.spec.js

import { destroyVM, createTest } from '../util'
import Hello from '@/components/Hello'
​
describe('Hello.vue', () => {
 let vm
​
 afterEach(() => {
   destroyVM(vm)
})
​
 it('测试获取元素内容', () => {
   vm = createTest(Hello, { content: 'Hello World' }, true)
   expect(vm.$el.querySelector('.hello h1').textContent).to.equal('Welcome!')
   expect(vm.$el.querySelector('.hello h2').textContent).to.have.be.equal('Hello World')
})
​
 it('测试获取Vue对象中数据', () => {
   vm = createTest(Hello, { content: 'Hello World' }, true)
   expect(vm.msg).to.equal('Welcome!')
   // Chai的语言链是无意义的,可以随便写。如下:
   expect(vm.content).which.have.to.be.that.equal('Hello World') 
})
​
 it('测试获取DOM中是否存在某个class', () => {
   vm = createTest(Hello, { content: 'Hello World' }, true)
   expect(vm.$el.classList.contains('hello')).to.be.true
   const title = vm.$el.querySelector('.hello h1')
   expect(title.classList.contains('hello-title')).to.be.true
   const content = vm.$el.querySelector('.hello-content')
   expect(content.classList.contains('hello-content')).to.be.true
})
})

输出结果

Hello.vue
√ 测试获取元素内容
√ 测试获取Vue对象中数据
√ 测试获取DOM中是否存在某个class

示例二:事件

Click.vue

<template>
 <div>
   <span class="init-num">初始值为{{ InitNum }}</span><br>
   <span class="click-num">点击了{{ ClickNum }}次</span><br>
   <span class="result-num">最终结果为{{ ResultNum }}</span><br>
   <button @click="add">累加{{ AddNum }}</button>
 </div>
</template><script>
export default {
 name'Click',
 props: {
   AddNum: {
     typeNumber,
     default1
  },
   InitNum: {
     typeNumber,
     default1
  }
},
 data () {
   return {
     ClickNum0,
     ResultNum0
  }
},
 mounted () {
   this.ResultNum = this.InitNum
},
 methods: {
   add () {
     this.ResultNum += this.AddNum
     this.ClickNum++
     this.$emit('result', {
       ClickNumthis.ClickNum,
       ResultNumthis.ResultNum
    })
  }
}
}
</script>

Click.spec.js

import { destroyVM, createTest, createVue } from '../util'
import Click from '@/components/Click'describe('click.vue', () => {
 let vm
​
 afterEach(() => {
   destroyVM(vm)
})
​
 it('测试按钮点击事件'done => {
   vm = createTest(Click, {
     AddNum10,
     InitNum11
  }, true)
   let buttonElm = vm.$el.querySelector('button')
   buttonElm.click()
   buttonElm.click()
   buttonElm.click()
   // 使用 setTimeout 的原因:
   // 在数据改变之后,界面的变化会有一定延时。不用timeout有时候会发现界面没有变化
   setTimeout(() => {
     expect(vm.ResultNum).to.equal(41)
     expect(vm.$el.querySelector('.init-num').textContent).to.equal('初始值为11')
     expect(vm.$el.querySelector('.click-num').textContent).to.equal('点击了3次')
     expect(vm.$el.querySelector('.result-num').textContent).to.equal('最终结果为41')
     done()
  }, 100)
})
​
 it('测试创建Vue对象'done => {
   let result
   vm = createVue({
     template`
       <click @click="handleClick"></click>
     `,
     props: {
       AddNum10,
       InitNum11
    },
     methods: {
       handleClick (obj) {
         result = obj
      }
    },
     components: {
       Click
    }
  }, true)
   vm.$el.click()
   vm.$nextTick(() => {
     expect(result).to.be.exist
     expect(result.ClickNum).to.equal(1)
     expect(result.ResultNum).to.be.equal(21)
     done()
  })
})

输出结果

click.vue
√ 测试按钮点击事件
√ 测试创建Vue对象

⚠️由于 Vue 进行 异步更新DOM 的情况,一些依赖DOM更新结果的断言必须在 Vue.nextTick 回调中进行。

⚠️ 异步测试必须以 done() 方法结尾。setTimeoutvm.$nextTick 是常用的异步测试。