React要寫單元測試?不妨這樣試試

長期以來,單元測試 (Unit testing/UT) 都是前端項目工程化繞不開的一個重點。而近兩年隨著越來越多更為便利的測試框架、工具的出現(xiàn),端到端測試(End-to-end testing/E2E)在項目實踐中的存在感也越來越強,面對E2E的“蠶食”,我們不得不思考,編寫UT最合理的“度”在哪里?另一方面,過去一年多,React Hooks強勢崛起,大家的編碼習慣在潛移默化中發(fā)生了不小的改變,與之相對,UT的實踐策略也應不斷進行調(diào)整,如何在React項目中落地單元測試,是一個很值得深入的話題。

對于Java、C#等后端語言,UT的實踐策略已經(jīng)比較成熟,而在前端領域,UT始終處于一個復雜且細分的階段。翻閱社區(qū)的眾多資料,UT在不同項目中的實踐方式可謂是五花八門,大家嘗試套用后端語言的成熟經(jīng)驗,然而在落地時,又會遇到很多水土不服的問題,讓執(zhí)行變得異常艱難。這篇文章,正是想要提供一種思路,以解決問題為導向,嘗試找到一個初步的實踐策略。

import

論述UT重要性或是講解如何實踐測試驅動開發(fā)(Test-Driven Development/TDD)的文章已經(jīng)很多[1],此處不再贅述,本文主要針對以下幾點問題進行討論:

  1. React組件化后,組件哪部分最具測試價值?
  2. 如何讓我們的測試用例更易編寫、維護?
  3. UT與E2E的邊界在哪里?

帶著這些問題,我們從React組件本身出發(fā),一探究竟。

一、React組件哪部分最具測試價值?

1. Component

Component 應著重關注render以及副作用,同時業(yè)務邏輯的處理過程,都應該盡量提取到Hooks和Utils文件中。因此,對于Component的測試,我們完全可以將重心主要放在以下這兩方面問題上:

  • 組件是否正常渲染了?
  • 組件副作用是否正常處理了?

在嘗試使用UT對這兩個關注點進行覆蓋時,首先就要面臨一個比較棘手的問題:開發(fā)人員需要mock整個組件渲染所需的所有數(shù)據(jù),包括且不限于Redux store中的state和所有的Props,如此才可保證組件能夠被正確渲染且覆蓋符合預期。而這就意味著開發(fā)人員在編寫測試用例時不得不耗費很大一部分精力mock數(shù)據(jù),且在開發(fā)后期,極有可能為了頁面新增的一個字段,開發(fā)人員卻需要花費不小的工時對mock數(shù)據(jù)進行維護。

反觀E2E,由于其并不關注程序的內(nèi)部實現(xiàn),因而在覆蓋并解決上述兩方面問題的同時,相對輕松地避開了UT所遭遇的痛點,故此我們更傾向于將基礎組件渲染這部分內(nèi)容的測試工作移交給E2E來負責,UT只需要關注帶有復雜顯示邏輯的組件。

同理,如果你的組件內(nèi)部包含復雜的渲染邏輯,你依然可以使用UT對其進行覆蓋,我們推薦使用react-testing-library來加載組件,mock接口之后,再寫一個類似E2E的集成測試。當然使用Enzyme直接進行render的測試也是個不錯的方案。

2. Hooks

如何測試React Hooks,社區(qū)目前已有相對成熟的解決方案,即@testing-library/react-hooks + react-test-renderer[2]。通過這兩個依賴,開發(fā)人員可以很輕松的mock出Hooks執(zhí)行所依賴的環(huán)境,把store的數(shù)據(jù)當作hooks的輸入,關注在hooks內(nèi)的業(yè)務邏輯,即可把Hooks當作純方法(Pure Function)來進行測試。

3. Redux/Slice

對于Redux,如果項目在使用 Redux Toolkit 的話,事情會簡單很多,開發(fā)人員只需要關注Dispatch的Actions即可。但如果Actions和Reducer是分開編寫,則需要針對性處理:

  • Action

對于Action creator,雖然官網(wǎng)展示了對應的測試用例形式,但是大多數(shù)情況,這一部分都是類似的模版代碼:

const orderLoading = () => ({ type: 'ORDER_LOADING' });

針對這類代碼鋪測試用例,唯一的效果只會是增加開發(fā)人員復制粘貼的工作量。這部分測試用例真正需要關注的,應是dispatch的那一部分代碼邏輯:我們對actions的dispatch是否符合預期?對Service返回數(shù)據(jù)的處理是否符合預期?諸如此類。

export const getOrderById = (orderId: string): AppThunk => async (dispatch) => {
  try {
    dispatch(orderLoading);
    const orders = await requestOrderAPI([mockOrder]);
    dispatch(addOrders(orders));
  } catch (error) {
    dispatch(orderLoadingError(error));
  }
};

  • Reducer

由于所有對于store state的操作,都應該放在action中來完成,因而大多數(shù)情況下,Reducer都是模版代碼。確實對于這類純函數(shù),編寫測試用例會輕松很多,但就實際情況而言,大部分的這類模板代碼都沒有測試的必要。

當然,如果reducer中還包含了對state的邏輯處理,甚至于涉及業(yè)務的分支邏輯,UT覆蓋還是很有價值的。

4. Redux Selectors

不同應用場景中,Selectors的復雜程度可高可低。若Selectors只是簡單且直接地返回store中存儲的某項數(shù)據(jù)時,不需要UT覆蓋;然而若涉及數(shù)據(jù)聚合、清洗等邏輯操作時,UT覆蓋不能偷懶。

