「技術雷達」之使用 Enzyme 測試 React(Native)元件

呂立青發表於2019-02-28

本文同步發表於個人部落格:使用 Enzyme 測試 React(Native)元件 - 呂立青的部落格

元件化與 UI 測試

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

特別是當瀏覽器中的 Web 應用越來越龐大的時候,借鑑於在後端將大型單體應用拆分成微服務架構的最佳實踐一樣,前端應用也可以被拆分成不同的頁面和特性。每個特性由一個單獨的團隊從端到端對其負責,它允許團隊規模化地交付那些能夠獨立部署和維護的服務,在最新一期的技術雷達當中這種方式稱之為微前端,微前端的目標就是允許 Web 應用的特性之間彼此獨立,每個特性可以獨立地開發、測試和部署。

React.js 作為前端框架的後起之秀,卻在 2015 年攜著虛擬 DOM,元件化,單向資料流等利器,給前端 UI 構建掀起了一波聲勢浩大的函式式新潮流。雖然說元件化不是 React 最先提出來的,但卻是 React 使得元件化在前端世界裡發揚光大的,而現在幾乎所有的所謂現代化 UI 框架比如 Angular 或者 Vue 都已經將元件化作為框架的立足之本。

「技術雷達」之使用 Enzyme 測試 React(Native)元件

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

React 元件樹的測試

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

「技術雷達」之使用 Enzyme 測試 React(Native)元件
Components-Tree

Shallow rendering lets you render a component "one level deep" and assert facts about what its render method returns, without worrying about the behavior of child components, which are not instantiated or rendered. This does not require a DOM.

淺渲染(Shallow Rendering)解決了這個問題,也就是說在我們針對某個上層元件進行測試時,可以不用渲染它的子元件,所以就不用再擔心子元件的表現和行為,這樣就可以只對特定元件的邏輯及其渲染輸出進行測試了。Facebook 官方提供了 react-addons-test-utils 可以讓我們使用淺渲染這個特性,用於測試虛擬 DOM 物件,即 React.Component 的例項。

使用 Enzyme 簡化測試程式碼

我們常常會提到,測試程式碼對於複雜程式碼庫的可維護性至關重要,但是測試的程式碼本身的易於理解和編寫,以及可讀性和可維護性也同等重要。

Enzyme is a JavaScript Testing utility for React that makes it easier to assert, manipulate, and traverse your React Components' output.

而 Enzyme 則來自於活躍在 JavaScript 開源社群的 Airbnb 公司,是對官方測試工具庫(react-addons-test-utils)的封裝,它模擬了 jQuery 的 API,非常直觀並且易於使用和學習,提供了一些與眾不同的介面和幾個方法來減少測試的樣板程式碼,方便你判斷、操縱和遍歷 React Components 的輸出,並且減少了測試程式碼和實現程式碼之間的耦合。Enzyme 理論上應該與所有 TestRunner 和斷言庫相相容,已經整合了多種測試類庫,比如 Jest,Mocha & Chai,或者 Jasmine,不過這些不是我們今天的重點。

對比一下兩者 facebook/react-addons-test-utils vs airbnb/enzyme 的 API 就一目瞭然,立見分明:

「技術雷達」之使用 Enzyme 測試 React(Native)元件

Enzyme 的三種渲染方法

shallow(node[, options]) => ShallowWrapper

shallow 方法就是對官方的 Shallow Rendering 的封裝,淺渲染在將一個元件作為一個單元進行測試的時候非常有用,可以確保你的測試不會去間接斷言子元件的行為。shallow 方法只會渲染出元件的第一層 DOM 結構,其巢狀的子元件不會被渲染出來,從而使得渲染的效率更高,單元測試的速度也會更快。

import { shallow } from 'enzyme'

