详解Vue单元测试Karma+Mocha学习笔记


Posted in Javascript onJanuary 31, 2018

在使用vue-cli创建项目的时候,会提示要不要安装单元测试和e2e测试。既然官方推荐我们使用这两个测试框架,那么我们就动手去学习实践一下他们吧。

简介

Karma

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

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

Mocha

Mocha是一个测试框架,在vue-cli中配合chai断言库实现单元测试。

而Chai断言库可以看Chai.js断言库API中文文档,很简单,多查多用就能很快掌握。

我对测试框架的理解

npm run unit 执行过程

  1. 执行 npm run unit 命令
  2. 开启Karma运行环境
  3. 使用Mocha去逐个测试用Chai断言写的测试用例
  4. 在终端显示测试结果
  5. 如果测试成功,karma-coverage 会在 ./test/unit/coverage 文件夹中生成测试覆盖率结果的网页。

Karma

对于Karma,我只是了解了一下它的配置选项。

下面是Vue的karma配置,简单注释了下:

var webpackConfig = require('../../build/webpack.test.conf')

module.exports = function (config) {
 config.set({
  // 浏览器
  browsers: ['PhantomJS'],
  // 测试框架
  frameworks: ['mocha', 'sinon-chai', 'phantomjs-shim'],
  // 测试报告
  reporters: ['spec', 'coverage'],
  // 测试入口文件
  files: ['./index.js'],
  // 预处理器 karma-webpack
  preprocessors: {
   './index.js': ['webpack', 'sourcemap']
  },
  // Webpack配置
  webpack: webpackConfig,
  // Webpack中间件
  webpackMiddleware: {
   noInfo: true
  },
  // 测试覆盖率报告
  // https://github.com/karma-runner/karma-coverage/blob/master/docs/configuration.md
  coverageReporter: {
   dir: './coverage',
   reporters: [
    { type: 'lcov', subdir: '.' },
    { type: 'text-summary' }
   ]
  }
 })
}

Mocha和chai

我们看下官方的例子(都用注释来解释代码意思了):

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

需要知道的知识点:

  1. 测试脚本都要放在 test/unit/specs/ 目录下。
  2. 脚本命名方式为  [组件名].spec.js。
  3. 所谓断言,就是对组件做一些操作,并预言产生的结果。如果测试结果与断言相同则测试通过。
  4. 单元测试默认测试 src 目录下除了 main.js 之外的所有文件,可在 test/unit/index.js 文件中修改。
  5. Chai断言库中,to be been is that which and has have with at of same 这些语言链是没有意义的,只是便于理解而已。
  6. 测试脚本由多个  descibe 组成,每个 describe 由多个 it 组成。
  7. 了解异步测试
it('异步请求应该返回一个对象', done => {
  request
  .get('https://api.github.com')
  .end(function(err, res){
   expect(res).to.be.an('object');
   done();
  });
});

了解一下 describe 的钩子(生命周期)

describe('hooks', function() {

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

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

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

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

 // test cases
});

实践

上面简单介绍了单元测试的用法,下面来动手在Vue中进行单元测试!

util.js

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

const Constructor = Vue.extend(Hello) // 获得Hello组件实例
 const vm = new Constructor().$mount() // 将组件挂载到DOM上

以上写法只是简单的获取组件,有时候我们需要传递props属性、自定义方法等,还有可能我们需要用到第三方UI框架。所以以上写法非常麻烦。

这里推荐Element的单元测试工具脚本Util.js,它封装了Vue单元测试中常用的方法。下面demo也是根据该 Util.js来写的。
这里简单注释了下各方法的用途。

