溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

詳解Vue單元測試Karma+Mocha學(xué)習(xí)筆記

發(fā)布時(shí)間:2020-08-27 09:45:05 來源:腳本之家 閱讀:193 作者:VioletJack 欄目:web開發(fā)

在使用vue-cli創(chuàng)建項(xiàng)目的時(shí)候,會(huì)提示要不要安裝單元測試和e2e測試。既然官方推薦我們使用這兩個(gè)測試框架,那么我們就動(dòng)手去學(xué)習(xí)實(shí)踐一下他們吧。

簡介

Karma

Karma是一個(gè)基于Node.js的JavaScript測試執(zhí)行過程管理工具(Test Runner)。該工具在Vue中的主要作用是將項(xiàng)目運(yùn)行在各種主流Web瀏覽器進(jìn)行測試。

換句話說,它是一個(gè)測試工具,能讓你的代碼在瀏覽器環(huán)境下測試。需要它的原因在于,你的代碼可能是設(shè)計(jì)在瀏覽器端執(zhí)行的,在node環(huán)境下測試可能有些bug暴露不出來;另外,瀏覽器有兼容問題,karma提供了手段讓你的代碼自動(dòng)在多個(gè)瀏覽器(chrome,firefox,ie等)環(huán)境下運(yùn)行。如果你的代碼只會(huì)運(yùn)行在node端,那么你不需要用karma。

Mocha

Mocha是一個(gè)測試框架,在vue-cli中配合chai斷言庫實(shí)現(xiàn)單元測試。

而Chai斷言庫可以看Chai.js斷言庫API中文文檔,很簡單,多查多用就能很快掌握。

我對(duì)測試框架的理解

npm run unit 執(zhí)行過程

  1. 執(zhí)行 npm run unit 命令
  2. 開啟Karma運(yùn)行環(huán)境
  3. 使用Mocha去逐個(gè)測試用Chai斷言寫的測試用例
  4. 在終端顯示測試結(jié)果
  5. 如果測試成功,karma-coverage 會(huì)在 ./test/unit/coverage 文件夾中生成測試覆蓋率結(jié)果的網(wǎng)頁。

Karma

對(duì)于Karma,我只是了解了一下它的配置選項(xiàng)。

下面是Vue的karma配置,簡單注釋了下:

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

