1. 程式人生 > 其它 >vue 陣列 find_前端單元測試--vue元件測試

vue 陣列 find_前端單元測試--vue元件測試

技術標籤:vue 陣列 find

所謂千呼萬喚始(屎)出來,繼前作Jest基本語法後,憋了許久時間終於開始著手開始寫元件測試了。

一、安裝vue單元測試模組

1.1、新專案

vue-cli3本身整合有單元測試模組,對於新建立的專案來說,安裝單元測試模組非常簡單。只需要在建立專案的時候選擇所需要的測試工具即可

d908ca348cd7c6d70cfc9da25816c729.png

專案建立成功後有一個test資料夾,裡面有一個example.spec.js的簡單測試用例,執行npm run test:unit,即可對HelloWorld元件進行測試。如果使用的IDE是Vs code的話,則可以安裝Jest外掛,這樣就可無需執行test指令就開始跑單測。

1.2、老專案

如果是vue-cli3的專案的話可以執行vue add unit-jest指令,就會往我們的專案中安裝@vue/cli-plugin-unit-jest單測模組,並新增jest.config.js配置檔案,可對其進行簡單的配置

module.exports = {
  preset: '@vue/cli-plugin-unit-jest',
  moduleFileExtensions: ['js', 'vue'], // jset匹配的檔案字尾為js和vue
  transform: {
    '^.+.vue$': '<rootDir>/node_modules/vue-jest', // vue檔案用vue-jest 處理
    '^.+.js$': '<rootDir>/node_modules/babel-jest' // js檔案用babel-jest
  },
  moduleNameMapper: { // 處理webpack配置的別名
    '^@/(.*)$': '<rootDir>/src/$1'// @表示src資料夾
  },
  snapshotSerializers: ['jest-serializer-vue'], // 序列化測試結果,使其更美觀
  testMatch: ['**/tests/**/*.spec.js'], // 單測檔案地址
  transformIgnorePatterns: ['<rootDir>/node_modules/'], // 不進行匹配的目錄
  collectCoverage: true, // 開啟單測覆蓋率
  collectCoverageFrom: ['**/*.{js,vue}', '!**/node_modules/**'] // 確定哪些需要檔案需要生成測試報告
}

如果還需進行進一步的配置的話,可參考官方的配置文件。

二、vue元件測試

2.1、元件的掛載

由於前端元件化,使得UI測試變得容易很多。每個元件都可以被簡化為類似於UI=fn(data)的表示式,這個表示式是一個描述UI是什麼樣的虛擬DOM,給這個表示式輸入一些引數,就會得到UI描述的輸出。

但是經過這樣的抽象後還是存在一個問題,由於DOM是一個樹形的結構。越處於上層的元件,其複雜度必然會隨之提高。對於最底層的子元件來說,我們可以很容易得將其進行渲染並測試其邏輯的正確與否,但對於較上層的父元件來說,通常來說就需要對其所包含的所有子元件都進行預先渲染,甚至於最上面的元件需要渲染出整個 UI 頁面的真實 DOM 節點才能對其進行測試,這顯然是不可取的。

在vue-unit-test中就提供了shallowMountmount 兩個方法來實現元件的掛載,其中shallowMount就可以解決這個問題,它只渲染元件本身,但會保留子元件在元件中的存根。

區別這兩種方法的目的在於,當我們只想對某個孤立的元件進行測試的時候,一方面可以避免其子元件的影響,另一方面對於包含許多子元件的元件來說,完全渲染子元件會導致元件的渲染樹過大,這可能會影響到我們的測試速度

在元件掛載後,我們可以通過wrapper.vm訪問到元件的例項,通過wrapper.vm進而可以訪問到元件所有的props、data和methods等等。

import { shallowMount, mount } from '@vue/test-utils'
import HelloWorld from '@/components/HelloWorld.vue'

