使用Karma、Mocha實現vue單元測試

weixin_34120274發表於2017-07-10

Karma

Karma是一個基於Node.js的JavaScript測試執行過程管理工具(Test Runner)。該工具在Vue中的主要作用是將專案執行在各種主流Web瀏覽器進行測試。
換句話說,它是一個測試工具,能讓你的程式碼在瀏覽器環境下測試。需要它的原因在於,你的程式碼可能是設計在瀏覽器端執行的,在node環境下測試可能有些bug暴露不出來;另外,瀏覽器有相容問題,karma提供了手段讓你的程式碼自動在多個瀏覽器(chrome,firefox,ie等)環境下執行。如果你的程式碼只會執行在node端,那麼你不需要用karma。

Mocha

Mocha是一個測試框架,在vue-cli中配合chai斷言庫實現單元測試。
Mocha的常用命令和用法不算太多,看阮一峰老師的測試框架 Mocha 例項教程就可以大致瞭解了。
而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')  
  })
})

需要知道的知識點:

  • 測試指令碼都要放在 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 這些語言鏈是沒有意義的,只是便於理解而已。
  • 測試指令碼由多個 descibe 組成,每個 describe 由多個 it 組成。
  • 瞭解非同步測試
    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();
      });
    });

相關文章