Vue單元測試探索

做者:江敏熙 貝聊前端開發工程師
本文同時發佈於我的博客html

爲何要單元測試?

項目的現狀

當前我在公司裏負責的項目,能夠分爲兩類:前端

  • 一類是類似度很高的項目,好比管理後臺,這類項目的頁面經過各類公共組件搭建而成。公共組件的複用性很高,因此質量尤其重要。若是開發人員在修改了公共組件以後留下了bug,那麼將會直接下降了整個項目的質量。我但願讓程序去測試這些公共組件,保證每個公共組件是可用的。vue

  • 另外一類是公司的核心項目,這些項目特色是維護週期長,而且會不斷加入新的功能。在項目版本迭代的過程當中,當一些原來經過了測試的舊功能發生了bug,通常只能到了測試階段才能被測試人員發現。我但願由程序去保證部分核心功能的正常運做,當核心功能發生了bug能快速的察覺到,而不是到了測試階段才發現。webpack

爲了解決上面的問題,我嘗試引入單元測試ios

單元測試的做用

  • 下降bug發生概率,快速定位bug,減小重複的手工測試。git

  • 提升代碼質量,爲項目帶來更高的代碼可維護性。github

  • 方便項目的交接工做,測試腳本就是最好的需求描述。web

接下來談談如何進行單元測試。vue-router

搭建測試框架

測試工具一覽

Mocha

image

Mocha(發音"摩卡")誕生於2011年,是如今最流行的JavaScript測試框架之一,在瀏覽器和Node環境均可以使用。chrome

Karma

image

Karma是由Google團隊開發的一個測試工具, 它不是一個測試框架, 只是一個跑測試的驅動. 你能夠經過karma的配置文件集成你喜歡的框架, 斷言庫和瀏覽器.

Vue Test Utils

Vue的官方的單元測試框架,它提供了一系列很是方便的工具,使咱們能夠更輕鬆地爲Vue應用編寫單元測試。主流的 JavaScript 測試運行器有不少,但 Vue Test Utils 都可以支持。它是測試運行器無關的。

Chai斷言庫

image

Chai是一個斷言庫,用於Node和瀏覽器,它能夠與任何JavaScript測試框架相結合

搭建方法:

本文選擇的測試框架由Karma + Mocha + Chai + Vue Test Utils搭配,本身手動配置過程比較繁瑣,在這裏強烈推薦你們使用vue-cli,vue-cli有現成的模板能夠生成項目,執行vue init webpack [項目名],'Pick a test runner'時選擇'Karma + Mocha' 。vue-cli會自動生成Karma + Mocha + Chai的配置,咱們只須要額外安裝Vue Test Utils,執行npm install @vue/test-utils。

image

若是想本身動手配置的同窗,能夠參考這篇文章

配置完成之後,下圖是項目目錄結構:

image

test文件夾下是unit文件夾,裏面放的是單元測試相關的文件。

image

specs裏存放的是測試腳本,這部分是由開發人員編寫的。
coverage文件夾裏存放的是測試報告,打開裏面的index.html能夠直觀地看到測試的代碼覆蓋率。
Karma.conf.js是karma的配置文件。

怎樣寫單元測試

舉個例子

被測試的組件HelloWorld.vue(path:E:\study\demo\src\components)

代碼以下:

<template>
  <div class="hello">
    <h1>Welcome to Your Vue.js App</h1>
  </div>
</template>
複製代碼

測試腳本HelloWorld.spec.js(path:E:\study\demo\test\unit\specs)

代碼以下:

import HelloWorld from '@/components/HelloWorld';
import { mount, createLocalVue, shallowMount } from '@vue/test-utils'

describe('HelloWorld.vue', () => {
  it('should render correct contents', () => {
    const wrapper = shallowMount(HelloWorld);
    let content = wrapper.vm.$el.querySelector('.hello h1').textContent;
    expect(content).to.equal('Welcome to Your Vue.js App');
  });
});
複製代碼

1.測試腳本的寫法

describe是"測試套件"(test suite),表示一組相關的測試。它是一個函數,第一個參數是測試套件的名稱("加法函數的測試"),第二個參數是一個實際執行的函數。

it是"測試用例"(test case),表示一個單獨的測試,是測試的最小單位。它也是一個函數,第一個參數是測試用例的名稱,第二個參數是一個實際執行的函數。

2.斷言庫的用法

上面的測試腳本里面,有一句斷言:

expect(content).to.equal('Welcome to Your Vue.js App');
複製代碼

所謂"斷言",就是判斷源碼的實際執行結果與預期結果是否一致,若是不一致就拋出一個錯誤。上面這句斷言的意思是,變量content應等於'Welcome to Your Vue.js App'。

全部的測試用例(it塊)都應該含有一句或多句的斷言。它是編寫測試用例的關鍵。

3.查看測試結果

最後運行一下npm run unit,來看結果:

image
結果顯示測試經過。

打開coverage下的index.vue查看代碼覆蓋率:

image

由於這是一個剛新建的項目,代碼很是簡單,因此覆蓋率是100%。代碼覆蓋率是一個客觀的數據,不能徹底真實表示項目的測試狀況,可是具備不錯的參考價值。在多人開發的團隊中,覆蓋率能夠做爲一個硬性的標準。

這就是一個簡單的單元測試編寫過程,是否是很簡單呢?你們都動手本身試試吧。

友情提示

1.用createLocalVue安裝插件

咱們在給實際項目寫單元測試的時候,項目代碼會比上面的demo組件複雜不少。若是你要測試的單個組件裏使用了vue-router或者Vuex的話,就要使用createLocalVue。 好比,有這樣一段代碼:

data() {
 return {
     brandId: this.$route.query.id,
 }
}
複製代碼

$route對象須要用createLocalVue注入router才能使用,不然執行測試腳本會出錯。使用createLocalVue解決這個問題,具體代碼:

import { shallowMount, createLocalVue } from '@vue/test-utils'
import VueRouter from 'vue-router'

const localVue = createLocalVue()
localVue.use(VueRouter)
const router = new VueRouter()

shallowMount(Component, {
  localVue,
  router
})
複製代碼

Vuex也是同理,關於createLocalVue詳細用法就不作贅述了,你們能夠去翻閱官方文檔。

2.nextTick怎麼辦

若是你須要在本身的測試文件中使用 nextTick,注意任何在其內部被拋出的錯誤可能都不會被測試運行器捕獲,由於其內部使用了 Promise。關於這個問題有兩個建議:要麼你能夠在測試的一開始將 Vue 的全局錯誤處理器設置爲 done 回調,要麼你能夠在調用 nextTick 時不帶參數讓其做爲一個 Promise 返回:

// 這不會被捕獲
it('will time out', (done) => {
  Vue.nextTick(() => {
    expect(true).toBe(false)
    done()
  })
})

// 接下來的兩項測試都會如預期工做
it('will catch the error using done', (done) => {
  Vue.config.errorHandler = done
  Vue.nextTick(() => {
    expect(true).toBe(false)
    done()
  })
})

it('will catch the error using a promise', () => {
  return Vue.nextTick()
    .then(function () {
      expect(true).toBe(false)
    })
})
複製代碼

在下面的項目實戰中,有使用到nextTick的例子,你們能夠當作參考。

3. 修改默認測試瀏覽器

測試在配置文件karma.conf裏,browsers默認是'PhantomJS'.

module.exports = function karmaConfig (config) {
  config.set({
    // browsers: ['PhantomJS'],
    browsers: ['Chrome'],
複製代碼

但我在使用過程當中發現PhantomJS環境的warning和error提示和平時在瀏覽器chrome看到的提示不太同樣,有點難懂,如圖:

Chrome:

image
PhantomJS:

image

browsers設置爲'Chrome',獲得的報錯提示和真實Chrome瀏覽器上一致,而且可使用console.log(),調試起來和真實開發的體驗同樣。惟一缺點是每次執行npm run unit都會彈出一個Chrome瀏覽器,PhantomJS則不會,推薦你們調試測試腳本時候使用Chrome,等腳本都跑通了不須要調試的時候能夠換回PhantomJS。

4. 加上--auto-watch

默認下auto-watch是關閉的,每次修改了測試腳本,或者修改了項目代碼以後都須要手動執行一次命令才能啓動測試,很是麻煩。咱們能夠加上--auto-watch,這樣在開發的過程當中,若是某個功能沒有經過測試用例,開發人員能夠馬上發現並修復。

"unit": "cross-env BABEL_ENV=test karma start test/unit/karma.conf.js --auto-watch",
複製代碼

項目實戰

實例1

場景:頁面上有一個textarea輸入框和提交按鈕,點擊按鈕發送請求。要求點擊提交後前端先校驗一下內容是否符合json格式,若是不符合則提示不能提交。

測試的目標:校驗程序

測試用例:經過條件覆蓋,輸入數字,字符串,錯誤的json字符串,'null',正確的json字符串去驗證全部的狀況是否正常執行,指望只有最後一種狀況纔是返回結果纔是經過的,其餘都是不經過。

// form-setting.vue測試校驗功能
describe('form-setting.vue測試校驗功能', () => {
	const wrapper = shallowMount(formSetting, {
		localVue
	});

	let vm = wrapper.vm;

	it('test form填入數字是否會不經過', () => {
		vm.appType = 'ios'; // 選擇系統ios
		vm.ios.schemeInfo = 1; // 輸入數字
		expect(vm.isValid()).to.equal(false);
	});

	it('test form填入字符串格式是否會不經過', () => {
		vm.appType = 'ios';
		vm.ios.schemeInfo = '1'; // 輸入字符串
		expect(vm.isValid()).to.equal(false);
	});

	it('test form填入錯誤json格式是否會不經過', () => {
		vm.appType = 'ios';
		vm.ios.schemeInfo = '{a:{a:}}'; // 輸入非法的相似json格式的字符串
		expect(vm.isValid()).to.equal(false);
	});

	it('test form填入空對象是否會不經過', () => {
		vm.appType = 'ios';
		vm.ios.schemeInfo = 'null'; // 輸入null對象字符串
		expect(vm.isValid()).to.equal(false);
	});

	it('test form填入正確JSON格式是否會經過', () => {
		vm.appType = 'ios';
		vm.ios.schemeInfo = '{"a": 111}'; // 輸入正確的json字符串
		expect(vm.isValid()).to.equal(true);
	});
});
複製代碼
實例2

場景:團隊開發了一個校驗插件,其做用是校驗輸入框是否知足相應規則,若不知足在輸入框下會出現一個提示錯誤的dom節點。

測試用例:經過列舉全部的輸入操做,而後判斷是否存在類名爲.error的錯誤提示節點。

在完成輸入操做後,若是內容不經過校驗,頁面會生成錯誤提示的dom節點。這個過程是異步的,因此用到了nextTick。具體的用法是

return Vue.nextTick().then(() => {
    ...斷言
}
複製代碼

關於這塊詳細的解釋,Vue Test Utils有相關篇幅

import { mount, createLocalVue } from '@vue/test-utils'
import ValidateDemo from '@/components/validate-demo'
import validate from '@/directive/validate/1.0/validate'
import Vue from 'Vue'
const localVue = createLocalVue() // 建立一個Vue實例
localVue.use(validate) // 掛載校驗插件
describe('測試validate-demo.vue', () => {
  it('沒發生輸入操做,[不顯示error]', () => {
    const wrapper = mount(ValidateDemo, {
      localVue
    })
    return Vue.nextTick().then(() => {
      expect(wrapper.find('.error').exists()).to.equal(false)
    })
  })
  it('聚焦輸入框而後失去焦點,[顯示error]', () => {
    const wrapper = mount(ValidateDemo, {
      localVue
    })
    let input = wrapper.find('input')
    input.trigger('focus') // 聚焦
    input.trigger('blur') // 失去焦點

    return Vue.nextTick().then(() => {
      expect(wrapper.find('.error').exists()).to.equal(true)
    })
  })

  it('發生輸入操做,而後清空,[顯示error]', () => {
    const wrapper = mount(ValidateDemo, {
      localVue
    })
    let vm = wrapper.vm
    let input = wrapper.find('input')
    input.trigger('focus')
    vm.name = '不爲空'
    vm.name = '' // 清空
    input.trigger('blur')
    return Vue.nextTick().then(() => {
      expect(wrapper.find('.error').exists()).to.equal(true)
    })
  })

  it('輸入內容後,[不顯示error]', () => {
    const wrapper = mount(ValidateDemo, {
      localVue
    })
    let vm = wrapper.vm
    vm.name = '不爲空' // 輸入內容
    return Vue.nextTick().then(() => {
      expect(wrapper.find('.error').exists()).to.equal(false)
    })
  })
})

複製代碼

單元測試的侷限性

單元測試有許多優勢,但不表明它就必定適合每一個項目,在我看來它會有如下侷限性:

1.額外的時間花費

即便你願意花費開發的幾分之一的時間去寫單元測試,可是一旦功能有變動,就意味着測試邏輯也須要調整。對於一些常常變動的功能來講,這會致使很大的單元測試維護量。 因此咱們要權衡好當中的利弊,能夠考慮只針對穩定的功能(好比一些公用組件)和核心流程編寫單元測試。

2.並不是所有代碼都能單元測試

若是項目裏充斥着顆粒度低,方法間互相耦合的代碼,你會發現沒法進行單元測試。由於單元測試旨在從代碼粒度上實現對應用質量的把握。面對這樣的狀況,要麼重構已有代碼,要麼放棄單元測試尋求其餘測試方法,好比人工測試,e2e測試。

雖然這算是單元測試的一個缺點,但我認爲同時也是優勢,習慣編寫單元測試能夠促使工程師提升代碼的顆粒度,思惟更加縝密。

3.沒法保證一整個流程的運做

前端是一個很是複雜的測試環境,由於每一個瀏覽器都有差別,須要的數據又依賴於後端。單元測試只能對功能每個單元進行測試,對於一些依賴api的數據通常只能mock,沒法真正的模擬用戶實際的使用場景。對於這種狀況,建議採用其餘測試方法,好比人工測試、e2e測試。

總結

經過此次對單元測試的探索,我以爲作單元測試最大的阻力是——時間

手工測試最大的優點在於:當一個功能代碼寫好之後,只須要手動刷新瀏覽器去實際操做一下,便能判斷程序是否正確。若是爲此去編寫單元測試則會花費額外的開發時間。

但人不是機器,不管多麼簡單的事都有可能出錯。咱們爲系統加入了新功能的以後,通常不會去手動測試之前的舊功能。由於這耗費時間而又無趣,而且咱們總會認爲本身寫的代碼是不會影響舊功能的。

然而咱們能夠換個角度去想,若是在開發舊功能的時候寫好了相應的單元測試,那麼每次進入測試階段以前,就能夠用測試腳本把舊功能都跑一遍。這樣既節省了測試舊功能的時間,本身也能夠問心無愧:不管怎麼樣,我都能確保我寫的代碼是經過測試的。

最後,感謝你們的閱讀,本文是我關於對一個Vue項目作的比較淺顯的單元測試的探索,屬於拋磚引玉,若是有什麼不合理的地方或建議,歡迎你們來指正!

相關文章
相關標籤/搜索