describe('HelloWorld.vue', () => {
  it('test shallowMount', () => {
    const wrapper = shallowMount(HelloWorld)
    // 判斷元件是否掛載
    expect(wrapper.exists()).toBe(true)
    
    // 訪問vm例項
    console.log(wrapper.vm)
  })
  it('test mount', () => {
    const wrapper = mount(HelloWorld)
    // 判斷元件是否掛載
    expect(wrapper.exists()).toBe(true)
  })
})

2.2、DOM測試

在元件掛載後,無論哪種渲染方式所返回的 wrapper 都有一個.find()方法,它接受一個 selector 引數,然後返回一個對應的 wrapper 物件。而.findAll()則會返回一個型別相同的 wrapper 物件陣列,裡面包含了所有符合條件的子元件。與jquery中的用法類似selector 可以是CSS 選擇器,除此之外也可以是 Vue 元件 或是一個 option 物件,以便於在 wrapper 物件中可以輕鬆地指定想要查詢的節點。

除此之外,返回的包裹器內還有attributes、classes、element.style、text、html等屬性用於驗證。依然以以上HelloWorld.vue元件為例,如果我們要測試span標籤是否有.item樣式,是否有id,可以進行如下測試:

import { shallowMount } from '@vue/test-utils'
import HelloWorld from '@/components/HelloWorld.vue'

describe('HelloWorld.vue', () => {
  it('test attribute and class', () => {
    const wrapper = shallowMount(HelloWorld)
    // 查詢第一個span標籤
    const dom = wrapper.find('span')
    expect(dom.classes()).toContain('item')
    expect(dom.attributes().id).toBeFalsy()
  })
})

通過find方法查詢 DOM 元素之後,還可以通過trigger方法在元件上模擬觸發某個 DOM 事件,比如 Click,Change 等等。如下例子所示,有一個點選後按鈕計數的元件

<template>
  <div class="container">
    <button id="testClick" @click="changeText">{{btnText}}</button>
  </div>
</template>

<script>
export default {
  data () {
    return {
      btnText: 1,
    }
  },
  methods: {
    changeText () {
      this.btnText++
    }
  }
}
</script>

那麼我們可以在以上元件的基礎上撰寫測試click原生事件的單元測試:

import { shallowMount } from '@vue/test-utils'
import HelloWorld from '@/components/HelloWorld.vue'

describe('HelloWorld.vue', () => {
  it('test trigger click', () => {
    const wrapper = shallowMount(HelloWorld)
    const dom = wrapper.find('#testClick')
    expect(wrapper.vm.btnText).toBe(1)
    dom.trigger('click')
    expect(wrapper.vm.btnText).toBe(2)
    expect(dom.text()).toContain(2)
  })
})

但是此時執行測試用例後發現,expect(dom.text()).toContain(2)用例報錯了,接受到的值為1。這是由於Vue 會非同步的將未生效的 DOM 更新批量應用,以避免因資料反覆突變而導致的無謂的重新渲染。我們要用nextTick做一下延時,來等待 Vue 把實際的 DOM 更新做完。

describe('HelloWorld.vue', () => {
  it('test trigger click', async () => {
    const wrapper = shallowMount(HelloWorld)
    // 查詢第一個span標籤
    const dom = wrapper.find('#testClick')
    expect(wrapper.vm.btnText).toBe(1)
    dom.trigger('click')
    expect(wrapper.vm.btnText).toBe(2)
    await wrapper.vm.$nextTick()
    expect(dom.text()).toBe('2')
  })
})

2.3、元件間值的傳遞測試

vue的元件免不了值得元件間通訊,如props,emit以及slot等。在掛載後的wrapper中也提供了相應的api來進行測試。在測試元件間的變化時,最直觀的測試方法就是,就是在父元件進行相關的操作,然後在測試子元件是否進行了對應的變化。但是這顯然違反了單元測試的理念,我們只關心元件的輸入輸出,不關心元件間的聯動。比如有如下的一個元件

<template>
  <div class="container">
   <span>{{name}}</span>
    <span @click="change">{{age}}</span>
  </div>
</template>

<script>
export default {
  props: ['name', 'age'],
  data () {
    return {
      myAge: 1,
    }
  },
  methods: {
    change () {
      this.$emit('change-age', this.myAge)
    }
  }
}
</script>

該元件接收由父元件傳遞的name,和age兩個引數。點選age欄位時向父元件傳遞change-age時間將myAge傳遞給父元件。

import { shallowMount } from '@vue/test-utils'
import HelloWorld from '@/components/HelloWorld.vue'

describe('HelloWorld.vue', () => {
  it('test props', () => {
    const wrapper = shallowMount(HelloWorld, {
      propsData: {
        name: 'keng',
        age: '100'
      }
    })
    expect(wrapper.props().name).toBe('keng')
    expect(wrapper.props('age')).toBe('100')
  })
  it('test emit', () => {
    const wrapper = shallowMount(HelloWorld)
    const ageDom = wrapper.find('#age')
    ageDom.trigger('click')
    console.log(wrapper.emitted())
    expect((wrapper.emitted('change-age')[0])).toEqual([1])
  })
})

在測試props時我們在掛載時利用propsData給props賦值,利用wrapper.props測試是否可以得到目標值,wrapper.props().name等價於wrapper.props('name')在對emit測試時模擬事件觸發,測試目標事件是否同時觸發,同時值是否也向外傳遞。

有時我們會為Vue例項新增一些屬性或者方法是一種常見的方式,例如:

import { Message } from 'element-ui'
Vue.prototype.$message = Message

this.$message.success('儲存成功')

那麼我們如何為這些例項屬性新增單元測試呢?答案是mocks,它可以為Vue例項提供額外的屬性。假如我們有如下message.vue元件:

<template>
  <div>
    <button id="success" @click="handleSuccessClick">成功</button>
    <button id="warning" @click="handleWarningClick">警告</button>
    <button id="error" @click="handleErrorClick">錯誤</button>
    <button id="info" @click="handleInfoClick">訊息</button>
  </div>
</template>

<script>
export default {
  methods: {
    handleSuccessClick () {
      this.$message.success('成功')
    },
    handleWarningClick () {
      this.$message.warning('警告')
    },
    handleErrorClick () {
      this.$message.error('錯誤')
    },
    handleInfoClick () {
      this.$message.info('訊息')
    }
  }
}
</script>

由於我們就是要mock來自第三方的外掛,因此我們在測試用例中並不需要安裝element-ui,所以我們可撰寫如下測試用例:

describe('message.vue', () => {
  it('add mocks', () => {
    const message = {
      success: jest.fn(),
      warning: jest.fn(),
      error: jest.fn(),
      info: jest.fn()
    }
    const wrapper = shallowMount(Message, {
      mocks: {
        $message: message
      }
    })
    const successBtn = wrapper.find('#success')
    const warningBtn = wrapper.find('#warning')
    const errorBtn = wrapper.find('#error')
    const infoBtn = wrapper.find('#info')

    successBtn.trigger('click')
    expect(message.success).toHaveBeenCalledTimes(1)

    warningBtn.trigger('click')
    expect(message.warning).toHaveBeenCalledTimes(1)

    errorBtn.trigger('click')
    expect(message.error).toHaveBeenCalledTimes(1)

    infoBtn.trigger('click')
    expect(message.info).toHaveBeenCalledTimes(1)
  })
})

其實測試理念還是一樣的,我們不去關心element-ui的Message 是怎麼實現的,我們關心的只是點選對應按鈕後,message的事件可以被觸發就可以了(什麼,你說要是不關心element-ui的實現,要是它內部實現錯了怎麼辦??那這鍋又不是我的,當然是甩出去啦)。

這裡是對第三方的外掛進行mock,要是要在測試用例中安裝的話就可以使用createLocalVue方法建立一個本地的Vue例項,用來替換全域性的Vue,隨後在掛載元件的時候傳遞這個本地Vue,如下:

