務實的react component unit test

2019-07-03

如何針對 Component 寫出務實(practical)的 test

react 裡 component 的特性

  • 有畫面,有邏輯 (html, css, js all in js)
  • 互相依賴,但是寫得好的話很容易做到可插拔,就變得很好 mock 一些依賴,props, HOC, renderProps這些設計和技巧都讓人很有依賴注入的感覺。

專案情境描述

  • 一個用 Create-react-app (以下都簡稱 CRA)創建出來的專案 所以它... -test 的驅動其實是靠 react-scripts 在做 (the tets script on GitHub)
    • test runner 就是 jest
  • react-scripts 的限制
    • 雖然整合好了很多東西,但是在設定上就沒有那麼彈性。

CRA 的測試在開始執行之前做了什麼

  • 使用 jsdom 在 node.js 的 JavaScript runtime 底下模擬出一個假的瀏覽器

  • 因為是假的,所以有些 Web API 是不存在的,如 canvas

    var canvasDomNode = document.getElementById('tutorial')
    var ctx = canvasDomNode.getContext('2d')
    //                    ^^^  Uncaught TypeError: canvasDomNode.getContext is not a function

    如果 component 裡有用到這些 API 的話,就必須想辦法做出一個替代品,讓它在能夠取代在執行測試時不存在的 API(俗稱 mock )

  • JSDom 在記憶體中建構出像是 DomTree 的東西,但是不會畫出畫面來(也沒辦法畫),因為不會畫出畫面,所以測試流程就能執行得比較

  • 透過 jest 去執行我們寫好的測試檔(describe, it, expect), jest 會自動在專案底下找檔名是符合特定格式的檔案來執行(xxx.test.js or xxx.spec.test ) @see CRA doc

    • 上面的事情,除了 mock 特定的 Web APIs 以外, CRA 在它的 test 指令裡都幫我們處理好了,感謝 CRA !
      # in pacaage.json file
      # ...其他設定
      "scripts": {
          # ......其他 npm script
        "test": "react-scripts test --env=jsdom",
        # 更多 npm script
      }

    所以執行 yarn run test 時就會執行測試

要在哪裡 mock 特定的 WebAPIs

  • package.json 檔案裡,可以加上一個 jest 的區塊,來 config jest,@see

      # in package.json file
    jest {
        # ...設定
    }

    其中有一個屬性 setupFiles 可以用來指定在 jest 開始執行測試前要先執行過哪些檔案,但是這個屬性在 CRA 底下沒辦法使用。
    所幸 CRA團隊 有留一個後門,只要在 src 底下寫一個 setupTests.jsreact-script 在執行測試前會先去執行過裡面的 js,我們就能夠在裡面做要先 mock 好的事情。 @see CRA_Issue6020

    CRA_setupTest_js

使用合適的 test helper library

  • 使用合適的 test helper library 來幫我們處理 render , 透過某些方式選取到指定的節點, 做assert這些都是好的 test helper library 能幫助我們的,
    之前在 react 社群中很流行使用 Airbnb公司 開源的 enzyme

    但是這邊我們要選擇另外一套目前在 react 社群中越來越火的, react-tesing-library ,它是在 react 社群中有名的 influencer Kent C. Dodds所推出。
    他在他的部落格youtube頻道中都介紹到很多很有用的 react 開發秘訣,本次我們也會參考他 youtube頻道中的 Testing a React component that uses useContext 這段之前的直播影片,來先寫出一個能夠搭配Redux使用的 test util function。

做好一個 custom render 的 util function

在上面的影片中 Kent 先寫了一個 custom render 的 util function,這是一個很重要的關鍵,儘管影片中是搭配 react 16.8 之後的 hook APIuseContext 使用,但是概念是一樣的,我們要測試的 Component 往往會連結 react-redux 的 Provider 裡的 store ,在為這個 Component 進行單元測試或是整合測試時必須要先為它準備好這樣的環境。

以下是一個 react 寫成的 todoList 的範例,host 在 CodeSandbox 上,

在使用 react-testing-libraryrender function ,把要被測試 Component render 到 jsdom 之前,我們連 react-reduxProvider 一起 init,並且把外部傳進來要塞進去 store 裡的假資料一起交給 redux ,搭配我們的 reducers,一起組成 store

在測試執行的時候我們就能夠直接把假的資料直接傳到我們的 helper function 裡,受測試的 Component 得到 由 mapStateToProps 轉化而成的 Props 裡的值,而直接 render 出畫面。

針對 middleware 那裡的操作是排除的。因為那些地方的測試會由針對 action creator 的測試去執行,這樣才夠單元...

一個 test case

上面的單元測試中我們可以先注意幾個地方:

  1. 第 7 行 import 了 jest-dom/extend-expect ,它是 test-library 團隊出的,目的是為了要讓執行測試時的 jest 有更多的assertfunction 可用

  2. 第 18 行在每個 describe 之後都會發動 cleanup ,應該是為了清除 testing-library 自己內部的狀態之類的

  3. 第 20 行 跟 第 35 行各是一個 test,每次都從新準備假資料,從新發動我們事先準備好的 renderReduxConnectedHOC ,每次 describe 裡都是在生出一組新的 Provider & Store,這也是 Kent C. Dodds 在影片中強調的,要保持每次測試的獨立性- test isolation with react ,確保上一次的測試不會影響到下一次的測試,不然可能只是越測越亂。

  4. renderReduxConnectedHOC 會返回一個由 react-testing-libraryrender 生成的 container,在之後我們就可以用它的各種 queriesAPI 去選取元素,準備去做 expect,因為我們的 Component 是用 StyledComponents 產生出來的,它產生出來的 Component 是沒有固定的 css class 的,所以我們無法用普通的 css selector 去抓到我們想要比對的元素,所以我們在要抓取的 dom 的地方,在 render 時,讓它 render 出 data-testidreact-testing-library 有對應的能透過 data-testid 去抓到元素的 queryAPI ,在第 29 行,第 48 行即使用這樣的方式。

  5. data-testid在要釋出正式版的程式碼時可以透過 babel-plugin-react-remove-properties去消除,可能在develop跟production的時候要使用 不同的.babelrc

UI 是資料的映射

今日的 web UI framework 的設計理念都是想要做到UI是資料的映射,資料一變動,UI就跟著變動,就像 用 Excel spreadsheet 函數在做資料的計算那樣,所以 react 真的很 react(UI 一直在反應著資料)。 Excel-react

在上面的 test case 中第一個 describe 是測試可以 render 得出一個個的 todoItem,所以給三筆 todo 資料,畫面上就必須要 render 出三筆 todo(用 data-testid 去抓取,然後比對抓到的 nodeList 的長度),簡單明暸。

再來就是這個 TodoList App 有一個 Highlight todo 的功能,click 了 Highlight 按鈕之後,該 todo 會改變顏色,然後跑到 List 區的最上端。如果我們想測,點擊了之後會 Highlight todo 這件事情,該怎麼寫測試?

我們先思考,一個 todo 怎麼會知道它是被 Highlight 的?畫面上是怎麼知道它必須被 Highlight?
那是因為每個 todo 資料裡會有一個 highlighted 屬性,click Highlight button 這件事情本身只是在修改對應的 todo data 裡那個屬性的值,
todoItem 在 render 的時候檢查自己的 highlighted 屬性,
如果是 true ,就 render 成 被 highlight 時該呈現的樣子,
如果是 false 就 render 回 沒有 被 highlight 時的樣子。

所以該測的不是 click 這件事,應該是要測試給了幾筆 highlighted 是 true 的資料,對應 render 出來的有被 highlight 的資料該有幾筆(給不同的 data-testid),沒有被 highlight 的資料應該要有幾筆,因為 UI 是資料的映射,UI 會反應資料,資料一變動,UI 應該要跟著起變動。

click 通常只是發送一個 action 去到 Reducer 那裡,這件事情可以在針對 action creator 的單元測試裡處理(測試該 action creator function 是否 return 出正確的 action object),而按鈕能否點擊(比如說是不是有綁好 even-handler,或是上面是否有蓋到一層 z-index 比下層還高的透明區塊造成下面被覆蓋的區塊點擊不到)這件事可以移到 e2e 測試中,寫自動化測試 script 去實行。

但是如果你真的想透過 react-testing-library 在寫的測試程式裡去執行 click, change 之類的事件的話,其實也是可以做得到的。 @see fireEvent

最後

codesandbox_testBtn

這個 CodeSandbox 的環境是可以直接執行上面提到的測試的,按下上圖紅框匡起來的按紐就能執行測試(超神奇!),請務必試試看。

response