Vue單元測試探索

貝聊科技發表於2018-06-25

作者:江敏熙 貝聊前端開發工程師
本文同時釋出於個人部落格

為什麼要單元測試?

專案的現狀

當前我在公司裡負責的專案,可以分為兩類:

  • 一類是相似度很高的專案,比如管理後臺,這類專案的頁面通過各種公共元件搭建而成。公共元件的複用性很高,所以質量尤為重要。如果開發人員在修改了公共元件之後留下了bug,那麼將會直接降低了整個專案的質量。我希望讓程式去測試這些公共元件,保證每一個公共元件是可用的。

  • 另一類是公司的核心專案,這些專案特點是維護週期長,並且會不斷加入新的功能。在專案版本迭代的過程中,當一些原來通過了測試的舊功能發生了bug,一般只能到了測試階段才能被測試人員發現。我希望由程式去保證部分核心功能的正常運作,當核心功能發生了bug能快速的察覺到,而不是到了測試階段才發現。

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

單元測試的作用

  • 降低bug發生機率,快速定位bug,減少重複的手工測試。

  • 提高程式碼質量,為專案帶來更高的程式碼可維護性。

  • 方便專案的交接工作,測試指令碼就是最好的需求描述。

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

搭建測試框架

測試工具一覽

Mocha

image

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

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專案做的比較淺顯的單元測試的探索,屬於拋磚引玉,如果有什麼不合理的地方或建議,歡迎大家來指正!

相關文章