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

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

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

import

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

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

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

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

1. Component

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

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

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

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

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

2. Hooks

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

3. Redux/Slice

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

  • Action

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

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

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

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

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

4. Redux Selectors

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

5. Service

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

6. Utils/Helpers

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

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

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

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

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

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

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

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

2. 如何讓過程更簡單?

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

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

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

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

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

export

最后,為 TL;DR 的同學(xué)簡單總結(jié)一下:

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

參考資料:

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

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

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

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

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

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

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

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

  • 在這里說一下前端開發(fā)的一個特點是更多的會涉及用戶界面,當(dāng)開發(fā)規(guī)模達(dá)到一定程度時,幾乎注定了其復(fù)雜度會成倍的增長。 ...
    愛碼小士閱讀 6,162評論 1 4
  • 寫在前面 關(guān)于MVP關(guān)于MVP的介紹很多,這不是本文的重點,這里列舉近期一些比較好的文章。 Android官方MV...
    geniusmart閱讀 33,557評論 23 349
  • TL;DR——什么是好的單元測試? 其實我是個標(biāo)題黨,單元測試根本沒有“藝術(shù)”可言。 好的測試來自于好的代碼,如果...
    ThoughtWorks閱讀 3,307評論 1 22
  • 本文為實踐的產(chǎn)物,總結(jié)從搭建到使用過程中遇到的問題,初學(xué)者可以參考。 Jest 和 Enzyme 的基本介紹 測試...
    寧小姐的慢時光閱讀 4,697評論 0 4
  • 久違的晴天,家長會。 家長大會開好到教室時,離放學(xué)已經(jīng)沒多少時間了。班主任說已經(jīng)安排了三個家長分享經(jīng)驗。 放學(xué)鈴聲...
    飄雪兒5閱讀 7,868評論 16 22

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