[note] 前端測試的概念與類型
React 本身是基於 JavaScript 的前端框架,因此在實際針對 React 元件進行測試前,需要先暸解在 JavaScript 中常用的測試套件有哪些。另外,測試的種類也非常多,從最大家最常聽到的 Unit Testing、到會整合不同 API 或元件互動的 Integration Testing,最後則是模擬使用者操作的 End to End Testing。
Jest 是 Node-based 的執行器,主要是用來進行元件的單元測試(unit test)而非 DOM 本身,若有需要針對 React 元件進行整合測試,檢驗 DOM 渲染的結果是否正確,可 以使用 react-testing-library。但若是需要的進行更貼近瀏覽器環境的 end-to-end 測試的話,則可以使用 cypress 或 puppeteer。
測試的不同類型
根據不同的測試目的,會分成不同的測試類型,簡單來說可以分成三類,但我認為這三類的區隔並一定壁壘分明的,是其中一種就不會是另一種。不論是哪一類型的測試,概念都是透過「預期結果(expect)」和「真實結果」去做比對,看看得到的真實結果是不是如同開發者所預期的,因此,如果你連預期會是如何都無法想像的話,那是完全沒辦法進行測試的。
單元測試(Unit Test)
測試的對象會是程式碼中最小的單元,通常會是自己撰寫的函式(function)、方法(method)、類別(classes)等等,也就是你預期這個 input 進去後,應該會得到什麼樣的 output。舉例來說,根據 LeetCode 的題目寫出 function 後,LeetCode 就會對你寫的 function 做許多的驗證,確保你寫的 function 在各種情況下都能滿足題目的需求,而針對個別 function 進行測試的情況就是所謂的單元測試(Unit Test)。
優點
在單元測試中,會盡可能 mock 相依的套件,因此較容易指出導致錯誤產生的位置。
缺點
它的缺點就是和實際上使用者在操作軟體的互動方式差距較遠,因此有可能:
- 單元測試通過了,但使用者仍然無法順利使用 Application 的情況,或是相反過來的情況
- 相較於 Integration Tests,Unit Tests 在 refactor 程式碼的時候更容易報錯
工具
這部分前端來說最常使到的工具是由 Facebook 推出的 Jest;後端則很常使用 mochajs 搭配 chai。
整合測試/功能測試(Integration Test / Functional Testing)
整合測試顧名思義就是需要「整合」,這表示測試的過程不是單一函式就能滿足,過程中可能會需要呼叫 API 獲取資料、使用其他的 library、或者和 DOM 進行整合,預期 DOM 上應該會呈現特定的 element。
以 React Component 的測試來說,就比較接近這類型的測試,因為在 React Component 中,可能會去 fetch API 取得資料,取得資料後需要將資料呈現在 DOM 上。這時候如果是撰寫整合測試的話,就需要寫 mock data 取假設 API 回傳的資料內容,在取得內容後,檢測 DOM 有沒有如同預期的呈現出 element;這個過程中,也可以以程式的方式模擬使用者點擊、輸入內容的動作。
優點
- Integration Testing 較接近使用者實際使用 App 的操作和互動方式
- 只要 User Flow 沒有改變,即是改了程式碼,測試也不會壞掉
缺點
- 可能較難立即發現導致錯誤的原因
工具
這部分以 React 來說最常提到的應該是 Testing Library 搭配 React Testing Library;或 enzyme。
End-to-end (E2E) tests
相較於 Unit Test 是測試單一邏輯、Integration Test 是測試整合多個邏輯下的情況、End-to-end (E2E) 則算是最模擬使用者操做實際產品的過程,透過 E2E Test, 你可以撰寫使用者操作的流程,並可以透過一個瀏覽器的畫面實際看到頁面被操作的過程,你可以想像成就是有一個使用者真的打開了瀏覽器,從瀏覽器輸入網址,接著進入網頁後進行後續對應的流程。
這部分前端最常聽到的是 Cypress、Google 推出的 Puppeteer、或是微軟的 Playwright。
不同測試類型的考量
不同類型的測試自然對應到不同的使用時機和情境,也不是說總是把測試補到最齊最滿一定是最好的做法,簡單來說,資源有限而測試案例常常是無窮的,為了在有限的資源中(開發能力、開發時程)能確保程式碼的品質,取捨要做哪些測試往往是更實際的。
在上述不同的測試類型中,Unit Test 可以算是第一道防線,也是通過測試後,比較不會因為其他功能更動而會壞掉的。你可以想像有一個用來驗證表單欄位是否為空的 function,這個 function 的驗證邏輯一旦寫好後,並不會欄位名稱不同、或表單的 UI 改動後,驗證的邏輯就有不同。
但以同樣確認表單欄位必填的功能來說,如果你做的是 integration test 或 E2E test,就有可能因為畫面改變、後端 API 回傳的資料改變,而導致測試結果失敗,因為在 integration test 或 E2E test 中,除了會想要需要驗證使用者該欄位是否漏填外,可能還會同時檢查漏填時,畫面應該要跳出的提示訊息。這時候, 一旦 UI 修改後,最後提示的文字內容有異動(例如,原本顯示 Please enter your name
,UI 修改後希望顯示 Require to enter your name
),或者是 API 回傳的資料有變更時,都可能導致 integration test 和 E2E test 有錯誤。
另外,Unit Test 作為第一道防線,也表示這通常是最容易找到問題「核心」的地方。舉例來說,如果想要驗證的是登入功能,使用的是 E2E Test,這時當 E2E Test 失敗,但若沒有搭配 Unit Test 的話,你就會像一般的使用者一樣只知道無法成功登入,但卻不知道為什麼不能登入;但若有搭配 Unit Test 的話,則會比較容易發現無法成功登入的原因。
不同的測試除了容易發現的問題不同之外,執行的時間(execution time)也不同,E2E Test 通常在測試上會花上的時間也做多,投入的開發成本(Development Cost)也最高,因為只要設計或 UI 一有變動,E2E Test 必然會需要修改。
然而,雖然 E2E Test 的開發成本、投入時間相對來說都比 Unit Test 來得高,但這並不表示 E2E Test 就不重要或不值得都入,因為很多時候,使用者在操作時之所以會碰到問題,是因為各個模組之間的交互作用導致,也就是個別模組獨立運作時是沒問題的,但一但整合再一起就有開發者意想不到的情況;又或者,是使用者實際的操作下可能產生的問題,例如,使用者因為忘了某些資訊而先按了上一頁後,接著在回到下一頁(原本的頁面)時,可能會因為快取等狀況而發生一些在做 Unit Test 時意想不到的情況。