Vue-Test-Utils + Jest 單元測試入門與實踐

介紹

Vue-Test-UtilsVue.js 官方的單元測試實用工具庫,它提供了一系列的 API 來使得咱們能夠很便捷的去寫 Vue 應用中的單元測試。javascript

主流的單元測試運行器有不少,好比 JestMochaKarma 等,這幾個在 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-UtilsJestAPI 來寫測試用例了。java

可是新建項目之初沒有選擇單元測試功能,須要後面去添加的話,有兩種方案:node

第一種配置:webpack

直接在項目中添加一個 unit-jest 插件,會自動將須要的依賴安裝配置好。ios

vue add @vue/unit-jest
複製代碼

第二種配置:git

這種配置會麻煩一點,下面是具體的操做步驟。github

安裝依賴

  • 安裝 JestVue Test Utilsweb

    npm install --save-dev jest @vue/test-utils
    複製代碼
  • 安裝 babel-jestvue-jest7.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
複製代碼

勾選 BabelUnit 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.jsonscripts 項中看到有個 test:unit ,執行它:

cd first-vue-jest
npm run test:unit
複製代碼

而後終端裏會看到輸出結果,PASS 表示測試用例經過了,這個是官方提供單元測試例子。下面咱們來寫點本身的東西。

first-vue-jest-result

實現一個ToDoList

to-do-list

看上面的原型圖,有這麼幾點明確的需求:

  • 在頭部右側輸入框輸入要作的事情,敲回車後,內容跑到待完成列表裏,同時清空輸入框
  • 輸入框爲空的時候敲回車,不作任何變化
  • 待完成列表支持編輯功能,已完成列表不能進行編輯
  • 每一個列表項的右側都有刪除按鈕,用 - 號表示,點擊後刪除該項
  • 待完成列表有標記爲已完成的按鈕,用 號表示,點擊後當前項移動到已完成列表
  • 已完成列表有標記爲未完成的按鈕,用 x 號表示,點擊後當前項移動到未完成列表
  • 列表序號從1開始遞增
  • 當待完成列表爲空的時候,不顯示待完成字樣
  • 當已完成列表爲空的時候,不顯示已完成字樣

先把上面的頁面寫好

寫頁面以前先把建立項目的時候生成的 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 是具體的可執行函數;一個測試套件裏能夠保護多個測試用例。
  • expectJest 內置的斷言風格,業界還存在別的斷言風格好比 ShouldAssert 等。
  • toBeJest 提供的斷言方法, 更多的能夠到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 組件也能夠是一個對象,這個對象包含了組件的 nameref 屬性,好比能夠這樣用:wrapper.find({ name: 'my-button' })
  • wrapper.vm 是一個 Vue 實例,只有 Vue 組件的包裹器纔有 vm 這個屬性;經過 wrapper.vm 能夠訪問全部 Vue 實例的屬性和方法。好比:wrapper.vm.$datawrapper.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圈')
})
複製代碼

先用 setDatatoDoList 設置一個初始值,使其渲染出一個列表項;而後找到這個列表項,用 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.jsonscripts 中新增一條配置:

"test:cov": "vue-cli-service test:unit --coverage"
複製代碼

而後咱們在終端運行: npm run test:cov,結果以下:

test:cov1

運行測試覆蓋率命名後會在項目根目錄生成 coverage 目錄,瀏覽器打開裏面的 index.html

test:cov2
相關文章
相關標籤/搜索