Jest | 再一次測試你的 Component - feat.react-testing-library 基本用法
前言
Hi !大家好,雖然之前有使用 Enzyme 講解如何搭配 Jest 對 React 的Component 做測試,但是幾個禮拜前偶然在某個討論串中看到有大神推薦另一套測試 Component 的套件 react-testing-library ,功能和 Enzyme 相同,兩者都是在測試時 Render Component 的 DOM 下斷言測試,如果是剛接觸 Enzyme 的朋友,不妨可以參考看看兩者的不同,來選擇愛用套件 😃 。
react-testing-library
SUT (測試目標)
在開始測試之前,仍然需要一個小助手,這裡請出之前常露面的 Counter
來擔任 SUT:
安裝 react-testing-library
因為只有在開發時的 Test 上用得到套件,因此安裝在 devDependencies 裡:
npm install --save-dev react-testing-library
6 / 16 更新:要注意哦! react-testing-library 似乎在版本 8 的時候將套件換成 @testing-library/react 了,目前筆者還不曉得差異在哪裡,使用來也沒有感到差別,所以如果文章中有問題再麻煩留言告知,感激不盡!
//新版本:
npm install --save-dev @testing-library/react
常用 API
撰寫測試前,先簡單說明幾個常用的 API :
render
react-testing-library 的 render
類似於 Enzyme 中的 Mount
,意思是它會將所有的子組件都 render
出來成為 DOM 節點。
getByTestId 、 getByText
render
後會回傳的 Method ,兩個都是用來搜尋 DOM , getByTestId
是以 DOM 中的 data-testid
值取要斷言的 DOM , getByText
則是以該 DOM 內呈現的內容,獲取到 DOM 後便能以 textContent
再取得內容。
container
container
也是 render
所回傳的,等於取得整包 DOM 物件,甚至是能夠直接對它使用 querySelector 來搜尋節點,通常我會在搞不清楚到底 Render 了什麼的時候,用 innerHTML
來偷看 😆。
fireEvent
這個 Method 可以觸發 DOM 的事件,例如 onClick
、 onChange
等等。
開始測試
其實只要了解上述幾個簡單的 API ,就能夠輕鬆對 Component 的節點做測試,下方先 render
出 Counter
,並對 span
的內容做斷言,確認是否 render
正確:
上方用了三種方式去取得 span
來確認 Component 顯示的是否正確, getByTestId
就是直接抓取相同 data-testid
的 DOM,然後如果不幸有兩個 DOM 同時用了一樣的 data-testid
那測試就會發生錯誤:
但錯誤的提示還滿不錯的,如果真的必須要取同樣的名稱,也可以用 getAllByTestId
,來獲得一個陣列,當然它也是由 render
回傳的 Method 之一。
第二種方式是以 DOM 的內容來獲取,因為一開始 Counter
中 State
的 count
值是 0 ,所以可以知道 span
的內容會是 點了0下
,這個 Mtehod 可以用在內容不會改變的地方,像是登入按鈕的字就永遠是登入,儲存就永遠是儲存,不會有變化,這種情況就能用 getByText
。
第三種的 container
就把它當成 JavaScript
取得的物件就好,但通常不會使用它來查找 DOM ,但為什麼?不是很方便嗎?這個是有原因的,文章的最後會整理一些結論。
接著,要來確認的是按鈕的事件,點擊時會不會讓 count
值加上一或二,這時候就能用到剛剛提及的 getByText
因為按鈕的文字是不會改變的,找到按鈕後就能使用 fireEvent
觸發點擊:
如果是 change
也是一樣的方式,只需將改變的值放在 fireEvent
的第二個參數,要注意的是改變的值仍然要模擬觸發事件本身的 event
:
fireEvent.change(input, { target: { value: '2', }, });
測試結果如下:
感覺上寫起來是不是直觀許多了?而且在這之前一直故意不去提,不曉得大家有沒有發現,上述的 Counter
是透過 Hooks 的 useState
管理 State ,也就是說 react-testing-library 百分之百完美相容 Hooks ,測試起來絕對不會有問題!
顯然這是個令人開心的消息,但 Redux 呢?會不會相對變得複雜?
你的考量,我看得見!
下方就持續講解該如何在 Redux 專案中進行測試!
Redux
這裡先將小助手 Counter
申裝上 Redux :
也少不了 Redux 的標配 actions
、 reducer
、 store
:
開始測試
欸等等?不需要再裝些什麼其他 redux-mock-store
嗎?
完全不用
貫徹 Mount
的精神,就直接用正式的 Reducer
正式的 store
來測試,把覆蓋率蓋好蓋滿,以下會示範三種在 Redux 中玩轉測試的方法。
直接使用 Reducer 創建 Store
在 render
的時候就直接帶入 Store 了,而且例子中有特別印了關於 Store 的兩段 console.log
,可以清楚看見 Store 會隨著測試改變 State 的值:
如此一來也能更方便的知道,觸發某個 dispatch
後, Store 內的 State 變化是不是在預料之中。
指定 Reducer 的預設值
除了用原有的 Reducer 外,也能另外指定 State 取代 Reducer 自身的初始 State :
自訂 renderWithRedux
這個 render
方式不是 react-testing-library 原有的 Method ,而是官方用了一些小技巧另外寫的,它長這樣子:
看起來有點複雜,但其實內部的原理就是將上方例子 render
的步驟簡化成 Method ,回傳的結果也和 render
後的 Component
一樣,只是會多傳一個 Store ,實際用起來如下:
筆者也建議可以直接使用 renderWithRedux
,讓測試的畫面看起來比較乾淨俐落,不會定義一堆重複的東西,上方關於 Redux 的例子也都會改成 renderWithRedux
重新寫在筆者的 GitHub 上,可以再參考看看。
使用心得
react-testing-library 的基本方法大家都應該了解了,最後就來談談使用的心得。
一開始最困惑的點是 getByTestId
和 getByText
,根本就不曉得到底為什麼要這樣子做,因此大量了使用 container
搭配 querySelector
抓取想斷言的 DOM ,初期用得很開心,但是最後突然發現,如果節點的位置發生改變,或多了另一個 DOM ,都有可能會讓 Test Case 錯誤,但其實不是錯在邏輯,而是因為原本的 querySelector
已經取不到更改前的 DOM 。
這裡 react-testing-library 的開發者 Kent C. Dodds ,也有在他的 Blog 寫了一篇文章提出對 UI 面對測試時的看法
文章裡有個最簡單的例子,當假設 Component 中有一個按鈕:
<button className="button_style" type="button">點我</button>
那 Test Case 會這樣子得到它:
container.querySelector('button[class="button_style"]')
看起來一切正常,但是當對 Component 做了異動:
<div>
<button className="button_style" type="button">想不到吧</button>
<button className="button_style" type="button">點我</button>
</div>
原本 Test Case 中的 querySelector
就取成第一個新增的按鈕,而不是原有的 點我
按鈕。
再來,若是有天 button
們都不再需要依賴 button_style
這個樣式,那是否應該要為了 Test Case 而將這沒有任何用的 ClassName 屬性留下?答案應該很明顯。
因此,提升 UI 在測試時的適應力非常重要!
如果考量到 data-testid
被 build 後會被看見,也可以透過 babel-plugin-react-remove-properties 將 data-testid
移除。
本文用了一些例子講解 react-testing-library ,因為筆者大約是今年三月多才開始玩測試,所以 Enzyme 及 react-testing-library 體感上會偏好使用後者,因為寫起來還滿方便的而且又支援 Hooks 😅,不過如果有不同看法,也歡迎提出一起討論!
最後對於文章中講解有任何不清楚或是覺得有需要補充的地方,再麻煩留言指教,謝謝!
參考文章: