單元測試之jest


       jest是Facebook的一套開源的JavaScript測試框架,它集成了快照測試、斷言、mock以及覆蓋率報告等功能,很全面而且基本不需要太多的配置便可使用Vue-Test-Utils是Vue的官方的單元測試框架,它提供了一系列非常方便的工具,使我們更加輕松的為Vue構建的應用來編寫單元測試。
      這里講的主要是Vue+Jest+Vue-Test-Utils的項目,假設現在你已經使用vue-cli3搭建了一個vue項目:
 
1.安裝jest
npm install --save-dev jest @vue/test-utils
 
//package.json
"scripts": {
    "test": "jest",    
}
 
2.vue-jest
vue-jest是一個 預處理器,如果不安裝vue-jest,jest無法處理.vue
npm install --save-dev vue-jest
 
3.配置jest
在src/test目錄新建jest.conf.js配置文件,目錄如下:
 
我的配置內容如下:
const path = require('path');
 
module.exports = {
  verbose: true,
  testURL: 'http://localhost/',
  rootDir: path.resolve(__dirname, '../../../'),
  moduleFileExtensions: [
    'js',
    'json',
    'vue',
  ],
  testMatch: [ // 匹配測試用例的文件
    '<rootDir>/src/test/unit/specs/*.spec.js',
  ],
  transform: {
    '^.+\\.js$': 'babel-jest',
    '.*\\.(vue)$': 'vue-jest',
  },
  testPathIgnorePatterns: [
    '<rootDir>/test/e2e',
  ],
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1',
  },
  
};
 
4.配置之后就可以開始寫測試用例了
在spec目錄下新建一個ATest.spec.js文件(jest的測試腳本為.spec.js為后綴)
//ATest.vue組件
<template>
    <div>
        <Checkbox class="checkAlls" @on-change="checkboxChange" v-model="checkAll">全選</Checkbox>
    </div>
</template>
<script>
export default {
  name: 'ATest',
  data() {
    return {
      checkAll: false,
    };
  },
  methods: {
    checkboxChange(value) {
      this.$emit('on-change', value);
    },
  },
};
</script>
 
ATest.spec.js測試文件
import { mount, createLocalVue } from '@vue/test-utils';
import ATest from '@/components/ATest.vue';
import iviewUI from 'view-design';
 
const localVue = createLocalVue();
localVue.use(iviewUI);
 
    describe('ATest.vue',()=>{
        const wrapper =mount(ATest, { 
            localVue 
        });
            
        it("事件被正常觸發",()=>{
            const stub = jest.fn();
            wrapper.setMethods({ checkboxChange: stub });
            //觸發自定義事件
            wrapper.find(".checkAlls").vm.$emit("on-change");
            wrapper.setData({checkAll: true});
            expect(wrapper.vm.checkAll).toBe(true);
        })
    })
 
5.vue-test-utils常用的API
  • mount()
     創建一個包含被掛載和渲染的 Vue 組件的 wrapper,它僅僅掛載當前實例
  • shallowMount()
   和  mount 一樣,創建一個包含被掛載和渲染的 Vue 組件的  Wrapper,只掛載一個組件而不渲染其子組件 (即保留它們的存根),這個方法可以保證你關心的組件在渲染時沒有同時將其子組件渲染,避免了             子組件可能帶來的副作用(比如Http請求等)
 
   mount和shallowMount區別的案例解釋
//App.vue
<template>
    <div id="app">
        <Page :messages="messages"></Page>
   </div>
</template>
 
//子組件
<template>
    <div>
        <p v-for="message in messages" :key="message">{{message}}</p>
    </div>
</template>
 
//測試用例App.spec.js
import { mount } from 'vue-test-utils';
import App from '@/App';
describe('App.test.js', () => {
  let wrapper;
  let vm;
  beforeEach(() => {
    wrapper = mount(App);
    vm = wrapper.vm;
    wrapper.setProps({ messages: ['Cat'] })
  });
  // 為App的單元測試增加快照(snapshot):
  it('has the expected html structure', () => {
    expect(vm.$el).toMatchSnapshot()
  })
});
 
執行單元測試后,測試通過,然后Jest會在test/__snapshots__/文件夾下創建一個快照文件App.spec.js.snap
exports[`App.test.js has the expected html structure 1`] = 
` <div id="app" > <div> <p> Cat </p> </div> </div> `;
 
//通過快照我們可以發現,子組件Test1被渲染到App中了。
 
將App.spec.js中的mount方法更改為shallow方法,再次查看快照
exports[`App.test.js has the expected html structure 1`] = 
` <div id="app" > <!----> </div> `;
 
//可以看出來,子組件沒有被渲染
該案例的詳細解釋可以看這篇文章: https://blog.csdn.net/duola8789/article/details/80434962
  • createLocalVue
   返回一個 Vue 的類供你添加組件、混入和安裝插件而不會污染全局的 Vue 類
import { createLocalVue, shallowMount } from '@vue/test-utils'import Foo from './Foo.vue'
const localVue = createLocalVue()const wrapper = shallowMount(Foo, { localVue, mocks: { foo: true }})expect(wrapper.vm.foo).toBe(true)
const freshWrapper = shallowMount(Foo)expect(freshWrapper.vm.foo).toBe(false)
  • 選擇器 (詳細看Vue-Test-Utils官網介紹)
  • $route 和 $router
