添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

54.Vue.js 项目使用 Karma 做自动化UI测试小结 #58

Open
@ccforward

Description

Vue.js 项目使用 Karma 做自动化UI测试

内部测试代码不宜公开,因此以开源项目 Rubik UI 的测试代码为例子

只是阶段小结,暂没有面面俱到

安装完 Karma 并自动生成 karma.conf.js

选用 mocha 作为测试框架

chai 作为断言库(添加 sino-chai 做扩展)

用 Chrome 和 Phantom.js 浏览器

同时测试代码还要用 karma-webpack 打包

最后用 spec karma-coverage 来报告测试覆盖率

因此,需要安装一堆的插件...

karma.conf

重点要在 karma 自动生成的配置文件 karma.conf.js 基础上添加 files preprocessors webpack plugins

完整的 karma.conf.js 文件

files 和 preprocessors

根据 Vue.js 官方提供的 例子

karma.conf.js files 的配置为 files: ['./file.js'],

file.js 中对测试起最核心作用的代码如下:

// require 所有的测试代码
const testsContext = require.context('./specs', true, /\.spec$/)
testsContext.keys().forEach(testsContext)

这里使用webpack的 require.context 来引入子目录 specs 下的所有 .spec.js 文件

preprocessors 的配置项为

preprocessors: {
  './file.js': ['webpack', 'sourcemap', 'coverage']

file.js 中 require 的文件都需要 webpack 来打包处理

完整的 file.js 文件

webpack

const webpackConfig = require('../../build/webpack.test.conf');
  webpack: webpackConfig,

上面引入的 webpack.test.conf.js 文件和普通的 webpack 配置没什么差别,除了 entry 之外, output resolve 各种 loader 等等都需要加上.

这里为了看起来方便,直接把 webpack.base.conf.js 文件 copy 过来,再起基础上做的修改

webpack.test.conf.js 文件

plugins

这一项很简单,把需要的插件全写进去

plugins: [ 'karma-webpack', 'karma-sourcemap-loader', 'karma-mocha', 'karma-chai', 'karma-sinon-chai', 'karma-chrome-launcher', 'karma-phantomjs-launcher', 'karma-spec-reporter', 'karma-coverage'

引入组件库

因为 Rubik UI 的引入需要两步

  • use: Vue.use(Rubik)
  • 在钩子函数 mounted 中初始化: vm.$rubik.init()
  • 因为 init 函数的本质是给 body 元素添加 click 或者 touchstar 事件,所以在测试中可以不考虑 mounted 钩子,只要存在 body 元素即可

    同时上面配置中的 file.js 文件也会被 webpack 打包后在浏览器中加载,因此 Rubik UI 的初始化就放在了 file.js

    import Vue from 'vue'
    import { createVM } from './vm'
    import Rubik from 'src/index.js'
    Vue.use(Rubik)
    // vm 是 Vue 的一个实例对象
    const vm = createVM({}, true)
    vm.$rubik.init()

    createVMvm.js 下面会详细介绍

    Vue 的 Unit Test

    关于 Unit Test 官方有文档

    我们这里直接参考了组件库 Element UI 的一个 工具函数 后做了简单修改

    vm.js 文件中 我们可以建立几个工具函数来生成 Vue 的实例

    createComponent

    // 创建一个 Vue 的实例对象
    exports.createComponent = function(Component, props = {}, mounted = false) {
      if (props === true || props === false) {
        mounted = props
        props = {}
      const elm = createElm() // 创建一个位于 body 上的空 div
      const Ctor = Vue.extend(Component)
      return new Ctor({ props }).$mount(mounted === false ? null : elm)
    

    因为很多组件都是 .vue 的单文件组件,所以在测试中可以直接 import 进来用 createComponent 生成对应的实例挂载到 DOM 上

    createVM

    exports.createVM = function(Component, mounted = false) {
      const elm = createElm() // 创建一个位于 body 上的空 div
      if (Object.prototype.toString.call(Component) === '[object String]') {
        Component = { template: Component }
      return new Vue(Component).$mount(mounted === false ? null : elm)
    

    createComponent 很像,可直接传 template 生成更复杂的实例对象,比如

    createVM({
      template: `
          <r-btn class="green white-text" v-dropdown:dropdown>下拉菜单</r-btn>
          <r-dropdown id="dropdown" right hover>
            <li><a class="dropdown-item" href="#">吃饭</a></li>
            <li><a class="dropdown-item" href="#">睡觉</a></li>
          </r-dropdown>
    }, true)

    通过 createComponentcreateVM 连个函数就可以很灵活的生成 .vue 这种单文件组件和自定义标签的组件的 Vue 实例,然后模拟各种操作来执行单元测试

    回到上面 引入组件库 一节中留下的问题

    // vm 是 Vue 的一个实例对象
    const vm = createVM({}, true)
    vm.$rubik.init()

    通过 createVM 生成一个空的 Vue 实例对象 vm 后,vm 就可以直接调用 init 方法来注册事件了。

    因为 init 函数已经挂载 Vue 的原型链上了

    Vue
    
    
    
    
        
    .prototype.$rubik = {
        init: Init,
    

    每一个单独的 Vue 实例对象都能方便调用 init 函数

    主要就是在 *.spec.js 中写各种测试用例

    这一点就和组件、业务强相关了,有两点需要注意

  • 因为 Vue 采用异步更新的 DOM 的机制,所以一些依赖 DOM 更新的结果的断言需要在 Vue.nextTick 的回调中执行
  • 模拟鼠标、键盘的事件后的断言也要放在一个事件循环之后来执行,可以简单 setTimeout( _ => { }, interval) interval 可以根据元素动画的执行时间来调整
  • 模拟鼠标、键盘动作的代码也放在了 vm.js文件

    // 触发事件: mouseenter, mouseleave, mouseover, keyup, change, click...
    exports.fireEvent = function(elm, name, ...opts) {
      let eventName;
      if (/^mouse|click/.test(name)) {
        eventName = 'MouseEvents'
      } else if (/^key/.test(name)) {
        eventName = 'KeyboardEvent'
      } else {
        eventName = 'HTMLEvents'
      const evt = document.createEvent(eventName)
      evt.initEvent(name, ...opts)
      elm.dispatchEvent
        ? elm.dispatchEvent(evt)
        : elm.fireEvent('on' + name, evt)
      return elm
    

    比如 raido 的一个测试用例

    fireEvent(radio2, 'click') setTimeout( _ => { expect(vm.$data.color).to.be.equal('white') expect(radio1.checked).to.be.false expect(radio2.checked).to.be.true done() }, 100) ...

    在模拟了 click 之后经过 100ms 再执行断言

    radio.spec.js 详细代码

  • 测试用例从另一方面来说是个很好的 API 文档,在我实习时候师兄就告诉我看不懂代码就新先去看单元测试,看懂了测试用例就会使用 API 了
  • 代码要易于测试,才是好代码。如果测试效率低,那必然生产环境也会效率低,易于测试就会易于生产。所以经过测试后重构的代码比直接去重构的代码质量肯定会高
  • TDD 是很有效的编码方式,因为通过测试可以快速的反馈我们的设计思路,验证想法
  • (感觉说了很多废话。。。)
  •