Morris's blog

務實的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)創建出來的專案 所以它...

  • react-scripts的限制

    • 雖然整合好了很多東西,但是在設定上就沒有那麼彈性。

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

  • 使用JSDom在node.js生出來的javaScript runtime底下模擬出一個假的瀏覽器

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

    var canvas = document.getElementById('tutorial');
    var ctx = canvas.getContext('2d');
              //  ^^^  拋出錯誤
     

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

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

  • 透過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好的事情。 @seeCRA_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往往會連結Provider裡的store,在為這個Component進行單元測試或是整合測試時必須要先為它準備好這樣的環境。

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

在使用react-testing-libraryrenderfunction,把要被測試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行即使用這樣的方式。

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去實行。

最後

codesandbox_testBtn

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

response