iT邦幫忙

第 11 屆 iThome 鐵人賽

DAY 20
7

前言

今天要進入如何對 Component 做測試,這裡筆者之前是使用 Enzyme ,可能也是主流的 React 的測試框架,但是學測試的時候 React 剛改版到 v16,那時候 Enzyme 還沒辦法支援 Hooks 做測試,所以筆者就不小心跳槽到 React Testing Library ,想說在 Enzyme 支援 Hooks 前找個避風港,結果沒想到一到 React Testing Library 後就回不去了。

React Testing Library 的測試方式很特別,一開始使用時可能會有點疑惑,但最後越寫越倒吃甘蔗,甚至還推坑身邊的朋友使用,接下來換我用本篇文章推坑給大家了:)


前置準備

  1. 文中的專案會以 Day18 的專案架構繼續講解,如果未跟到前一天的進度,可以從 GitHub 上 Clone 下來。
  2. 一顆擁有學習熱忱的心。

使用方法

安裝 React Testing Library

npm install --save-dev @testing-library/react

受測 Component

這個部分和大家說明一下,因為目前寫下的 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

常用方法

撰寫測試前,先簡單說明幾個常用的方法:

render

react-testing-library 的 render 會將所有的子組件都 Render 出來成為 Dom 節點。

getByTestId

render 後會回傳的方法,兩個都是用來搜尋 Dom,getByTestId 是以 Dom 中的 data-testid 值取要斷言的 Dom,獲取到 Dom 後便能以 textContent 屬性取得其內容。

container

container 也是 render 所回傳的,等於取得整包 Dom 物件,甚至是能夠直接對它使用 querySelector 來搜尋節點,通常我會在搞不清楚到底 Render 了什麼的時候,用 innerHTML 來偷看,其實是滿少用的。

fireEvent

這個 Method 可以觸發 Dom 的事件,例如 onClickonChange 等等。

開始測試

進入到測試後,讓我們為 Component 的測試案例建立一個新資料夾 component,並將要測試的 Component 建立對應的同名測試的路徑,例如上方將要用 src/component/Counter 做測試,那測試的文件就放到__tests__/component/Counter:

|-__tests__
 |-component
  |-Counter
   |-Counter.test.jsx

Counter.test.jsx 就是我們要寫下測試 Counter 的檔案了。

根據上方那幾個常用方法,第一步先在測試案例中將要測試的 Component Counter 用 @testing-library/react 的 rendergetByTestId 取出來:

因為 getByTestId 只能找到被標記 data-testid 屬性的 DOM,因此我們要回到 Counter 中,替裡面的 divbuttonspan 都加上 data-testid

但是這裡有些觀念需要先釐清,那就是我們該測試什麼?

而我們該測試什麼,取決於 Component 的行為。

Counter 為例,它的功能就是使用者會看見畫面上出現按鈕及目前點擊次數的文字,且點擊按鈕時會讓顯示的次數加一,因此我們的測試案例要寫下的內容有兩項:

  1. 確認 Counter 有沒有正常 render。
  2. 點擊按鈕是否會讓畫面上的顯示次數加一。

只要明白的知道 Component 在做什麼,就能夠寫好測試,相對來說,如果覺得某個 Component 或是 Function 的測試很難寫,就代表設計層面上可能出了點問題(包含違反單一職責等等)。

既然曉得我們要做什麼,那需要加上 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 取得要觸發的按鈕,接著再將按鈕放進 fireEventclick 觸發(如果是 change 的話,可以在 fireEvent 放入第二個參數,就是要改變的值)。

那既然現在可以點擊按鈕了,就要取出 span,並用 textContent 取得其內容,驗證數字是否有隨著點擊而加一,因為 textContent 取出來是字串,所以就直接使用 toBe 做斷言驗證:

沒問題的話執行測試結果應該會 PASS:

最後提一下 getByTestId 的部分,應該會有很多人疑惑,Dom 身上有太多東西可以參考了,像是 ClassName 或是上方說的以 container 配合 querySelector 把 Dom 取出來,為什麼偏偏要使用 data-testid 加上 getByTestId 這種看似多餘的步驟來測試,其實有個主要的原因:

為了讓測試適應程式碼的變化。

舉個簡單的例子,假設在測試案例中我使用了 container 加上 querySelector,尋找某個 ClassName,然後也順利將 Dom 給取出,順利測試。

但是如果有一天,那個 Dom 不再需要該 ClassName,你也沒辦法將它刪除,因為測試案例依賴著 ClassName,即使它不再使用以及沒任何意義。

因此替固定行為的 Dom 設定不會容易改變的 data-testid,也就間接守護了測試的可維護性。

沒有可維護性的測試,只要改了一些 Dom(例如:調換兩個按鈕的排列順序,或是將它們移動到另一個 div 中),就要接受原本正常的測試案例出錯,就算 Component 本身的行為沒有改變也一樣,要維護的程式碼便成了兩倍,最後就會讓人越來越不想繼續編寫測試,是放棄測試最常見的原因之一。

覆蓋率 Coverage

接下來要說明的是覆蓋率,他能提供給我們一個測試的指標,

覆蓋率的算法為,測試案例所測試到的程式碼,與確實執行驗證斷言的比例。

如果要在測試後產生覆蓋率報告,得在 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 哦!

如果文章中有任何問題,或是不理解的地方,都可以留言告訴我!謝謝大家!


上一篇
Day18 | 用 Mock 打造國家機器,驗證函式執行 feat. jest
下一篇
Day20 | Component 被 Redux 罩著怎麼測試?
系列文
在 React 生態圈內打滾的一年 feat. TypeScript31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中
1
連城
iT邦新手 4 級 ‧ 2020-06-17 20:21:26

5.10的jest 疑似已經將toBeInTheDocument 加入斷言庫
在加入的情況下使用expect.extend({toBeInTheDocument})反而會錯
(待驗證)

神Q超人 iT邦研究生 5 級 ‧ 2020-06-18 14:05:46 檢舉

你說對了!!!!
現在改成要去下載 @testing-library/jest-dom,然後不用再下 expect.extend,但還是要記得從 @testing-library/jest-dom 裡面 import 斷言方法 toBeInTheDocument

0
kika
iT邦新手 4 級 ‧ 2021-07-06 00:14:51

剛剛在測試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

0
harry xie
iT邦研究生 1 級 ‧ 2022-09-10 20:03:55

因為我看這篇都是用 getByTestId 去查找 virtual dom,所以想討論個問題

react testing library 的 Queries API 語法其實有優先序,可以參考官方文件,所以這篇的範例優先使用 getByRole 等 Queries API 應該會更適合吧 ? 當然用 getByTestId 也不能說錯誤就是了,也可能是 2020 年當時主要是以 getByTestId 做測試為主

我要留言

立即登入留言