務實的react component unit test
如何針對 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 特定的 Web APIs 以外,
要在哪裡 mock 特定的 WebAPIs
package.json
檔案裡,可以加上一個 jest 的區塊,來 config jest,@see# in package.json file jest { # ...設定 }
其中有一個屬性 setupFiles 可以用來指定在
jest
開始執行測試前要先執行過哪些檔案,但是這個屬性在CRA
底下沒辦法使用。
所幸CRA團隊
有留一個後門,只要在src
底下寫一個setupTests.js
,react-script
在執行測試前會先去執行過裡面的 js,我們就能夠在裡面做要先 mock 好的事情。 @see CRA_Issue6020
使用合適的 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 API
的 useContext 使用,但是概念是一樣的,我們要測試的 Component 往往會連結 react-redux 的 Provider
裡的 store
,在為這個 Component 進行單元測試或是整合測試時必須要先為它準備好這樣的環境。
以下是一個 react 寫成的 todoList 的範例,host 在 CodeSandbox 上,
在使用 react-testing-library
的 render
function ,把要被測試 Component render 到 jsdom 之前,我們連 react-redux
的 Provider
一起 init,並且把外部傳進來要塞進去 store
裡的假資料一起交給 redux
,搭配我們的 reducers,一起組成 store
在測試執行的時候我們就能夠直接把假的資料直接傳到我們的 helper function 裡,受測試的 Component 得到
由 mapStateToProps
轉化而成的 Props 裡的值,而直接 render 出畫面。
針對 middleware 那裡的操作是排除的。因為那些地方的測試會由針對 action creator
的測試去執行,這樣才夠單元
...
一個 test case
上面的單元測試中我們可以先注意幾個地方:
第 7 行 import 了 jest-dom/extend-expect ,它是
test-library
團隊出的,目的是為了要讓執行測試時的 jest 有更多的assert
function 可用第 18 行在每個
describe
之後都會發動cleanup
,應該是為了清除 testing-library 自己內部的狀態之類的第 20 行 跟 第 35 行各是一個 test,每次都從新準備
假資料
,從新發動我們事先準備好的renderReduxConnectedHOC
,每次describe
裡都是在生出一組新的 Provider & Store,這也是 Kent C. Dodds 在影片中強調的,要保持每次測試的獨立性
- test isolation with react ,確保上一次的測試不會影響到下一次的測試,不然可能只是越測越亂。renderReduxConnectedHOC
會返回一個由react-testing-library
的render
生成的 container,在之後我們就可以用它的各種 queriesAPI 去選取元素,準備去做 expect,因為我們的 Component 是用 StyledComponents 產生出來的,它產生出來的 Component 是沒有固定的css class
的,所以我們無法用普通的 css selector 去抓到我們想要比對的元素,所以我們在要抓取的 dom 的地方,在 render 時,讓它 render 出data-testid
,react-testing-library
有對應的能透過data-testid
去抓到元素的 queryAPI ,在第 29 行,第 48 行即使用這樣的方式。data-testid
在要釋出正式版的程式碼時可以透過 babel-plugin-react-remove-properties去消除,可能在develop跟production的時候要使用不同的.babelrc
UI 是資料的映射
今日的 web UI framework 的設計理念都是想要做到UI是資料的映射,資料一變動,UI就跟著變動
,就像
用 Excel spreadsheet 函數在做資料的計算那樣,所以 react 真的很 react(UI 一直在反應著資料)。
在上面的 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 的環境是可以直接執行上面提到的測試的,按下上圖紅框匡起來的按紐就能執行測試(超神奇!),請務必試試看。