Vue 應用單元測試的策略與實踐 03 - Vue 組件單元測試

本文首發於Vue 應用單元測試的策略與實踐 03 - Vue 組件單元測試 | 呂立青的博客javascript

歡迎關注知乎專欄 —— 前端的逆襲(凡可 JavaScript,終將 JavaScript。)前端

歡迎關注個人博客知乎GitHub掘金vue


本文的目標

2.1 在Vue應用的單元測試中,對不一樣UI組件的單元測試有何不一樣?顆粒度該細到什麼樣的程度?java

// Given
一個有基本的UT知識但沒寫過Vue測試的新人🚶
// When
當他🚶閱讀和練習本文的Vue單元測試的部分
// Then
固然,他可以學會Vue組件在測試當中的幾種渲染方式
他可以學會UI組件的分類,特別是交互行爲的測試方式
複製代碼

組件化與 UI 測試

在組件化出現以前,咱們都壓根不談 UI 的單元測試,哪怕是對於 UI 頁面層級的測試來講都是一件很是困難的事情。其實組件化並不全是爲了複用,不少狀況下也偏偏是爲了分治,從而咱們能夠分組件對 UI 頁面進行開發,而後分別對其進行單元測試。git

前端組件化已經讓 UI 測試變得容易不少,每一個組件均可以被簡化爲這樣一個表達式,即 UI = f(data),這個純函數返回的只是一個描述 UI 組件應該是什麼樣子的虛擬 DOM,本質上就是一個樹形的數據結構。給這個純函數輸入一些應用程序的狀態,就會獲得相應的 UI 描述的輸出,這個過程不會去直接操做實際的 UI 元素,也不會產生所謂的反作用。github

Vue 組件樹的測試

按理來講按照純函數這樣的思路,Vue 組件的測試應該很簡單的說。但與此同時,對 UI 渲染的組件樹進行測試依然存在一個問題,從下圖中能夠看出,越處於上層的組件,其複雜度必然會隨之提升。對於最底層的子組件來講,咱們能夠很容易得將其進行渲染並測試其邏輯的正確與否,但對於較上層的父組件來講,一般來講就須要對其所包含的全部子組件都進行預先渲染,甚至於最上面的組件須要渲染出整個 UI 頁面的真實 DOM 節點才能對其進行測試,這顯然是不可取的。數組

Components-Tree

在單元測試中,一般咱們但願將重點放在做爲獨立單元進行測試的組件上,並避免間接斷言其子組件的行爲。此外,對於包含許多子組件的組件,整個 render 樹會變得很是之大,而反覆 render 全部的子組件可能會減慢單元測試的速度。瀏覽器

而根據 Mike Cohn 的測試金字塔中所提到的兩件事:數據結構

  • 編寫不一樣粒度的測試
  • 層次越高,你寫的測試應該越少

爲了維持金字塔形狀,一個健康、快速、可維護的測試組合應該是這樣的:寫許多小而快的單元測試。適當寫一些更粗粒度的測試,寫不多高層次的端到端測試。注意不要讓你的測試變成冰淇淋那樣子,這對維護來講將是一個噩夢,而且跑一遍也須要太多時間。(via 測試金字塔實戰 – ThoughtWorks洞見架構

測試金字塔

對於 Vue 組件樹來講,淺渲染(Shallow Rendering)解決了這個問題,也就是說在咱們針對某個上層組件進行測試時,能夠不用渲染它的子組件,因此就不用再擔憂子組件的表現和行爲,這樣就能夠只對特定組件的邏輯及其渲染輸出進行測試了。Vue 官方提供了 @vue/test-utils 可讓咱們使用淺渲染這個特性,用於測試虛擬 DOM 對象,即 Vue.component 的實例。

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

const wrapper = shallowMount(Component)
wrapper.vm // the mounted Vue instance
複製代碼

Vue 組件的渲染方式

淺渲染 shallowMount(component[, options]) => Wrapper

淺渲染在將一個組件做爲一個單元進行測試的時候很是有用,能夠確保你的測試不會去間接斷言子組件的行爲。shallowMount 方法就是 Shallow Rendering 的封裝,shallowMountmount 相似返回 mountedrendered Vue 組件的 Wrapper,但只會渲染出組件的第一層 DOM 結構,其嵌套的子組件不會被渲染出來,從而使得渲染的效率更高,單元測試的速度也會更快。

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

describe('Vue Component shallowMount', () => {
  it('should have three <todo /> components', () => {
    const wrapper = shallowMount(App)
    expect(wrapper.find({ name: 'Todo' })).toHaveLength(3)
  })
}
複製代碼

全量渲染 mount(component[, options]) => Wrapper

mount 方法則會將 Vue 組件和全部子組件渲染爲真實的 DOM 節點,特別是在你依賴真實的 DOM 結構必須存在的狀況下,好比說按鈕的點擊事件。徹底的 DOM 渲染須要在全局範圍內提供完整的 DOM API, 這也就意味着 Vue Test Utils 依賴於瀏覽器環境。

從技術上講,你能夠在真實的瀏覽器中運行,但因爲在不一樣平臺上啓動真實瀏覽器的複雜性,更建議使用 JSDOM 在虛擬瀏覽器環境中運行 Node 中的測試。推薦使用 mount 的方法是依賴於一個名爲 jsdom的庫,它本質上是一個徹底在 JavaScript 中實現的 headless 瀏覽器。

import { mount } from '@vue/test-utils'

describe('Vue Component Mount', () => {
  it('should delete Todo when click button', () => {
    const wrapper = mount(App)
    const todoLength = wrapper.find('li').length
    wrapper.find('button.delete').at(0).trigger('click')
    expect(wrapper.find('li').length).toEqual(todoLength - 1)
  })
})
複製代碼

靜態渲染 render(component[, options]) => CheerioWrapper

render 方法則會將 Vue 組件渲染成靜態的 HTML 字符串,而返回的則是一個 Cheerio 實例對象,採用的是一個第三方的 HTML 解析庫 Cheerio,這是一個類 jQuery 的庫,能夠在 Node.js 中遍歷 DOM。渲染後所返回的 CheerioWrapper 能夠用於分析最終結果的 HTML 代碼結構,好處是它的 API 跟 shallowMountmount 方法的 API 都基本保持一致。

import { render } from '@vue/test-utils'

describe('Vue Component Render', () => {
  it('should not have .todo-done class', () => {
    const wrapper = render(App)
    expect(wrapper.find('.todo-done').length).toEqual(0)
    expect(wrapper.text()).toContain('<div class="todo"></div>')
  })
})
複製代碼

純字符串渲染 renderToString(component[, options]) => string

renderToString 很簡單,顧名思義就是把一個組件渲染成對應的 HTML 字符串,在此再也不贅述。

import { renderedString } from '@vue/test-utils'

describe('Vue Component renderedString', () => {
  it('should have .todo class', () => {
    const renderedString = renderToString(App)
    expect(renderedString).toContain('<div class="todo"></div>')
  })
})
複製代碼

實例 Wrapper find() 方法與選擇器

從前面的示例代碼中能夠看到,不管哪一種渲染方式所返回的 wrapper 都有一個 .find() 方法,它接受一個 selector 參數,而後返回一個對應的 wrapper 對象。而 .findAll() 則會返回一個類型相同的 wrapper 對象數組,裏面包含了全部符合條件的子組件。在這個對象數組的基礎上,at 方法則能夠返回指定位置的子組件,trigger 方法用於在組件之上模擬觸發某種行爲。

@vue/test-utils 中的 Selectors 即選擇器,既能夠是 CSS 選擇器(也支持比較複雜的關係選擇器組合),也能夠是 Vue 組件 或是一個 option 對象,以便於在 wrapper 對象中能夠輕鬆地指定想要查找的節點。

/* CSS Selector */
wrapper.find('.foo') //class syntax
wrapper.find('input') //tag syntax
wrapper.find('#foo') //id syntax 
wrapper.find('[foo="bar"]') //attribute syntax
wrapper.find('div:first-of-type') //pseudo selectors
複製代碼

在下面的示例中,咱們能夠經過 Vue 組件構造函數的引用找到該組件,與此同時也能夠基於 Vue 組件屬性的子集來查找組件和節點,或者經過根據 $ref 選擇相應元素。

/* Component Constructor */
import foo from './foo.vue'

const wrapper = shallowMount(app)
expect(wrapper.find(foo).is(foo)).toBe(true)

/* Find Option Object */
const wrapper = appWrapper.find({ name: 'my-button' })
wrapper.trigger('click')

/* Find by refs */
const wrapper = appWrapper.find({ ref: 'myButton' })
wrapper.trigger('click')
複製代碼

UI 組件交互行爲的測試

咱們不但能夠經過 find 方法查找 DOM 元素,還能夠經過 trigger 方法在組件上模擬觸發某個 DOM 事件,好比 Click,Change 等等。對於淺渲染來講,事件模擬並不會像真實環境中所預期的那樣進行傳播,所以咱們必須在一個已經設置好了事件處理方法的實際節點上纔可以調用,實際上 .trigger() 方法將會根據模擬的事件觸發這個組件的 prop。例如,.trigger('click') 實際上會獲取 對應的 clickHandler propsData 並調用它。

it('should trigger event when click button', () => {  
  const clickHandler = jest.fn()
  const wrapper = shallowMount(Foo, {
    propsData: { clickHandler }
  })
  wrapper.trigger('click')
  expect(clickHandler).toHaveBeenCalled()
})
複製代碼

關於 nextTick 怎麼辦?

Vue 會異步的將未生效的 DOM 更新批量應用,以免因數據反覆突變而致使的無謂的從新渲染。這也是爲何在實踐過程當中咱們常常在觸發狀態改變後用 Vue.nextTick 來等待 Vue 把實際的 DOM 更新作完的緣由。

爲了簡化用法,Vue Test Utils 同步應用了全部的更新,因此你不須要在測試中使用 Vue.nextTick 來等待 DOM 更新。

注意:當你須要爲諸如異步回調或 Promise 解析等操做顯性改進爲事件循環的時候,nextTick 仍然是必要的。

總結一下

Vue 組件的單元測試是前端 UI 測試組合的基石,單元測試保證了代碼庫裏的每一個組件(被測試的主體)都能按照預期那樣工做,它的數量在測試組合中應該遠遠多於其餘類型的測試。其實呢,也不要太拘泥於測試金字塔中各層次的名字,UI 測試顯然沒必要位於金字塔的最高層,你也徹底能夠用 Cypress、Nightwatch 這樣的 E2E 框架對 UI 進行單元測試,這個的話咱們就留到後面再聊。

未完待續……

## 單元測試基礎

  • [x] ### 單元測試與自動化的意義
  • [x] ### 爲何選擇 Jest
  • [x] ### Jest 的基本用法
  • [x] ### 該如何測試異步代碼?

## Vue 單元測試

  • [x] ### Vue 組件的渲染方式
  • [x] ### Wrapper find() 方法與選擇器
  • [x] ### UI 組件交互行爲的測試

## Vuex 單元測試

  • [ ] ### CQRS 與 Redux-like 架構
  • [ ] ### 如何對 Vuex 進行單元測試
  • [ ] ### Vue組件和Vuex store的交互

## Vue應用測試策略

  • [ ] ### 單元測試的特色及其位置
  • [ ] ### 單元測試的關注點
  • [ ] ### 應用測試的測試策略

本文首發於Vue 應用單元測試的策略與實踐 03 - Vue 組件單元測試 | 呂立青的博客

歡迎關注知乎專欄 —— 前端的逆襲(凡可 JavaScript,終將 JavaScript。)

歡迎關注個人博客知乎GitHub掘金

相關文章
相關標籤/搜索