今天要進入如何對 Component 做測試,這裡筆者之前是使用 Enzyme ,可能也是主流的 React 的測試框架,但是學測試的時候 React 剛改版到 v16,那時候 Enzyme 還沒辦法支援 Hooks 做測試,所以筆者就不小心跳槽到 React Testing Library ,想說在 Enzyme 支援 Hooks 前找個避風港,結果沒想到一到 React Testing Library 後就回不去了。
React Testing Library 的測試方式很特別,一開始使用時可能會有點疑惑,但最後越寫越倒吃甘蔗,甚至還推坑身邊的朋友使用,接下來換我用本篇文章推坑給大家了:)
npm install --save-dev @testing-library/react
這個部分和大家說明一下,因為目前寫下的 Component 都搭配了 Redux,所以下方會先新增一個新的 Component Counter
,讓大家瞭解對單一 Component 的基本測試方法,接下來兩篇才會再解釋如何在 Redux 和 Router 的環境下測試 Comonent。
在 src/component 下建立一個 Counter 目錄,我們要在裡面建立本篇文章要測試的 Component,程式碼如下:
相信大家跟著我一路走到現在,都能夠輕鬆了解 Counter
的內容,當然也別忘了在同目錄下建立一個 index.js,將 Counter
匯出:
src/component/Counter/index.js
import Counter from './Counter';
export default Counter;
受測的 Component 已經完成了,接下來要考驗大家的記憶力,記得前兩天剛學測試的時候,我們將外部的 function import
到測試檔案做測試時發生了什麼事嗎?
好了,公佈答案,就是因為在測試時沒有經過編譯,所以因為不認識 import
而發生錯誤,因此在專案下新增了 .babelrc.js,讓 ES6
的語法能在測試時被編譯。
所以現在的 React 也要,雖然還沒看見錯誤,但是那種東西最好是別出現比較好,打開 .babelrc.js,加入 React 的 Preset:
module.exports = {
presets: ['@babel/preset-react', '@babel/preset-env'],
};
除此之外,接下來我們要驗證的東西會 Render 成 Dom,因此我們還會需要使用 @testing-library/jest-dom,有了它便可以對 Dom 做一些基本的斷言,例如有沒有被 Render 出來等等。
下載 @testing-library/jest-dom
:
npm install @testing-library/jest-dom --save-dev
撰寫測試前,先簡單說明幾個常用的方法:
react-testing-library 的 render
會將所有的子組件都 Render 出來成為 Dom 節點。
render
後會回傳的方法,兩個都是用來搜尋 Dom,getByTestId
是以 Dom 中的 data-testid
值取要斷言的 Dom,獲取到 Dom 後便能以 textContent
屬性取得其內容。
container
也是 render
所回傳的,等於取得整包 Dom 物件,甚至是能夠直接對它使用 querySelector 來搜尋節點,通常我會在搞不清楚到底 Render 了什麼的時候,用 innerHTML 來偷看,其實是滿少用的。
這個 Method 可以觸發 Dom 的事件,例如 onClick
、onChange
等等。
進入到測試後,讓我們為 Component 的測試案例建立一個新資料夾 component,並將要測試的 Component 建立對應的同名測試的路徑,例如上方將要用 src/component/Counter 做測試,那測試的文件就放到__tests__/component/Counter:
|-__tests__
|-component
|-Counter
|-Counter.test.jsx
Counter.test.jsx 就是我們要寫下測試 Counter
的檔案了。
根據上方那幾個常用方法,第一步先在測試案例中將要測試的 Component Counter
用 @testing-library/react 的 render
將 getByTestId
取出來:
因為 getByTestId
只能找到被標記 data-testid
屬性的 DOM,因此我們要回到 Counter
中,替裡面的 div
、button
、span
都加上 data-testid
。
以 Counter
為例,它的功能就是使用者會看見畫面上出現按鈕及目前點擊次數的文字,且點擊按鈕時會讓顯示的次數加一,因此我們的測試案例要寫下的內容有兩項:
Counter
有沒有正常 render。既然曉得我們要做什麼,那需要加上 data-testid
的部分就是,Counter
Render 出來的第一層 div
,以及點擊可以增加次數的 button
和讓我們能夠驗證次數是否有增加的 span
:
回到 Counter.test.js 測試檔案,既然需要驗證 Dom 是否有正確 Render,就得使用剛剛下載的 @testing-library/jest-dom,它有個斷言為 toBeInTheDocument
,可以確認指定的 Dom 有沒有 Render,但是 toBeInTheDocument
並沒有在原始的 jest
斷言庫中,因此要另外使用 expect.extend()
把它加入斷言,完整的測試案例會長這樣:
完成第一個測試案例後,可以輸入指令 npm run test
確認是否正常測試:
看來很完美,接下來要測試點擊按鈕後 span
的內容是否會加一,要觸發事件可以使用上方列出的 fireEvent
,使用方式為:
先用 getByTestId
取得要觸發的按鈕,接著再將按鈕放進 fireEvent
的 click
觸發(如果是 change
的話,可以在 fireEvent
放入第二個參數,就是要改變的值)。
那既然現在可以點擊按鈕了,就要取出 span
,並用 textContent
取得其內容,驗證數字是否有隨著點擊而加一,因為 textContent
取出來是字串,所以就直接使用 toBe
做斷言驗證:
沒問題的話執行測試結果應該會 PASS:
最後提一下 getByTestId
的部分,應該會有很多人疑惑,Dom 身上有太多東西可以參考了,像是 ClassName
或是上方說的以 container
配合 querySelector 把 Dom 取出來,為什麼偏偏要使用 data-testid
加上 getByTestId
這種看似多餘的步驟來測試,其實有個主要的原因:
舉個簡單的例子,假設在測試案例中我使用了 container
加上 querySelector,尋找某個 ClassName
,然後也順利將 Dom 給取出,順利測試。
但是如果有一天,那個 Dom 不再需要該 ClassName
,你也沒辦法將它刪除,因為測試案例依賴著 ClassName
,即使它不再使用以及沒任何意義。
data-testid
,也就間接守護了測試的可維護性。沒有可維護性的測試,只要改了一些 Dom(例如:調換兩個按鈕的排列順序,或是將它們移動到另一個 div
中),就要接受原本正常的測試案例出錯,就算 Component 本身的行為沒有改變也一樣,要維護的程式碼便成了兩倍,最後就會讓人越來越不想繼續編寫測試,是放棄測試最常見的原因之一。
接下來要說明的是覆蓋率,他能提供給我們一個測試的指標,
如果要在測試後產生覆蓋率報告,得在 jest
的測試指令後增加一段 --coverage
為了方便,還是將它加到 package.json 的 script
中:
接著執行 npm run jest-cov
,就能看見結果有些不同,除了 PASS 的提示外,又多了一個表格:
這就是覆蓋率報告,各項指標內容由左至右分別為:語法、分支、函式數、行數。
除此之外,jest
也會自動在專案目錄下建立一個目錄 coverage,用網頁開啟 coverage/lcov-report/index.html,也能看到各項測試內更詳細的內容:
標記綠色的部分代表測試案例有執行到該部分,如果沒有的話就會是紅色的,例如先把測試案例 Counter_Click_AddCounter
先註解掉,在產生覆蓋率報告,就會看見結果與剛才有一段落差:
覆蓋率整體變低了,此時再到 coverage/lcov-report/index.html 內查看 Counter
,便能看見沒有被測試覆蓋到的部分:
因為測試案例少了點擊按鈕,所以它的 onClick
就沒執行,覆蓋率也就沒有 100%。
本文的範例程式碼會提供在 GitHub 上,歡迎各位參考:)
當初第一次看見 getByTestId
真的非常不習慣,所以一直用 container
配合 querySelector 去找要斷言的 Dom,後來才知道這樣只要稍微有個地方結構改變,測試案例就會完全爆掉,是一段痛苦的經驗,因此為了讓你的測試適應變化,還是要習慣使用 getByTestId
哦!
如果文章中有任何問題,或是不理解的地方,都可以留言告訴我!謝謝大家!
5.10的jest 疑似已經將toBeInTheDocument 加入斷言庫
在加入的情況下使用expect.extend({toBeInTheDocument})反而會錯
(待驗證)
你說對了!!!!
現在改成要去下載 @testing-library/jest-dom,然後不用再下 expect.extend
,但還是要記得從 @testing-library/jest-dom 裡面 import
斷言方法 toBeInTheDocument
剛剛在測試expect(getByTestId('counterBlock')).toBeInTheDocument()的時候,發現會出現類似toBeInTheDocument not found的錯誤,但嘗試以下方式有獲得解決。
在testing file前面下:
import '@testing-library/jest-dom/extend-expect';
資料來源: stack overflow: https://stackoverflow.com/questions/57861187/property-tobeinthedocument-does-not-exist-on-type-matchersany
因為我看這篇都是用 getByTestId 去查找 virtual dom,所以想討論個問題
react testing library 的 Queries API 語法其實有優先序,可以參考官方文件,所以這篇的範例優先使用 getByRole 等 Queries API 應該會更適合吧 ? 當然用 getByTestId 也不能說錯誤就是了,也可能是 2020 年當時主要是以 getByTestId 做測試為主