module.exports = function (config) {
 config.set({
  // 瀏覽器
  browsers: ['PhantomJS'],
  // 測試框架
  frameworks: ['mocha', 'sinon-chai', 'phantomjs-shim'],
  // 測試報(bào)告
  reporters: ['spec', 'coverage'],
  // 測試入口文件
  files: ['./index.js'],
  // 預(yù)處理器 karma-webpack
  preprocessors: {
   './index.js': ['webpack', 'sourcemap']
  },
  // Webpack配置
  webpack: webpackConfig,
  // Webpack中間件
  webpackMiddleware: {
   noInfo: true
  },
  // 測試覆蓋率報(bào)告
  // 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' // 導(dǎo)入Vue用于生成Vue實(shí)例
import Hello from '@/components/Hello' // 導(dǎo)入組件
// 測試腳本里面應(yīng)該包括一個(gè)或多個(gè)describe塊,稱為測試套件(test suite)
describe('Hello.vue', () => {
 // 每個(gè)describe塊應(yīng)該包括一個(gè)或多個(gè)it塊,稱為測試用例(test case)
 it('should render correct contents', () => {
  const Constructor = Vue.extend(Hello) // 獲得Hello組件實(shí)例
  const vm = new Constructor().$mount() // 將組件掛在到DOM上
  //斷言:DOM中class為hello的元素中的h2元素的文本內(nèi)容為Welcome to Your Vue.js App
  expect(vm.$el.querySelector('.hello h2').textContent)
   .to.equal('Welcome to Your Vue.js App') 
 })
})

需要知道的知識(shí)點(diǎn):

  1. 測試腳本都要放在 test/unit/specs/ 目錄下。
  2. 腳本命名方式為  [組件名].spec.js。
  3. 所謂斷言,就是對(duì)組件做一些操作,并預(yù)言產(chǎn)生的結(jié)果。如果測試結(jié)果與斷言相同則測試通過。
  4. 單元測試默認(rèn)測試 src 目錄下除了 main.js 之外的所有文件,可在 test/unit/index.js 文件中修改。
  5. Chai斷言庫中,to be been is that which and has have with at of same 這些語言鏈?zhǔn)菦]有意義的,只是便于理解而已。
  6. 測試腳本由多個(gè)  descibe 組成,每個(gè) describe 由多個(gè) it 組成。
  7. 了解異步測試
it('異步請(qǐng)求應(yīng)該返回一個(gè)對(duì)象', done => {
  request
  .get('https://api.github.com')
  .end(function(err, res){
   expect(res).to.be.an('object');
   done();
  });
});

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

describe('hooks', function() {

 before(function() {
  // 在本區(qū)塊的所有測試用例之前執(zhí)行
 });

 after(function() {
  // 在本區(qū)塊的所有測試用例之后執(zhí)行
 });

 beforeEach(function() {
  // 在本區(qū)塊的每個(gè)測試用例之前執(zhí)行
 });

 afterEach(function() {
  // 在本區(qū)塊的每個(gè)測試用例之后執(zhí)行
 });

 // test cases
});

實(shí)踐

上面簡單介紹了單元測試的用法,下面來動(dòng)手在Vue中進(jìn)行單元測試!

util.js

從Vue官方的demo可以看出,對(duì)于Vue的單元測試我們需要將組件實(shí)例化為一個(gè)Vue實(shí)例,有時(shí)還需要掛載到DOM上。

 const Constructor = Vue.extend(Hello) // 獲得Hello組件實(shí)例
 const vm = new Constructor().$mount() // 將組件掛載到DOM上

以上寫法只是簡單的獲取組件,有時(shí)候我們需要傳遞props屬性、自定義方法等,還有可能我們需要用到第三方UI框架。所以以上寫法非常麻煩。

這里推薦Element的單元測試工具腳本Util.js,它封裝了Vue單元測試中常用的方法。下面demo也是根據(jù)該 Util.js來寫的。
這里簡單注釋了下各方法的用途。

/**
 * 回收 vm,一般在每個(gè)測試腳本測試完成后執(zhí)行回收vm。
 * @param {Object} vm
 */
exports.destroyVM = function (vm) {}

/**
 * 創(chuàng)建一個(gè) Vue 的實(shí)例對(duì)象
 * @param {Object|String} Compo   - 組件配置,可直接傳 template
 * @param {Boolean=false} mounted  - 是否添加到 DOM 上
 * @return {Object} vm
 */
exports.createVue = function (Compo, mounted = false) {}

/**
 * 創(chuàng)建一個(gè)測試組件實(shí)例
 * @param {Object} Compo     - 組件對(duì)象
 * @param {Object} propsData   - props 數(shù)據(jù)
 * @param {Boolean=false} mounted - 是否添加到 DOM 上
 * @return {Object} vm
 */
exports.createTest = function (Compo, propsData = {}, mounted = false) {}

/**
 * 觸發(fā)一個(gè)事件
 * 注: 一般在觸發(fā)事件后使用 vm.$nextTick 方法確定事件觸發(fā)完成。
 * mouseenter, mouseleave, mouseover, keyup, change, click 等
 * @param {Element} elm   - 元素
 * @param {String} name   - 事件名稱
 * @param {*} opts      - 配置項(xiàng)
 */
exports.triggerEvent = function (elm, name, ...opts) {}

/**
 * 觸發(fā) “mouseup” 和 “mousedown” 事件,既觸發(fā)點(diǎn)擊事件。
 * @param {Element} elm   - 元素
 * @param {*} opts     - 配置選項(xiàng)
 */
exports.triggerClick = function (elm, ...opts) {}

示例一

示例一中我們測試了 Hello 組件的各種元素的數(shù)據(jù),學(xué)習(xí)  util.js 的 destroyVM 和 createTest 方法的用法以及如何獲取目標(biāo)元素進(jìn)行測試。獲取DOM元素的方式可查看DOM 對(duì)象教程。

Hello.vue

<template>
 <div class="hello">
  <h2 class="hello-title">{{ msg }}</h2>
  <h3 class="hello-content">{{ content }}</h3>
 </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('測試獲取元素內(nèi)容', () => {
  vm = createTest(Hello, { content: 'Hello World' }, true)
  expect(vm.$el.querySelector('.hello h2').textContent).to.equal('Welcome!')
  expect(vm.$el.querySelector('.hello h3').textContent).to.have.be.equal('Hello World')
 })

 it('測試獲取Vue對(duì)象中數(shù)據(jù)', () => {
  vm = createTest(Hello, { content: 'Hello World' }, true)
  expect(vm.msg).to.equal('Welcome!')
  // Chai的語言鏈?zhǔn)菬o意義的,可以隨便寫。如下:
  expect(vm.content).which.have.to.be.that.equal('Hello World') 
 })

 it('測試獲取DOM中是否存在某個(gè)class', () => {
  vm = createTest(Hello, { content: 'Hello World' }, true)
  expect(vm.$el.classList.contains('hello')).to.be.true
  const title = vm.$el.querySelector('.hello h2')
  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
 })
})

輸出結(jié)果

Hello.vue
  √ 測試獲取元素內(nèi)容
  √ 測試獲取Vue對(duì)象中數(shù)據(jù)
  √ 測試獲取DOM中是否存在某個(gè)class

示例二

示例二中我們使用 createTest 創(chuàng)建測試組件測試點(diǎn)擊事件,用 createVue 創(chuàng)建Vue示例對(duì)象測試組件 Click 的使用。這里主要可以看下到 createVue 方法的使用。

Click.vue

<template>
 <div>
  <span class="init-num">初始值為{{ InitNum }}</span><br>
  <span class="click-num">點(diǎn)擊了{(lán){ ClickNum }}次</span><br>
  <span class="result-num">最終結(jié)果為{{ 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('測試按鈕點(diǎn)擊事件', () => {
  vm = createTest(Click, {
   AddNum: 10,
   InitNum: 11
  }, true)
  let buttonElm = vm.$el.querySelector('button')
  buttonElm.click()
  buttonElm.click()
  buttonElm.click()
  // setTimeout 的原因
  // 在數(shù)據(jù)改變之后,界面的變化會(huì)有一定延時(shí)。不用timeout有時(shí)候會(huì)發(fā)現(xiàn)界面沒有變化
  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('點(diǎn)擊了3次')
   expect(vm.$el.querySelector('.result-num').textContent).to.equal('最終結(jié)果為41')
   done()
  }, 100)
 })

 it('測試創(chuàng)建Vue對(duì)象', () => {
  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()
  })
})

輸出結(jié)果

click.vue
  √ 測試按鈕點(diǎn)擊事件
  √ 測試創(chuàng)建Vue對(duì)象

其他

所有示例代碼都放Github倉庫中便于查看。如果想查看更多好的測試用例,建議配合 Util.js 看一下 Element 的單元測試腳本的寫法,里面有很多測試腳本可以供我們學(xué)習(xí)。作為被廣大Vue用戶使用的UI組件庫,測試腳本肯定也寫很很不錯(cuò)的~甚至可以將這些腳本照抄一遍,相信這會(huì)對(duì)學(xué)習(xí)Vue組件的單元測試有很大幫助。

下面是本人看Element單元測試的筆記,供參考。

Util.js 方法包含了大多數(shù)Vue組件化的測試需求。

vm.$el vm.$nextTick 和 vm.$ref 都是在測試過程中比較常用的一些Vue語法糖。

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

異步斷言,方法參數(shù)需要是 _ 或者 done

大多數(shù)時(shí)候查詢元素通過 querySelector 方法查詢class獲得

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

大多數(shù)情況下查詢是否存在某個(gè)Class通過 classList.contains 方法獲得,查找的結(jié)果為 true 或 false

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

異步測試必須以 done() 方法結(jié)尾。setTimeout 和 vm.$nextTick 是常用的異步測試。

實(shí)現(xiàn)按鈕點(diǎn)擊:通過獲取按鈕元素 btn,執(zhí)行 btn.click() 方法實(shí)現(xiàn)。

由于 Vue 進(jìn)行異步更新DOM 的情況,一些依賴DOM更新結(jié)果的斷言必須在 Vue.nextTick 回調(diào)中進(jìn)行。

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學(xué)習(xí)系列項(xiàng)目地址:https://github.com/violetjack/VueStudyDemos

以上就是本文的全部內(nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持億速云。

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請(qǐng)聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI