Unit Testing(單元測試)--Jest
一個完整的測試程序通常由幾種不同的測試組合而成,比如end to end(E2E)測試,有時還包括整體測試、簡要測試和單元測試。這里要介紹的是Vue中的單元測試,使用流行的Jest JavaScript測試庫來運行我們的測試。
1.測試的目的
排除故障
每個應用的開發中,多少會出現一些意料之外的bug。通過測試應用程序,可以幫助我們大大減少此類問題,並增強應用程序的邏輯性。
保證團隊成員的邏輯統一
如果您是團隊的新成員,並且對應用程序還不熟悉,那么一組測試就好像是有經驗的開發人員監視你編寫代碼,確保您處於代碼應該執行的正確路線之內。通過這些測試,您可以確信在添加新功能或更改現有代碼時不會破壞任何東西。
可以提高質量代碼
當您在編寫vue組件時,由於考慮到測試,最好的方案將是創建獨立的、更可重用的組件。如果您開始為您的組件編寫測試,並且您注意到這些組件不容易測試,那么您可能會重構您的組件,最終起到改進它們的效果。
起到很好的說明文檔作用
正如在第一點中提到的,測試的另一個結果是,它最終可以為您的開發團隊生成良好的文檔。當某人對代碼庫還不熟悉時,他們可以查看測試以獲得指導,這可以提供關於組件應該如何工作的意圖的洞察,並為可能要測試的邊緣部分提供線索。
1.我們需要測試什么
在測試的過程中,很容易照成過分的測試一些不需要的東西,這會不必要地減慢開發時間。那么我們在Vue.js應用程序中測試什么呢?答案其實很簡單:組件。由於Vue應用程序是一個個組件相互組合而成的,我們需要測試它們各自的行為,以確保它們正常工作。
組件原理及分解
我們可以先分解組件,看看組件之間是怎么協調工作的。
一般來說,組件需要遵照我們定義的邏輯來工作,這里有一個輸入和輸出的概念。組件會接收一些靜態或動態的輸入值,然后依照邏輯輸出一些值或者是dom元素。
正常來說,組件的輸入輸出有以下這些:
1// 輸入值
2
3// 靜態接收數據
4Component Data
5// 動態接收數據
6Component Props
7// 用戶互交,例如:一個用戶單擊的按鈕
8User Interaction
9// 生命周期邏輯,例如:mounted(), created()
10Lifecycle Methods
11// 組件狀態值
12Vuex Store
13// 路由參數
14Route Params
15
16// 輸出值
17
18// 輸出的dom
19What is rendered to the DOM
20// 外部調用的函數
21External function calls
22// 組件觸發的事件
23Events emitted by the component
24// 路由的變更
25Route Changes
26// 組件狀態值得更新
27Updates to the Vuex Store
28// 對子組件的改變
29Connection with children
30
我們可以識別組件的輸入和輸出,挑選我們應該測試的內容,從而避開測試組件的內部業務邏輯。換句話說,我們不應該因為擔心每一行代碼如何工作而陷入困境。
這看起來可能有悖常理,但是單元測試的目標純粹是確保組件產生預期的結果。我們在這里不關心它是如何得出這個結果的。后期我們甚至可能會改變我們的邏輯方式,所以我們不希望測試如何實現這些邏輯。這些邏輯不是測試的工作。就單元測試而言,我們只要確保組件的輸出正常就行了。
要測試的部分
1<template>
2 <div>
3 <button v-show="loggedIn">Logout</button>
4 </div>
5</template>
6
7<script>
8export default {
9 data() {
10 return {
11 loggedIn: false
12 }
13 }
14}
15</script>
在這個例子中,我們有一個組件,如果loggedIn屬性為true,它將顯示一個logout按鈕。為了弄清楚我們應該測試這個組件的哪個部分,我們的第一步是確定組件的輸入和輸出。
1input
2
3// 靜態數據
4data => loggedIn
5// 這個數據屬性決定按鈕是否顯示,所以這是一個我們應該測試的輸入
6
7output
8
9Dom輸出(button)
10根據輸入(loggedIn),我們的按鈕應該在什么時候顯示在DOM中
對於更復雜的組件,將有更多的方面需要測試,但同樣的方法也適用。
不測試的部分
對你應該測試的東西知道大概當然是必要的,知道你不應該測試的東西也是有幫助的。如下:

不測試組件邏輯的詳情(implementation details)
當單元測試時,我們不需要為某些邏輯是如何工作而煩惱,只要它們確實工作就行了。我們不在乎這里的內部結構。我們只關心組件產生了我們期望的輸出。
不要測試框架本身
開發人員經常試圖測試太多,包括框架本身的內部工作。但是框架作者已經建立了這樣的測試。
例如,props中的數據類型,如果我們嘗試傳入錯誤數據,Vue將拋出錯誤。我們不需要浪費時間測試Vue.js框架。這包括不要對Vue路由器和Vuex進行不必要的測試。
不測試第三方庫
如果您使用的第三方庫是高質量的,那么它們已經有了自己的測試。我們不需要測試它們的內部結構。例如,我們不需要測試Axios是如何工作的。Axios隊為我們做了這件事。
2.編寫單元測試
當我們使用Vue CLI創建應用項目時:
- 首先需要選擇
Manually select features
自定義一些項目配置- 然后在選擇項目依賴時,勾選
Unit Testing
選項- 最后在選擇測試方案時。選擇
Jest
- vue/cli項目可以這樣安裝
1vue add @vue/unit-jest
2
3✔ Successfully installed plugin: @vue/cli-plugin-unit-jest
4// 如果安裝的jest運行報錯可能是版本問題,可以嘗試npm update
這樣創建的項目中就為我們安裝好了依賴。
打開項目后,讓我們從查看package.json開始,在這里我們將看到為我們安裝了Jest和vue測試實用程序。
1// package.json
2"devDependencies": {
3 "@vue/cli-plugin-unit-jest": "^3.11.0",
4 "@vue/test-utils": "1.0.0-beta.29"
5}
Jest是一個JavaScript測試框架,它專注於簡化單元測試。Jest將為我們運行單元測試,並在測試通過或失敗時向我們報告。
API list:
1afterAll(fn, timeout)
2afterEach(fn, timeout)
3beforeAll(fn, timeout)
4beforeEach(fn, timeout)
5describe(name, fn)
6describe.each(table)(name, fn, timeout)
7describe.only(name, fn)
8describe.only.each(table)(name, fn)
9describe.skip(name, fn)
10describe.skip.each(table)(name, fn)
11test(name, fn, timeout)
12test.each(table)(name, fn, timeout)
13test.only(name, fn, timeout)
14test.only.each(table)(name, fn)
15test.skip(name, fn)
16test.skip.each(table)(name, fn)
17test.todo(name)

@vue/test-utils
是Vue.js的官方單元測試實用程序庫。它使我們能夠在測試中渲染組件,然后對這些渲染的組件執行各種操作。這使得我們可以測試組件的運行結果。
如何運行Jest測試,我們來查看
package.json
中的運行腳本
1"scripts": {
2 ...
3 "test:unit": "vue-cli-service test:unit"
4}
5// 在我們編寫好測試代碼后只要運行:
6npm run test:unit
7// 或者在CLI中運行測試就可以運行測試代碼
8// 這個命令的意思就是查看tests/unit目錄,並運行目錄下名為[componentsName].spec.js的文件。
查看項目根目錄下的tests/unit文件夾,有一個初始化的Example.spec.js文件。spec是specification的縮寫,即詳述組件運行規則的js文件
- ## demo1(測試一個方法)
創建函數文件demo1.js
1function sum(a, b) {
2 return a + b;
3}
4module.exports = sum;
在tests/unit/中創建demo1.spec.js
1// 導入要測試的文件
2const sum = require('./sum')
3
4// 使用test(name, fn, timeout)創建一個測試
5test('adds 1 + 2 to equal 3', () => {
6 expect(sum(1, 2)).toBe(3)
7})
8
9// 其中,name是對測試的描述,測試結果會打印
10// fn是要執行的測試
11// expect()方法中描述的是測試的結果
12// toBe()是測試預期的值
運行這個單元測試
1npm run test:unit
2
3PASS tests/unit/demo1.spec.js
4✓ adds 1 + 2 to equal 3 (5ms)
5// PASS表示測試通過,函數運行正常
6// 下面列出每一項text(),[test.name (測試使用的時間)]
- ## demo2(測試一個組件)
創建組件demo2.vue
1<template>
2 <div>
3 <button v-show="loggedIn">Logout</button>
4 </div>
5</template>
6<script>
7export default {
8 data() {
9 return {
10 loggedIn: false
11 }
12 }
13}
14</script>
關於這個組件,前面我們已經介紹過了,要測試的點如下:
- 如果用戶未登錄,則不顯示“注銷”按鈕
- 如果用戶已登錄,則顯示“注銷”按鈕
在tests/unit/中創建demo2.spec.js
1// 導入要測試的組件
2import demo2 from '@/components/demo2'
3
4// 因為我們要測試的是組件,所以我們在測試時需要加載這個組件
5// `@vue/test-utils`為我們提供了這項功能
6import { mount } from '@vue/test-utils'
7// 使用describe(name, fn)創建一個測試組
8// 當我們有多個測試時,用這種方式組織它們比較具有邏輯性
9
10describe('ComponentsDemo2', () => {
11
12 // 其中寫所有組件內需要的測試
13 test('if user is not logged in, do not show logout button', () => {
14 // 安裝組件
15 const wrapper = mount(AppHeader)
16
17 // 默認登錄狀態是false,則找到組件內的btn,查看其可見性為false
18 expect(wrapper.find('button').isVisible()).toBe(false)
19 })
20 test('if a user is logged in, show logout button', () => {
21 // 安裝組件
22 const wrapper = mount(AppHeader)
23
24 // 要測試登錄狀態是‘已登錄’,則先設置其狀態值
25 wrapper.setData({ loggedIn: true })
26
27 // 這時找到組件內的btn,查看其可見性為true
28 expect(wrapper.find('button').isVisible()).toBe(true)
29 })
30})
提示:
您可能還會看到使用it()
的測試塊,它是test()
的別名。@vue/test-utils
,還提供shallowMount()
方法。如果組件有子組件,shallowMount()
將返回該組件的本身,而不是完全渲染的詳細組件單元測試的焦點是單一的組件,多數時候我們會忽略其子組件。
運行這個單元測試
1npm run test:unit
2
3PASS tests/unit/demo2.spec.js
4ComponentsDemo2
5 ✓ if user is not logged in, do not show logout button (20ms)
6 ✓ if a user is logged in, show logout button (30ms)
7
8Test Suites: 1 passed, 1 total
9Tests: 2 passed, 2 total
10Snapshots: 0 total
11Time: 2.653s
12Ran all test suites.
步驟總結如下圖:

- 創建一個測試組:describe()
- 開始一個測試:test()
- 安裝渲染組件:mount()
- 必要的時候設置組件參數:setData()
- 調試預期的組件行為及數據正確與否:expect()
長按二維碼關注公眾號