import { shallowMount, createLocalVue } from '@vue/test-utils'
import HelloWorld from '@/components/HelloWorld.vue'
import Vuex from 'vuex'
import Router from 'vue-router'
import ElementUI from 'element-ui'
const localVue  = createLocalVue()
localVue.use(Vuex)
localVue.use(Router)
localVue.use(ElementUI)

describe('HelloWorld.vue', () => {
  it('use localVue', () => {
    const wrapper = shallowMount(HelloWorld, {
      localVue
    })
  })
})

三、vue單元測試原則

編寫容易維護的單元測試有一些原則,這些原則對於任何語言、任何層級的測試都適用。這些原則不是新東西,但總是需要時時溫故知新,前人總結成 F.I.R.S.T 五個原則,以此為鏡,可以時時檢驗你的單元測試是否高效:

  • F Fast:測試需要頻繁執行,因此要能快速執行;
  • I Independent:測試應該相互獨立,一次只測一條分支;
  • R Repeatable:測試本身不包含邏輯,能在任何環境中重複;
  • S Self-validating:只關注輸入輸出,不關注內部實現;
  • Timely:測試應該及時編寫,表達力極強,易於閱讀;

fast,單元測試只有在毫秒級別內完成,我們才會使用它。要是跑個用例都要好幾分鐘,那誰還等的下去。對前端來說,反正有UI介面,有這時間我多點幾下,他不香嗎。所以再寫測試用例時請多用mock.

Independent,一條分支就是一個業務場景。取名字已經很難了,把好幾個功能糾在一起寫的好describe和it中的描述要怎麼寫。專業點說這違反了單一職責原則。

Repeatable,跟寫宣告式的程式碼一樣的道理,測試需要都是簡單的宣告:準備資料、呼叫函式、斷言,讓人一眼就明白這個測試在測什麼。如果含有邏輯,你讀的時候就要多花時間理解;一旦測試掛掉,你咋知道是實現掛了還是測試本身就掛了呢?

Self-validating,其實在上面的元件測試中也多次提到了,對元件的測試,我們不關心內部的實現。只要輸入後能得到對應的輸出即可

Timely,測試應該及時編寫,只有在當下最熟悉業務的時候,才能夠寫出表達力最強的測試。而當我們在未來不小心破壞某個功能時,表達力強的測試才能在失敗的時候給你非常迅速的反饋。

寫測試用例的最大難點在於,測試需要花費太多的時間和精力了。迭代任務已經很重了,哪有那麼多的時間來寫測試啊。所以對專案制定測試策略還是很重要的。對前端專案來說通常有以下幾個部分utils (各種輔助工具函式),UI(最外層的主元件),component(子元件)

utils ,工具函式沒啥可說的,輸入輸出明確非常適合單元測試,要儘可能的做到100%覆蓋

UI,最外層的UI,由於前端頁面變化較快,動不動就會來個UI修改。對這塊進行測試收益不大。如果實在有時間的話,可以對一些元件內的方法進行測試,不過純UI,和css也還是沒有測試的必要。

component,這塊其實最為複雜,不過還是以代價最低,收益最高為指導原則進行。vue元件一般是以渲染出一個語法樹render()為終點的,它描述了頁面的 UI 內容、結構、樣式和一些邏輯component(props) => UI。內容、結構和樣式,比起測試,直接在頁面上除錯反饋效果更好。測也不是不行,但都難免有不穩定的成本在;邏輯這塊,有一測的價值,但需要控制好依賴。我覺得對元件來說元件分支渲染邏輯事件呼叫和引數傳遞可以被覆蓋到即可。

四、總結

對前端來說單元測試還是很有必要的,畢竟開發的功能終將被測試,bug如果不是被自己發現的話就是被使用者發現。雖然編寫會花費一些時間,但是在以後往裡面新增新功能的時候會省去很多回歸的時間(舊的單測過不了,那肯定是改掛了)。同時清晰易懂的單測,也有助於後來接手的人理解功能,也免得自己寫出的屎一樣的程式碼,被後人唾棄反覆鞭屍。