跳至主要内容

[Day25] 測試一定要寫好寫滿?時間有限怎麼辦?

既然要寫測試,就先來了解前端常見的幾種測試類型,從最大家最常聽到的單元測試(Unit Testing)、到會整合不同 API 或元件互動的整合測試(Integration Testing),最後則是模擬使用者操作的 End to End Testing。

不同測試類型會搭配不同的測試工具,以單位測試來說,目前在前端最常用的是由 Facebook 推出的 Jest,它是 Node-based 的執行器,主要用來進行元件或函式的單元測試(unit test);若有需要針對頁面上的元素或 React 元件進行整合測試,筆者自己習慣用的是 test-library 上的 react-testing-library。最後,若是需要的進行更貼近瀏覽器環境的 end-to-end 測試的話,則可以使用 cypresspuppeteer

測試的不同類型

根據不同的測試目的,會分成不同的測試類型,簡單來說可以分成三類,但筆者認為這三類的區隔並非一定壁壘分明的,是其中一種就不會是另一種。不論是哪一類型的測試,核心概念都是透過「預期結果(expect)」和「真實結果」去做比對,看看得到的真實結果是不是如同開發者所預期的,因此,如果你連預期會是如何都無法想像的話,那是完全沒辦法進行測試的

單元測試(Unit Test)

測試的對象會是程式碼中最小的單元,通常會是自己撰寫的函式(function)、方法(method)、類別(classes)等等,也就是你預期這個 input 進去後,應該會得到什麼樣的 output。舉例來說,根據 LeetCode 的題目寫出 function 後,LeetCode 就會對你寫的 function 做許多的驗證,確保你寫的 function 在各種情況下都能滿足題目的需求,而針對個別 function 進行測試的情況就是所謂的單元測試(Unit Test)。

這部分前端來說最常使到的工具是由 Facebook 推出的 Jest;後端則很常使用 mochajs 搭配 chai

整合測試(Integration Test)

整合測試顧名思義就是需要「整合」,這表示測試的過程不是單一函式就能滿足,過程中可能會需要呼叫 API 獲取資料、使用其他的 library、或者和 DOM 進行整合,預期 DOM 上應該會呈現特定的 element。以 React Component 的測試來說,就比較接近這類型的測試,因為在 React Component 中,可能會去 fetch API 取得資料,取得資料後需要將資料呈現在 DOM 上。這時候如果是撰寫整合測試的話,就需要寫 mock data 來假設 API 回傳的資料內容,並在取得假資料後,檢測 DOM 有沒有如同預期的呈現出 element;這個過程中,也可以以程式的方式模擬使用者點擊、輸入內容的動作。

這部分以 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

不同測試類型的考量

不同類型的測試自然對應到不同的使用時機和情境,也不是說總是把測試補到最齊最滿一定是最好的做法,簡單來說,資源有限而測試案例常常是無窮的,為了在有限的資源中(開發能力、開發時程)能確保程式碼的品質,取捨要做哪些測試往往是更實際的。

在上述不同的測試類型中,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

另外,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 時意想不到的情況。

Testing Pyramid

上面的這些概念可以整理成一個經典的「測試金三角(Testing Pyramid)」,上面提到三種不同的測試類型它們分別位於該三角形中的不同位置,在三角形中越上方所需耗費的成本越高、執行的時間通常會更長,且需求有任何變動通常都需要修改測試,但卻更有機會找到意料之外的錯誤。

testing pyramid

圖片來源:Automation Panda

時間有限該先寫哪一種?

以筆者的角度來說,並非所有的 function 或 component 都一定要寫一個對應的測試,畢竟時間和資源有限的情況下勢必要有所取捨。當產品還沒開發出來,下個月的薪水都還沒著落時,卻還在一一寫每個 function 或 component 的 test case 時,自己可能也很難安心。

在時間資源有限的情況下,我會鼓勵先做「單元測試」,它們對於程式可維護性的提升都能有相當的幫助,特別是會被多個不同開發者或多個不同模組使用到的共用函式,昨天提到,撰寫測試的好處包括「測試程式本身就有輔助文件的效果」,因為在測試的程式中提供了許多範例讓其他開發者可以更好理解這個方法的使用,此外未來若有重構或需求變更時,可以讓你自己在程式碼在改動後保有一定程度的信心。

除了單元測試外,若你的應用程式本身包含相當複雜的商業邏輯,這些複雜的商業邏輯,不是單純那種「使用者登入後就看不到『登入按鈕』」這麼直覺的邏輯,而是其他人看了程式碼後第一眼可能也無法理解為什麼的部分,例如,使用者看到的價格取決於使用者的等級、年齡、性別等等,一般人第一時間也無法馬上理解的商業邏輯,這種則會很建議可以使用整合測試或 E2E 測試來保護起來。如此,一開始開發好後就可以用測試檢驗自己是否有依照規格正確實作了商業邏輯,提升對自己程式的信心,再來則是在未來功能添加、需求改動、或是程式重構時,不會因為不小心忽略而改掉了原本的邏輯而不自知。

參考資料