/**
 * 回收 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) {}

示例一

示例一中我们测试了 Hello 组件的各种元素的数据,学习  util.js 的 destroyVM 和 createTest 方法的用法以及如何获取目标元素进行测试。获取DOM元素的方式可查看DOM 对象教程。

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: {
  content: String
 },
 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

示例二

示例二中我们使用 createTest 创建测试组件测试点击事件,用 createVue 创建Vue示例对象测试组件 Click 的使用。这里主要可以看下到 createVue 方法的使用。

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: {
   type: Number,
   default: 1
  },
  InitNum: {
   type: Number,
   default: 1
  }
 },
 data () {
  return {
   ClickNum: 0,
   ResultNum: 0
  }
 },
 mounted () {
  this.ResultNum = this.InitNum
 },
 methods: {
  add () {
   this.ResultNum += this.AddNum
   this.ClickNum++
   this.$emit('result', {
    ClickNum: this.ClickNum,
    ResultNum: this.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('测试按钮点击事件', () => {
  vm = createTest(Click, {
   AddNum: 10,
   InitNum: 11
  }, true)
  let buttonElm = vm.$el.querySelector('button')
  buttonElm.click()
  buttonElm.click()
  buttonElm.click()
  // setTimeout 的原因
  // 在数据改变之后,界面的变化会有一定延时。不用timeout有时候会发现界面没有变化
  setTimeout(done => {
   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对象', () => {
  let result
  vm = createVue({
   template: `
    <click @click="handleClick"></click>
   `,
   props: {
    AddNum: 10,
    InitNum: 11
   },
   methods: {
    handleClick (obj) {
     result = obj
    }
   },
   components: {
    Click
   }
  }, true)
  vm.$el.click()
  vm.$nextTick(done => {
   expect(result).to.be.exist
   expect(result.ClickNum).to.equal(1)
   expect(result.ResultNum).to.be.equal(21)
   done()
  })
})

输出结果

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

其他

所有示例代码都放Github仓库中便于查看。如果想查看更多好的测试用例,建议配合 Util.js 看一下 Element 的单元测试脚本的写法,里面有很多测试脚本可以供我们学习。作为被广大Vue用户使用的UI组件库,测试脚本肯定也写很很不错的~甚至可以将这些脚本照抄一遍,相信这会对学习Vue组件的单元测试有很大帮助。

下面是本人看Element单元测试的笔记,供参考。

Util.js 方法包含了大多数Vue组件化的测试需求。

vm.$el vm.$nextTick 和 vm.$ref 都是在测试过程中比较常用的一些Vue语法糖。

需要注意: vm.$nextTick 方法是异步的,所以需要在里面使用done方法。

异步断言,方法参数需要是 _ 或者 done

大多数时候查询元素通过 querySelector 方法查询class获得

vm.$el.querySelector('.el-breadcrumb').innerText

大多数情况下查询是否存在某个Class通过 classList.contains 方法获得,查找的结果为 true 或 false

vm.$el .classList.contains('el-button--primary')

异步测试必须以 done() 方法结尾。setTimeout 和 vm.$nextTick 是常用的异步测试。

实现按钮点击:通过获取按钮元素 btn,执行 btn.click() 方法实现。

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

triggerEvent(vm.$refs.cascader.$el, 'mouseenter');
vm.$nextTick(_ => {
   vm.$refs.cascader.$el.querySelector('.el-cascader__clearIcon').click();
   vm.$nextTick(_ => {
    expect(vm.selectedOptions.length).to.be.equal(0);
    done();
   });
});

Vue.js学习系列项目地址:https://github.com/violetjack/VueStudyDemos

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
些很实用且必用的小脚本代码
Jun 26 Javascript
如何快速的呈现我们的网页的技巧整理
Jul 01 Javascript
js png图片(有含有透明)在IE6中为什么不透明了
Feb 07 Javascript
JavaScript打印网页指定区域的例子
May 03 Javascript
兼容最新firefox、chrome和IE的javascript图片预览实现代码
Aug 08 Javascript
D3.js实现饼状图的方法详解
Sep 21 Javascript
BootStrapTable 单选及取值的实现方法
Jan 10 Javascript
jquery实现侧边栏左右伸缩效果的示例
Dec 19 jQuery
JS工厂模式开发实践案例分析
Oct 17 Javascript
详解vue中使用axios对同一个接口连续请求导致返回数据混乱的问题
Nov 06 Javascript
js实现车辆管理系统
Aug 26 Javascript
vue-router 按需加载 component: () =&gt; import() 报错的解决
Sep 22 Javascript
Vue单页面应用保证F5强刷不清空数据的解决方案
Jan 31 #Javascript
Vue服务器渲染Nuxt学习笔记
Jan 31 #Javascript
微信小程序页面生命周期详解
Jan 31 #Javascript
在vue项目中使用Nprogress.js进度条的方法
Jan 31 #Javascript
pace.js和NProgress.js两个加载进度插件的一点小总结
Jan 31 #Javascript
微信小程序App生命周期详解
Jan 31 #Javascript
jQuery NProgress.js加载进度插件的简单使用方法
Jan 31 #jQuery
You might like
全国FM电台频率大全 - 4 山西省
2020/03/11 无线电
php中取得URL的根域名的代码
2011/03/23 PHP
PHP判断字符串长度的两种方法很实用
2015/09/22 PHP
四个PHP非常实用的功能
2015/09/29 PHP
PHP实现数组和对象的相互转换操作示例
2019/03/20 PHP
Struts2的s:radio标签使用及用jquery添加change事件
2013/04/08 Javascript
理解javascript中DOM事件
2015/12/25 Javascript
AngularJS 支付倒计时功能实现思路
2017/06/05 Javascript
详细分析JS函数去抖和节流
2017/12/05 Javascript
运用js实现图层拖拽的功能
2019/05/24 Javascript
vue实现axios图片上传功能
2019/08/20 Javascript
QML实现圆环颜色选择器
2019/09/25 Javascript
Vue CLI4 Vue.config.js标准配置(最全注释)
2020/06/05 Javascript
[04:01]2014DOTA2国际邀请赛 TITAN告别Ohaiyo期望明年再战
2014/07/15 DOTA
Python使用Redis实现作业调度系统(超简单)
2016/03/22 Python
python实现拓扑排序的基本教程
2018/03/11 Python
Python下调用Linux的Shell命令的方法
2018/06/12 Python
Python正则表达式指南 推荐
2018/10/09 Python
pygame游戏之旅 添加键盘按键的方法
2018/11/20 Python
对python读取CT医学图像的实例详解
2019/01/24 Python
浅谈Python中函数的定义及其调用方法
2019/07/19 Python
PyTorch中Tensor的数据统计示例
2020/02/17 Python
Python Django form 组件动态从数据库取choices数据实例
2020/05/19 Python
LN-CC美国:伦敦时尚生活的缩影
2019/02/19 全球购物
澳大利亚宠物食品和用品商店:PETstock
2020/01/02 全球购物
HashMap和Hashtable的区别
2013/05/18 面试题
党校培训思想汇报
2013/12/30 职场文书
酒店总经理职务说明书
2014/02/26 职场文书
领导班子对照检查剖析材料
2014/10/13 职场文书
美术教师个人工作总结
2015/02/06 职场文书
酒店辞职信怎么写
2015/02/27 职场文书
考生诚信考试承诺书
2015/04/29 职场文书
七一晚会主持词
2015/06/29 职场文书
2015七夕情人节宣传语
2015/07/14 职场文书
pandas中DataFrame检测重复值的实现
2021/05/26 Python
Android学习之BottomSheetDialog组件的使用
2022/06/21 Java/Android