5. Service

不同項目或團隊對Service的定義各不相同,這里我們要聊的主要指負責處理HTTP請求的request和response,以及相應的異常處理的數(shù)據(jù)層。Service主要的功能是對接Action,因而理想情況下Service只需要包含與API通信的代碼,這種情況下,UT可有可無。但一些場景下,如果項目中沒有使用BFF承擔數(shù)據(jù)處理的角色,后端也沒能提供完全符合前端數(shù)據(jù)結構需求的接口時,不可避免的,開發(fā)人員需要在此處完善數(shù)據(jù)處理的邏輯,以便獲取清洗或聚合后的數(shù)據(jù),因而這種情況下,UT覆蓋是非常有必要的。

6. Utils/Helpers

Utils/Helpers主要包含以下幾類類型:

  • 數(shù)據(jù)結構的轉化,各種convert工具函數(shù)
  • 數(shù)據(jù)結構的處理,比如數(shù)據(jù)提取、合并壓縮、整理工具函數(shù)
  • 公共的工具函數(shù)

根據(jù)我們目前的項目習慣,當一段邏輯需要在Utils/Helpers中實現(xiàn)時,那么它一定是純函數(shù),其中多數(shù)情況又會包含一定程度的數(shù)據(jù)處理邏輯,所以基本都需要UT覆蓋。

二、如何讓我們的測試用例更易編寫、維護?

回答這個問題,我們需要先思考一下,什么樣的測試用例編寫起來最輕松?答案可能因人而異,但輸入輸出簡單明了的純函數(shù)一定能算上一個。從這個觀點出發(fā),結合黑盒測試的特性,我們可以將這個問題拆分為以下兩點:

1. 如何讓輸入輸出更清晰?

這個問題,說到底是管理mock數(shù)據(jù)的問題。隨著項目的不斷膨脹,組織mock數(shù)據(jù)會逐漸成為編寫UT時負擔最重的那個環(huán)節(jié)。隨手mock在項目前期可能會稍顯方便,但這無異于給自己挖坑。

最直接的解決方案還是首選集中管理mock數(shù)據(jù):項目中可以考慮集中維護一個DTO mock集合,其中提供不同類型的Base DTO mock數(shù)據(jù),由各個測試用例在使用時按需導入,再在其內(nèi)部轉化成他所需要的數(shù)據(jù),具體實現(xiàn)方式可因項目而異,在搭建出框架后,通過使用的方式來進一步明確項目中的需求,進行調(diào)整。

2. 如何讓過程更簡單?

要回答這個問題,既“簡單”又“困難”,因為答案的核心很明確,即降低代碼的深度和復雜度,控制代碼分支數(shù)量,如此這般在一定程度上減少測試用例。但在實際場景中,無論是編碼水平有限,項目框架限制還是需求時限要求,總有各種各樣“合理”的理由阻礙開發(fā)人員將代碼寫得簡單。這種情況下,不妨多了解一些關于TDD的實踐方法,在避免形式主義的前提下,結合項目情況,嘗試改變一些既定的編碼習慣。同時,有舍有得,根據(jù)F.I.R.S.T.原則[3],對已有UT測試用例進行優(yōu)化和重構。

三、UT與E2E的邊界在哪里?

在實踐E2E的過程中,我們意識到為了提高E2E的可維護性及測試用例的運行效率,E2E的關注點應更側重于從更高的維度,對于項目整體的流水線進行測試,而非過分關注具體的細節(jié),如某一個按鈕的顯隱。

且隨著E2E測試用例數(shù)量的增加,在維護的過程中,只有不斷進行精簡與合并,逐漸刪減掉那些過于獨立的測試用例,并將不同環(huán)節(jié)的獨立測試用例串聯(lián)為完整的流程,如此才能保證E2E的健壯。

因此,顯而易見的,UT與E2E在編寫或維護過程中,確實存在重疊的可能性,但它們最終形態(tài)的關注點卻是完全不同的,而關注點的差異,正是其邊界所在。

export

最后,為 TL;DR 的同學簡單總結一下:

  • UT應關注代碼中最具測試價值的部分,以盡可能小的成本換取最大化的收益
  • 測試價值取決于項目本身的側重點及開發(fā)人員的編碼習慣,這里提供一種思路供參考:
    • Component:應覆蓋包含復雜顯示邏輯的組件,除此之外可以不覆蓋
    • Hooks: 應全部覆蓋
    • Redux:應覆蓋action函數(shù),及包含數(shù)據(jù)處理邏輯的reducer函數(shù),除此之外可以不覆蓋
    • Selectors:應覆蓋包含數(shù)據(jù)處理邏輯的函數(shù),除此之外可以不覆蓋
    • Service:應覆蓋包含數(shù)據(jù)處理邏輯的函數(shù),除此之外可以不覆蓋
    • Utils/Helpers:應全部覆蓋
  • 確定關注點,同時通過對測試用例不斷的分解和組合,在實踐中明確UT及E2E的邊界

參考資料:

  1. React單元測試策略及落地

  2. testing-library/react-hooks-testing-library

  3. Agile in a Flash - F.I.R.S.T

“卓派前端工作志,聚焦實用前端技術,讓編程更有趣!”

前端技術組 @ 西安卓派科技 NEXT Trucking — 拉勾 | Boss | 知乎 | 掘金 | 簡書

如果覺得本文對你有幫助的話,快來關注我們吧!

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容