import { mount} from '@vue/test-utils' 
import Test from '@/components/common/Test.vue';
describe('Test.vue',()=>{
    const wrapper = mount(Test, { 
        mocks: { 
            $route: { path: '/login' }
        } 
     }) 
    it("test", ()=>{
       expect(wrapper.vm.$route.path).toMatch('/login')
    })
})
  • 狀態管理Vuex
import Vuex from 'vuex';
import {mount, createLocalVue} from '@vue/test-utils';
 
const localVue = createLocalVue();
localVue.use(Vuex);
describe('Test.vue',()=>{
    let wrapper;
    let getters;
    let store;
    let action;
    let mutations;
    let option;
    beforeEach(()=>{
               state = {
            name: '張三'
         }
        getters = {
            GET_NAME: ()=> '張三'
        }
        mutations = {
            SET_NAME: jest.fn()
        }
        action = {
            setName: jest.fn()
        }
        store = new Vuex.Store({
            getters,
            mutations,
            action
        })
        option = {
            store,
            localVue
        }
        wrapper = mount(Test, option)
    })
    
    it("測試vuex", ()=>{
        expect(getters.GET_Name()).toEqual('張三');
        wrapper.vm.$store.state.name= "李四";
        expect(wrapper.vm.name).toMatch('李四');
        wrapper.find('.btn').trigger('click')
        expect(actions.setName).toHaveBeenCalled()  
        wrapper.find({ref: 'testComp'}).vm.$emit('on-select');
        expect(mutations.SET_NAME).toBeCalled() 
 })
    
})
  • wrapper
          一個 Wrapper 是一個包括了一個掛載組件或 vnode,以及測試該組件或 vnode 的方法
         屬性      
         setMethods: 設置 Wrapper vm 的方法並強制更新。 通過setMethods方法用mock函數代替真實的方法,然后就可以斷言點擊按鈕后對應的方法有沒有被觸發、觸發幾次、傳入的參數等等
 
6.測試鈎子
  • beforeEach(fn)  在每一個測試之前需要做的事情,比如測試之前將某個數據恢復到初始狀態
  • afterEach(fn)       在每一個測試用例執行結束之后運行
  • beforeAll(fn)          在所有的測試之前需要做什么
  • afterAll(fn)             在測試用例執行結束之后運行
他們的調用順序為: beforeAll =>  beforeEach =>  afterAll =>  afterEach
 
7.模擬接口請求
//Test.vue
mounted() {
    this.getDataList();
 },
 
methods: {
getDataList() {
    this.$api.data.getData().then((res) => {
        this.List= res.data;
    });
}
 
 
import {shallowMount, createLocalVue} from '@vue/test-utils';
const localVue = createLocalVue();
 
//axios請求
jest.mock('../data.js', ()=>({
    getData: () => Promise.resolve({data:{name:'張三'}})
}))
describe('Test.vue',()=>{
    const option;
    let wrapper =shallowMount(Test, option)
    it("異步接口被正常執行", async()=>{
       const getDataList= jest.fn();
        option.methods = {getDataList};
        shallowMount(Test, option);
        await expect(getDataList).toBeCalled();
    })
 
    it('測試異步接口的返回值', () => {
        return localVue.nextTick().then(() => {
          expect(wrapper.vm.List).toEqual( {name:'張三'});
        })
    })
    
})
 
7.常見的報錯
[vue-test-utils]: find did not return .btn, cannot call trigger() on empty Wrapper
//出現該問題的時候,除了要確保你的組件確實存在該類名的情況,還要確保存在v-if的時候是否為true,如果為false,只需要在單測里將其設置為true,該問題便可解決
 "ReferenceError: sessionStorage is not defined"
//出現該問題只需要去模擬本地存儲就可以了,npm提供了一個模擬本地存儲數據的依賴包mock-local-storage,安裝后在單測文件里導入即可
//import 'mock-local-storage';
//包地址:https://www.npmjs.com/package/mock-local-storage

 

 
TypeError: Cannot read property xxxx of undefined
//這個問題的一般解決方法是直接mock數據
wrapper = mount(Test, {
 
          
mocks:{
 
          
    $router:[],  //不然可能會報  Cannot read property 'push' of undefined,
 
          
    $cacheKeys: { TOKEN: 1 }, //  Cannot read property 'TOKEN' of undefined
 
          
}
 
          
})
 
         
 
注:要明白測試的目的,測試關心是我們的代碼有沒有達到我們預期的效果,它並不關心實現的過程,所以不需要太過於糾結變量的取值或者其他的問題
 
8、覆蓋率報告
單元測試有四個指標:
  • %stmts是語句覆蓋率(statement coverage):是否每個語句都執行了?

  • %Branch分支覆蓋率(branch coverage):是否每個if代碼塊都執行了?

  • %Funcs函數覆蓋率(function coverage):是否每個函數都調用了?

  • %Lines行覆蓋率(line coverage):是否每一行都執行了?

   jest提供了生成測試覆蓋率報告的命令
  • npx jest --init 生成配置文件jest.config.js
  • package.json添加上 --coverage 這個參數
//修改package.json
"scripts": {
    "test": "jest --coverage" 
}

npm run test之后會生成coverage文件

 

 然后再網頁打開index.html,就會看到下圖

 

 三種顏色分別代表不同比例的覆蓋率(<50%紅色,50%~80%灰色, ≥80%綠色)

點擊文件名可以查看代碼的執行情況,

旁邊顯示的1x代表執行的次數

 

 

 

                          jest官網:https://jestjs.io/docs/en/getting-started
            vue-jest-utils官網:https://vue-test-utils.vuejs.org/zh/guides/
單元測試的相關參考鏈接:https://alexjover.com/blog/


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM