長期以來触创,單元測試 (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)行討論:
- React組件化后弹谁,組件哪部分最具測試價值乾巧?
- 如何讓我們的測試用例更易編寫、維護(hù)预愤?
- 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)行精簡與合并蚯妇,逐漸刪減掉那些過于獨(dú)立的測試用例敷燎,并將不同環(huán)節(jié)的獨(dú)立測試用例串聯(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的邊界
參考資料:
“卓派前端工作志,聚焦實用前端技術(shù)祥楣,讓編程更有趣开财!”
前端技術(shù)組 @ 西安卓派科技 NEXT Trucking — 拉勾 | Boss | 知乎 | 掘金 | 簡書
如果覺得本文對你有幫助的話,快來關(guān)注我們吧荣堰!