describe('Enzyme Shallow', () => {
  it('App should have three <Todo /> components', () => {
    const app = shallow(<App />)
    expect(app.find('Todo')).to.have.length(3)
  })
}複製程式碼

mount(node[, options]) => ReactWrapper

mount 方法則會將 React 元件渲染為真實的 DOM 節點,特別是在你依賴真實的 DOM 結構必須存在的情況下,比如說按鈕的點選事件。完全的 DOM 渲染需要在全域性範圍內提供完整的 DOM API, 這也就意味著它必須在至少“看起來像”瀏覽器環境的環境中執行,如果不想在瀏覽器中執行測試,推薦使用 mount 的方法是依賴於一個名為 jsdom 的庫,它本質上是一個完全在 JavaScript 中實現的 headless 瀏覽器。

import { mount } from 'enzyme'

describe('Enzyme Mount', () => {
  it('should delete Todo when click button', () => {
    const app = mount(<App />)
    const todoLength = app.find('li').length
    app.find('button.delete').at(0).simulate('click')
    expect(app.find('li').length).to.equal(todoLength - 1)
  })
})複製程式碼

render(node[, options]) => CheerioWrapper

render 方法則會將 React 元件渲染成靜態的 HTML 字串,返回的是一個 Cheerio 例項物件,採用的是一個第三方的 HTML 解析庫 Cheerio,官方的解釋是「我們相信 Cheerio 可以非常好地處理 HTML 的解析和遍歷,再重複造輪子只能算是一種損失」。這個 CheerioWrapper 可以用於分析最終結果的 HTML 程式碼結構,它的 API 跟 shallow 和 mount 方法的 API 都保持基本一致。

import { render } from 'enzyme'

describe('Enzyme Render', () => {
  it('Todo item should not have todo-done class', () => {
    const app = render(<App />)
    expect(app.find('.todo-done').length).to.equal(0)
    expect(app.contains(<div className="todo" />)).to.equal(true)
  })
})複製程式碼

Enzyme 的 API 方法

find() 方法與選擇器

從前面的示例程式碼中可以看到,無論哪種渲染方式所返回的 wrapper 都有一個 .find() 方法,它接受一個 selector 引數,然後返回一個型別相同的 wrapper 物件,裡面包含了所有符合條件的子元件。在這個物件的基礎上,at 方法則可以返回指定位置的子元件,simulate 方法可以在這個元件上模擬觸發某種行為。

Enzyme 中的 Selectors 即選擇器類似於 CSS 選擇器,但是隻支援非常簡單的 CSS 選擇器,如果需要支援複雜的 CSS 選擇器,就需要引入 react-dom 模組的 findDOMNode 方法,而這是官方的 TestUtils 本身都不提供的方式。

/* CSS Selector */
wrapper.find('.foo') //class syntax
wrapper.find('input') //tag syntax
wrapper.find('#foo') //id syntax 
wrapper.find('[htmlFor="foo"]') //prop syntax複製程式碼

Selectors 也可以是許多其他的東西,以便於在 Enzyme 的 wrapper 中可以輕鬆地指定想要查詢的節點,在下面的示例中,我們可以通過 React 元件建構函式的引用找到該元件,也可以基於 React 的 displayName 來查詢元件,如果一個元件存在於渲染樹中,其中設定了 displayName 並且它的第一個字元為大寫字母,就能通過字串找到它,與此同時也可以基於 React 元件屬性的子集來查詢元件和節點。

/* Component Constructor */
wrapper.find(ChildrenComponent)
myComponent.displayName = 'ChildrenComponent'
wrapper.find('ChildrenComponent')

/* Object Property Selector */
const wrapper = mount(
  <div>
    <span foo={3} bar={false} title="baz" />
  </div>
)

wrapper.find({ foo: 3 })
wrapper.find({ bar: false })
wrapper.find({ title: 'baz'})複製程式碼

測試元件的互動行為

我們不但可以通過 find 方法查詢 DOM 元素,還可以通過 simulate 方法在元件上模擬觸發某個 DOM 事件,比如 Click,Change 等等。對於淺渲染來說,事件模擬並不會像真實環境中所預期的那樣進行傳播,因此我們必須在一個已經設定好了事件處理方法的實際節點上才能夠呼叫,實際上 .simulate() 方法將會根據模擬的事件觸發這個元件的 prop。例如,.simulate('click') 實際上會獲取 onClick prop 並呼叫它。

Sinon 則是一個可以用來 Mock 和 Stub 資料程式碼的第三方測試工具庫,當我們需要檢查一個元件當中某個特定的函式是否被呼叫時,我們可以使用 sinon.spy() 方法監視所傳入該元件作為 prop 的 onButtonClick 方法,然後再通過 wrapper 的 simulate 方法模擬一個 Click 事件,最終驗證這個被 spy 的 onButtonClick 函式是否被呼叫。

it('simulates click events', () => {  
  const onButtonClick = sinon.spy()
  const wrapper = shallow(
    <Foo onButtonClick={onButtonClick} />
  )
  wrapper.find('button').simulate('click')
  expect(onButtonClick.calledOnce).to.be.true
})複製程式碼

如何測試 React Native?

前面我們所談論的都是如何測試使用 react-dom 所構建的 React 元件,即最終渲染的結果是瀏覽器當中的 DOM 結構,但對於 React Native 來說,JavaScript 程式碼最終會被編譯並用於呼叫 iOS 或 Android 上的 Native 程式碼,因此無法再使用基於 DOM 的測試工具了。與此同時,React Native 還有特別多的 Mobile 環境依賴,所以在沒有真實裝置的情況下很難對其執行環境進行模擬,特別是當你希望在持續整合伺服器(如 Jenkins、Travis CI)執行單元測試的時候。

事實上,我們可以通過欺騙 React Native 讓它返回常規的 React 元件而不是 Native 元件,然後就又能愉快地使用傳統的 JavaScript 測試庫來單獨測試 React Native 元件邏輯。最基本的 mock 示例程式碼如下:

const mockComponent = (type) => {
  return React.createClass({
    displayName: type,
    propTypes: {
      children: React.PropTypes.node
    },
    render() {
      return <div {...this.props}>{this.props.children}</div>
    }
  })
}

RN.View = mockComponent("View")
RN.Text = mockComponent("Text")
RN.Image = mockComponent("Image")複製程式碼

Enzyme 推薦在測試環境中使用 react-native-mock 這個輔助庫,這是一個使用純 JavaScript 將全部的 React Native 元件進行 mock 的第三方庫,只需要匯入這個庫就可以對 React Native 元件進行渲染和測試。

總結

技術雷達:我們非常享受Enzyme為React.js應用提供的快速元件級UI測試功能。與許多其他基於快照的測試框架不同,Enzyme允許開發者在不進行裝置渲染的情況下做測試,從而實現速度更快,粒度更小的測試。在開發React應用時,我們經常需要做大量的功能測試,而Enzyme可以在大規模地減少功能測試數量上做出貢獻。

「技術雷達」之使用 Enzyme 測試 React(Native)元件
TechRadar

相關文章