Vue-Test-Utils
是 Vue.js
官方的單元測試實用工具庫,它提供了一系列的 API
來使得咱們能夠很便捷的去寫 Vue
應用中的單元測試。javascript
主流的單元測試運行器有不少,好比 Jest
、Mocha
和 Karma
等,這幾個在 Vue-Test-Utils
文檔裏都有對應的教程,這裏咱們只介紹 Vue-Test-Utils + Jest
結合的示例。html
Jest 是一個由 Facebook 開發的測試框架。Vue 對其進行描述:是功能最全的測試運行器。它所需的配置是最少的,默認安裝了 JSDOM,內置斷言且命令行的用戶體驗很是好。不過你須要一個可以將單文件組件導入到測試中的預處理器。咱們已經建立了 vue-jest 預處理器來處理最多見的單文件組件特性,但仍不是 vue-loader 100% 的功能。vue
經過腳手架 vue-cli
來新建項目的時候,若是選擇了 Unit Testing
單元測試且選擇的是 Jest
做爲測試運行器,那麼在項目建立好後,就會自動配置好單元測試須要的環境,直接能用 Vue-Test-Utils
和 Jest
的 API
來寫測試用例了。java
可是新建項目之初沒有選擇單元測試功能,須要後面去添加的話,有兩種方案:node
第一種配置:webpack
直接在項目中添加一個 unit-jest
插件,會自動將須要的依賴安裝配置好。ios
vue add @vue/unit-jest
複製代碼
第二種配置:git
這種配置會麻煩一點,下面是具體的操做步驟。github
安裝 Jest
和 Vue Test Utils
web
npm install --save-dev jest @vue/test-utils
複製代碼
安裝 babel-jest
、 vue-jest
和 7.0.0-bridge.0
版本的 babel-core
npm install --save-dev babel-jest vue-jest babel-core@7.0.0-bridge.0
複製代碼
安裝 jest-serializer-vue
npm install --save-dev jest-serializer-vue
複製代碼
Jest
Jest
的配置能夠在 package.json
裏配置;也能夠新建一個文件 jest.config.js
, 放在項目根目錄便可。這裏我選擇的是配置在 jest.config.js
中:
module.exports = {
moduleFileExtensions: [
'js',
'vue'
],
transform: {
'^.+\\.vue$': '<rootDir>/node_modules/vue-jest',
'^.+\\.js$': '<rootDir>/node_modules/babel-jest'
},
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1'
},
snapshotSerializers: [
'jest-serializer-vue'
],
testMatch: ['**/__tests__/**/*.spec.js'],
transformIgnorePatterns: ['<rootDir>/node_modules/']
}
複製代碼
各配置項說明:
moduleFileExtensions
告訴 Jest
須要匹配的文件後綴transform
匹配到 .vue
文件的時候用 vue-jest
處理, 匹配到 .js
文件的時候用 babel-jest
處理moduleNameMapper
處理 webpack
的別名,好比:將 @
表示 /src
目錄snapshotSerializers
將保存的快照測試結果進行序列化,使得其更美觀testMatch
匹配哪些文件進行測試transformIgnorePatterns
不進行匹配的目錄package.json
寫一個執行測試的命令腳本:
{
"script": {
"test": "jest"
}
}
複製代碼
爲了保證環境的一致性,咱們從建立項目開始一步一步演示操做步驟。
vue-cli
建立一個項目當前我用到的是 3.10.0
版本的 vue-cli
。開始建立項目:
vue create first-vue-jest
複製代碼
選擇 Manually select features
進行手動選擇功能配置:
Vue CLI v3.10.0
┌───────────────────────────┐
│ Update available: 4.0.4 │
└───────────────────────────┘
? Please pick a preset:
VUE-CLI3 (vue-router, node-sass, babel, eslint)
default (babel, eslint)
❯ Manually select features
複製代碼
勾選 Babel
、Unit Testing
:
? Check the features needed for your project:
◉ Babel
◯ TypeScript
◯ Progressive Web App (PWA) Support
◯ Router
◯ Vuex
◯ CSS Pre-processors
◯ Linter / Formatter
◉ Unit Testing
◯ E2E Testing
複製代碼
選擇 Jest
:
? Pick a unit testing solution:
Mocha + Chai
❯ Jest
複製代碼
選擇 In dedicated config files
將各配置信息配置在對應的 config
文件裏:
? Where do you prefer placing config for Babel, PostCSS, ESLint, etc.? (Use arrow keys)
❯ In dedicated config files
In package.json
複製代碼
輸入n,不保存預設:
? Save this as a preset for future projects? (y/N) n
複製代碼
項目建立完成後,部分文件的配置信息以下:
babel.config.js
:
module.exports = {
presets: [
'@vue/cli-plugin-babel/preset'
]
}
複製代碼
jest.config.js
, 這個文件的配置默認是預設插件的,能夠按實際需求改爲上面提到的配置 Jest
裏的配置同樣。
module.exports = {
preset: '@vue/cli-plugin-unit-jest'
}
複製代碼
package.json
:
{
"name": "first-vue-jest",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build",
"test:unit": "vue-cli-service test:unit"
},
"dependencies": {
"core-js": "^3.1.2",
"vue": "^2.6.10"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^4.0.0",
"@vue/cli-plugin-unit-jest": "^4.0.0",
"@vue/cli-service": "^4.0.0",
"@vue/test-utils": "1.0.0-beta.29",
"vue-template-compiler": "^2.6.10"
}
}
複製代碼
用上面的步驟建立的項目完成項目後,咱們能夠在 package.json
的 scripts
項中看到有個 test:unit
,執行它:
cd first-vue-jest
npm run test:unit
複製代碼
而後終端裏會看到輸出結果,PASS
表示測試用例經過了,這個是官方提供單元測試例子。下面咱們來寫點本身的東西。
看上面的原型圖,有這麼幾點明確的需求:
-
號表示,點擊後刪除該項√
號表示,點擊後當前項移動到已完成列表x
號表示,點擊後當前項移動到未完成列表寫頁面以前先把建立項目的時候生成的 HelloWorld.vue
和對應的測試文件 example.spec.js
刪除;同時修改 App.vue
文件,引入 ToDoList
組件:
<template>
<div id="app">
<ToDoList></ToDoList>
</div>
</template>
<script> import ToDoList from './components/ToDoList' export default { components: { ToDoList } } </script>
複製代碼
在 src/compoents
下新建一個文件 ToDoList.vue
,樣式較多就不貼出來了,具體能夠去看本項目源碼:
<template>
<div class="todolist">
<header>
<h5>ToDoList</h5>
<input class="to-do-text" v-model="toDoText" @keyup.enter="enterText" placeholder="輸入計劃要作的事情"/>
</header>
<h4 v-show="toDoList.length > 0">待完成</h4>
<ul class="wait-to-do">
<li v-for="(item, index) in toDoList" :keys="item">
<p>
<i>{{index + 1}}</i>
<input :value="item" @blur="setValue(index, $event)" type="text" />
</p>
<p>
<span class="move" @click="removeToComplete(item, index)">√</span>
<span class="del" @click="deleteWait(index)">-</span>
</p>
</li>
</ul>
<h4 v-show="completedList.length > 0">已完成</h4>
<ul class="has-completed">
<li v-for="(item, index) in completedList" :keys="item">
<p>
<i>{{index + 1}}</i>
<input :value="item" disabled="true" type="text" />
</p>
<p>
<span class="move" @click="removeToWait(item, index)">x</span>
<span class="del" @click="deleteComplete(index)">-</span>
</p>
</li>
</ul>
</div>
</template>
複製代碼
<script>
export default {
data() {
return {
toDoText: '',
toDoList: [],
completedList: []
}
},
methods: {
setValue(index, e) {
this.toDoList.splice(index, 1, e.target.value)
},
removeToComplete(item, index) {
this.completedList.splice(this.completedList.length, 0, item)
this.toDoList.splice(index, 1)
},
removeToWait(item, index) {
this.toDoList.splice(this.toDoList.length, 0, item)
this.completedList.splice(index, 1)
},
enterText() {
if (this.toDoText.trim().length > 0) {
this.toDoList.splice(this.toDoList.length, 0, this.toDoText)
this.toDoText = ''
}
},
deleteWait(index) {
this.toDoList.splice(index, 1)
},
deleteComplete(index) {
this.completedList.splice(index, 1)
}
}
};
</script>
複製代碼
頁面寫完,原型上的需求也大概開發完成,頁面大概長以下樣子:
接下來就是開始編寫單元測試文件了,寫以前咱們先把測試文件目錄修改下爲 __tests__
,同時修改 jest.config.js
爲以下配置,注意其中的 testMatch
已經修改成匹配 __tests__
目錄下的全部 .js
文件了。
module.exports = {
moduleFileExtensions: [
'js',
'vue'
],
transform: {
'^.+\\.vue$': '<rootDir>/node_modules/vue-jest',
'^.+\\.js$': '<rootDir>/node_modules/babel-jest'
},
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1'
},
snapshotSerializers: [
'jest-serializer-vue'
],
testMatch: ['**/__tests__/**/*.spec.js'],
transformIgnorePatterns: ['<rootDir>/node_modules/']
}
複製代碼
在 __tests__/unit/
目錄下新建文件 todolist.spec.js
,咱們約定測試某個 vue
文件,那麼它的單元測試文件習慣命名成 *.spec.js
或 *.test.js
。
import { shallowMount } from '@vue/test-utils'
import ToDoList from '@/components/ToDoList'
describe('test ToDoList', () => {
it('輸入框初始值爲空字符串', () => {
const wrapper = shallowMount(ToDoList)
expect(wrapper.vm.toDoText).toBe('')
})
})
複製代碼
上面這個測試文件簡要說明:
shallowMount
將會建立一個包含被掛載和渲染的 Vue
組件的 Wrapper
,只存根當前組件,不包含子組件。describe(name, fn)
這邊是定義一個測試套件,test ToDoList
是測試套件的名字,fn
是具體的可執行的函數it(name, fn)
是一個測試用例,輸入框初始值爲空字符串
是測試用例的名字,fn
是具體的可執行函數;一個測試套件裏能夠保護多個測試用例。expect
是 Jest
內置的斷言風格,業界還存在別的斷言風格好比 Should
、Assert
等。toBe
是 Jest
提供的斷言方法, 更多的能夠到Jest Expect
查看具體用法。it('待完成列表初始值應該爲空數組', () => {
const wrapper = shallowMount(ToDoList)
expect(wrapper.vm.toDoList.length).toBe(0)
})
it('已完成列表初始值應該爲空數組', () => {
const wrapper = shallowMount(ToDoList)
expect(wrapper.vm.completedList).toEqual([])
})
複製代碼
待完成和已完成列表,竟然是列表,因此存放數據的字段必須是 Array
類型,空列表就是空數組。若是第二個測試用例改爲:
expect(wrapper.vm.completedList).toBe([])
複製代碼
將會報錯,由於 toBe
方法內部是調用 Object.is(value1, value2)
來比較2個值是否相等的,和 ==
或 ===
的判斷邏輯不同。顯然 Object.is([], [])
會返回 false
。
it('輸入框值變化的時候,toDoText應該跟着變化', () => {
const wrapper = shallowMount(ToDoList)
wrapper.find('.to-do-text').setValue('晚上要陪媽媽逛超市')
expect(wrapper.vm.toDoText).toBe('晚上要陪媽媽逛超市')
})
it('輸入框沒有值,敲入回車的時候,無變化', () => {
const wrapper = shallowMount(ToDoList)
const length = wrapper.vm.toDoList.length
const input = wrapper.find('.to-do-text')
input.setValue('')
input.trigger('keyup.enter')
expect(wrapper.vm.toDoList.length).toBe(length)
})
it('輸入框有值,敲入回車的時候,待完成列表將新增一條數據,同時清空輸入框', () => {
const wrapper = shallowMount(ToDoList)
const length = wrapper.vm.toDoList.length
const input = wrapper.find('.to-do-text')
input.setValue('晚上去吃大餐')
input.trigger('keyup.enter')
expect(wrapper.vm.toDoList.length).toBe(length + 1)
expect(wrapper.vm.toDoText).toBe('')
})
複製代碼
setValue
能夠設置一個文本控件的值並更新 v-model
綁定的數據。.to-do-text
是一個 CSS
選擇器;Vue-Test-Utils
提供了 find
方法來經過查找選擇器,來返回一個 Wrapper
;選擇器能夠是 CSS
選擇器、能夠是 Vue
組件也能夠是一個對象,這個對象包含了組件的 name
或 ref
屬性,好比能夠這樣用:wrapper.find({ name: 'my-button' })
wrapper.vm
是一個 Vue
實例,只有 Vue
組件的包裹器纔有 vm
這個屬性;經過 wrapper.vm
能夠訪問全部 Vue
實例的屬性和方法。好比:wrapper.vm.$data
、wrapper.vm.$nextTick()
。trigger
方法能夠用來觸發一個 DOM
事件,這裏觸發的事件都是同步的,因此沒必要將斷言放到 $nextTick()
裏去執行;同時支持傳入一個對象,當捕獲到事件的時候,能夠獲取到傳入對象的屬性。能夠這樣寫:wrapper.trigger('click', {name: "bubuzou.com"})
it('待完成列表支持編輯功能,編輯後更新toDoList數組', () => {
const wrapper = shallowMount(ToDoList)
wrapper.setData({toDoList: ['跑步半小時']})
wrapper.find('.wait-to-do li').find('input').setValue('繞着公園跑3圈')
wrapper.find('.wait-to-do li').find('input').trigger('blur')
expect(wrapper.vm.toDoList[0]).toBe('繞着公園跑3圈')
})
複製代碼
先用 setData
給 toDoList
設置一個初始值,使其渲染出一個列表項;而後找到這個列表項,用 setValue
給其設置值,模擬了編輯;列表項的輸入框是用 :value="item"
綁定的 value
, 因此 setValue
沒法觸發更新;只能經過 trigger
來觸發更新 toDoList
的值。
it('待完成列表點擊刪除,同時更新toDoList數組', () => {
const wrapper = shallowMount(ToDoList)
wrapper.setData({toDoList: ['睡前看一小時書']})
expect(wrapper.vm.toDoList.length).toBe(1)
wrapper.find('.wait-to-do li').find('.del').trigger('click')
expect(wrapper.vm.toDoList.length).toBe(0)
})
it('點擊待完成列表中某項的已完成按鈕,數據對應更新', () => {
const wrapper = shallowMount(ToDoList)
wrapper.setData({toDoList: ['中午餐後吃一個蘋果']})
expect(wrapper.vm.toDoList.length).toBe(1)
expect(wrapper.vm.completedList.length).toBe(0)
wrapper.find('.wait-to-do li').find('.move').trigger('click')
expect(wrapper.vm.toDoList.length).toBe(0)
expect(wrapper.vm.completedList.length).toBe(1)
})
it('點擊已完成列表中某項的未完成按鈕,數據對應更新', () => {
const wrapper = shallowMount(ToDoList)
wrapper.setData({completedList: ['唱了一首歌']})
expect(wrapper.vm.toDoList.length).toBe(0)
expect(wrapper.vm.completedList.length).toBe(1)
wrapper.find('.has-completed li').find('.move').trigger('click')
expect(wrapper.vm.toDoList.length).toBe(1)
expect(wrapper.vm.completedList.length).toBe(0)
})
it('列表序號從1開始遞增', () => {
const wrapper = shallowMount(ToDoList)
wrapper.setData({toDoList: ['早上作做業', '下午去逛街']})
expect(wrapper.vm.toDoList.length).toBe(2)
expect(wrapper.find('.wait-to-do').html()).toMatch('<i>1</i>')
expect(wrapper.find('.wait-to-do').html()).toMatch('<i>2</i>')
})
it('當待完成列表爲空的時候,不顯示待完成字樣', () => {
const wrapper = shallowMount(ToDoList)
wrapper.setData({toDoList: []})
expect(wrapper.find('h4').isVisible()).toBeFalsy()
wrapper.setData({toDoList: ['明天去爬北山']})
expect(wrapper.find('h4').isVisible()).toBeTruthy()
})
複製代碼
一個測試用例中能夠寫多個 expect
以保證斷言的準確性。
最後咱們爲了模擬異步測試,因此加一個需求,即頁面加載的時候會去請求遠程待完成列表的數據。 在項目根目錄新建 __mocks__
目錄,同時新建 axios.js
:
const toToList = {
success: true,
data: ['上午去圖書館看書', '下去出去逛街']
}
export const get = (url) => {
if (url === 'toToList.json') {
return new Promise((resolve, reject) => {
if (toToList.success) {
resolve(toToList)
} else {
reject(new Error())
}
})
}
}
複製代碼
修改 ToDoList.vue
,導入 axios
和增長 mounted
:
<script>
import * as axios from '../../__mocks__/axios'
export default {
mounted () {
axios.get('toToList.json').then(res => {
this.toDoList = res.data
}).catch(err => {
})
},
};
</script>
複製代碼
測試用例編寫爲:
it('當頁面掛載的時候去請求數據,請求成功後應該會返回2條數據', (done) => {
wrapper.vm.$nextTick(() => {
expect(wrapper.vm.toDoList.length).toBe(2)
done()
})
})
複製代碼
對於異步的代碼,寫斷言的時候須要放在 wrapper.vm.$nextTick()
裏,且手動調用 done()
。
測試用例寫了部分,若是咱們看下覆蓋率如何,就須要要配置測試覆蓋率。在 jest.config.js
裏新增配置:
collectCoverage: true,
collectCoverageFrom: ["**/*.{js,vue}", "!**/node_modules/**"],
複製代碼
在 package.json
的 scripts
中新增一條配置:
"test:cov": "vue-cli-service test:unit --coverage"
複製代碼
而後咱們在終端運行: npm run test:cov
,結果以下:
運行測試覆蓋率命名後會在項目根目錄生成 coverage
目錄,瀏覽器打開裏面的 index.html
: