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

呂立青發表於2018-10-31

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

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

歡迎關注我的部落格知乎GitHub掘金


本文的目標

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

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

元件化與 UI 測試

在元件化出現之前,我們都壓根不談 UI 的單元測試,哪怕是對於 UI 頁面層級的測試來說都是一件非常困難的事情。其實元件化並不全是為了複用,很多情況下也恰恰是為了分治,從而我們可以分元件對 UI 頁面進行開發,然後分別對其進行單元測試。

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

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

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() 方法與選擇器

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

從前面的示例程式碼中可以看到,無論哪種渲染方式所返回的 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 元件互動行為的測試

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

我們不但可以通過 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掘金

相關文章