摘要:寫好的單元測(cè)試,對(duì)開發(fā)速度項(xiàng)目維護(hù)有莫大的幫助。我認(rèn)為單元測(cè)試的上下文存在于敏捷中。接下來(lái)一小節(jié),就可以正式進(jìn)入如何做的環(huán)節(jié)了如何寫好單元測(cè)試。前面說到,我們對(duì)單元測(cè)試寄予
寫好的單元測(cè)試,對(duì)開發(fā)速度、項(xiàng)目維護(hù)有莫大的幫助。前端的測(cè)試工具一直推陳出新,而測(cè)試的核心、原則卻少有變化。與產(chǎn)品代碼一并交付可靠的測(cè)試代碼,是每個(gè)專業(yè)開發(fā)者應(yīng)該不斷靠近的一個(gè)理想之地。本文就圍繞測(cè)試講講,為什么我們要做測(cè)試,什么是好的測(cè)試和原則,以及如何在一個(gè) React 項(xiàng)目中落地這些測(cè)試策略。
本文使用的測(cè)試框架、斷言工具是 jest。文章不打算對(duì)測(cè)試框架、語(yǔ)法本身做過多介紹,因?yàn)橐延泻芏辔恼?。本文假定讀者已有一定基礎(chǔ),至少熟悉語(yǔ)法,但并不假設(shè)讀者寫過單元測(cè)試。在介紹什么是好的單元測(cè)試時(shí),我會(huì)簡(jiǎn)單介紹一個(gè)好的單元測(cè)試的結(jié)構(gòu)。目錄Github 討論:https://github.com/linesh-sim...
原文地址:https://blog.linesh.tw/#/post...
為什么要做單元測(cè)試
單元測(cè)試的上下文
測(cè)試策略:測(cè)試金字塔
如何寫好單元測(cè)試:好測(cè)試的特征
有且僅有一個(gè)失敗的理由
表達(dá)力極強(qiáng)
快、穩(wěn)定
React 單元測(cè)試策略及落地
React 應(yīng)用的單元測(cè)試策略
actions 測(cè)試
reducer 測(cè)試
selector 測(cè)試
saga 測(cè)試
來(lái)自官方的錯(cuò)誤姿勢(shì)
正確姿勢(shì)
component 測(cè)試
業(yè)務(wù)型組件 - 分支渲染
業(yè)務(wù)型組件 - 事件調(diào)用
功能型組件 - children 型高階組件
utils 測(cè)試
總結(jié)
未盡話題 & 歡迎討論
為什么要做單元測(cè)試雖然關(guān)于測(cè)試的文章有很多,關(guān)于 React 的文章也有很多,但關(guān)于 React 應(yīng)用之詳細(xì)單元測(cè)試的文章還比較少。而且更多的文章都更偏向于對(duì)工具本身進(jìn)行講解,只講「我們可以這么測(cè)」,卻沒有回答「我們?yōu)槭裁匆@么測(cè)」、「這么測(cè)究竟好不好」的問題。這幾個(gè)問題上的空白,難免使人得出測(cè)試無(wú)用、測(cè)試成本高、測(cè)試使開發(fā)變慢的錯(cuò)誤觀點(diǎn),導(dǎo)致在「質(zhì)量?jī)?nèi)建」已漸入人心的今日,很多人仍然認(rèn)為測(cè)試是二等公民,是成本,是錦上添花。這一點(diǎn)上,我的態(tài)度一貫鮮明:不僅要寫測(cè)試,還要把單元測(cè)試寫好;不僅要有測(cè)試前移質(zhì)量?jī)?nèi)建的意識(shí),還要有基于測(cè)試進(jìn)行快速反饋快速開發(fā)的能力。沒自動(dòng)化測(cè)試的代碼不叫完成,不能驗(yàn)收。
「為什么我們需要做單元測(cè)試」,這是一個(gè)關(guān)鍵的問題。每個(gè)人都有自己關(guān)于該不該做測(cè)試、該怎么做、做到什么程度的看法,試圖面面俱到、左右逢源地評(píng)價(jià)這些看法是不可能的。我們需要一個(gè)視角,一個(gè)談?wù)搯卧獪y(cè)試的上下文。做單元測(cè)試當(dāng)然有好處,但本文不會(huì)從有什么好處出發(fā)來(lái)談,而是談,在我們?cè)谝獾倪@個(gè)上下文中,不做單元測(cè)試會(huì)有什么問題。
那么我們談?wù)搯卧獪y(cè)試的上下文是什么呢?不做單元測(cè)試我們會(huì)遇到什么問題呢?
單元測(cè)試的上下文先說說問題。最大的一個(gè)問題是,不寫單元測(cè)試,你就不敢重構(gòu),就只能看著代碼腐化。代碼質(zhì)量談不上,持續(xù)改進(jìn)談不上,個(gè)人成長(zhǎng)更談不上。始終是原始的勞作方式。
再說說上下文。我認(rèn)為單元測(cè)試的上下文存在于「敏捷」中。現(xiàn)代企業(yè)數(shù)字化競(jìng)爭(zhēng)日益激烈,業(yè)務(wù)端快速上線、快速驗(yàn)證、快速失敗的思路對(duì)技術(shù)端的響應(yīng)力提出了更高的要求:更快上線、更頻繁上線、持續(xù)上線。怎么樣衡量這個(gè)「更快」呢?那就是第一圖提到的 lead time,它度量的是一個(gè) idea 從提出并被驗(yàn)證,到最終上生產(chǎn)環(huán)境面對(duì)用戶獲取反饋的時(shí)間。顯然,這個(gè)時(shí)間越短,軟件就能越快獲得反饋,對(duì)價(jià)值的驗(yàn)證就越快發(fā)生。這個(gè)結(jié)論對(duì)我們寫不寫單元測(cè)試有什么影響呢?答案是,不寫單元測(cè)試,你就快不起來(lái)。為啥呢?因?yàn)槊看伟l(fā)布,你都要投入人力來(lái)進(jìn)行手工測(cè)試;因?yàn)闆]有測(cè)試,你傾向于不敢隨意重構(gòu),這又導(dǎo)致代碼逐漸腐化,復(fù)雜度使得你的開發(fā)速度降低。
再考慮到以下兩個(gè)大事實(shí):人員會(huì)流動(dòng),應(yīng)用會(huì)變大。人員一定會(huì)流動(dòng),需求一定會(huì)增加,再也沒有任何人能夠了解任何一個(gè)應(yīng)用場(chǎng)景。因此,意圖依賴人、依賴手工的方式來(lái)應(yīng)對(duì)響應(yīng)力的挑戰(zhàn)首先是低效的,從時(shí)間維度上來(lái)講也是不現(xiàn)實(shí)的。那么,為了服務(wù)于「高響應(yīng)力」這個(gè)目標(biāo),我們就需要一套自動(dòng)化的測(cè)試套件,它能幫我們提供快速反饋、做質(zhì)量的守衛(wèi)者。唯解決了人工、質(zhì)量的這一環(huán),效率才能穩(wěn)步提升,團(tuán)隊(duì)和企業(yè)的高響應(yīng)力才可能達(dá)到。
那么在「響應(yīng)力」這個(gè)上下文中來(lái)談要不要單元測(cè)試,我們就可以很有根據(jù)了,而不是開發(fā)爽了就用,不爽就不用這樣含糊的答案:
如果你說我的業(yè)務(wù)部門不需要頻繁上線,并且我有足夠的人力來(lái)覆蓋手工測(cè)試,那你可以不用單元測(cè)試
如果你說我是個(gè)小項(xiàng)目小部門不需要多高的響應(yīng)力,每天摸摸魚就過去了,那你可以不用單元測(cè)試
如果你說我不在意代碼腐化,并且我也不做重構(gòu),那你可以不用單元測(cè)試
如果你說我不在意代碼質(zhì)量,好幾個(gè)沒有測(cè)試保護(hù)的 if-else 裸奔也不在話下,腦不好還做什么程序員,那你可以不用單元測(cè)試
如果你說我確有快速部署的需求,但我們不 care 質(zhì)量問題,出回歸問題就修,那你可以不用單元測(cè)試
除此之外,你就需要寫單元測(cè)試。如果你想隨時(shí)整理重構(gòu)代碼,那么你需要寫單元測(cè)試;如果你想有自動(dòng)化的測(cè)試套件來(lái)幫你快速驗(yàn)證提交的完整性,那么你需要寫單元測(cè)試;如果你是個(gè)長(zhǎng)期項(xiàng)目有人員流動(dòng),那么你需要寫單元測(cè)試;如果你不想花大量的時(shí)間在記住業(yè)務(wù)場(chǎng)景和手動(dòng)測(cè)試應(yīng)用上,那么你就需要單元測(cè)試。
至此,我們從「響應(yīng)力」這個(gè)上下文中,回答了「為什么我們需要寫單元測(cè)試」的問題。接下來(lái)可以談下一個(gè)問題了:「為什么是單元測(cè)試」。
測(cè)試策略:測(cè)試金字塔上面我直接從高響應(yīng)力談到單元測(cè)試,可能有的同學(xué)會(huì)問,高響應(yīng)力這個(gè)事情我認(rèn)可,也認(rèn)可快速開發(fā)的同時(shí),質(zhì)量也很重要。但是,為了達(dá)到「保障質(zhì)量」的目的,不一定得通過測(cè)試呀,也不一定得通過單元測(cè)試?guó)啞?/p>
這是個(gè)好的問題。為了達(dá)到保障質(zhì)量這個(gè)目標(biāo),測(cè)試當(dāng)然只是其中一個(gè)方式,穩(wěn)定的自動(dòng)化部署、集成流水線、良好的代碼架構(gòu)、組織架構(gòu)的必要調(diào)整等,都是必須跟上的設(shè)施。我從未認(rèn)為單元測(cè)試是解決質(zhì)量問題的銀彈,多方共同提升才可能起到效果。但相反,也很難想象單元測(cè)試都沒有都寫不好的項(xiàng)目,能有多高的響應(yīng)力。
即便我們談自動(dòng)化測(cè)試,未必也不可能全部都是寫單元測(cè)試。我們對(duì)自動(dòng)化測(cè)試套件寄予的厚望是,它能幫我們安全重構(gòu)已有代碼、保存業(yè)務(wù)上下文、快速回歸。測(cè)試種類多種多樣,為什么我要重點(diǎn)談單元測(cè)試呢?因?yàn)?del>這篇文章主題就是談單元測(cè)試啊…它寫起來(lái)相對(duì)最容易、運(yùn)行速度最快、反饋效果又最直接。下面這個(gè)圖,想必大家都有所耳聞:
這就是有名的測(cè)試金字塔。對(duì)于一個(gè)自動(dòng)化測(cè)試套件,應(yīng)該包含種類不同、關(guān)注點(diǎn)不同的測(cè)試,比如關(guān)注單元的單元測(cè)試、關(guān)注集成和契約的集成測(cè)試和契約測(cè)試、關(guān)注業(yè)務(wù)驗(yàn)收點(diǎn)的端到端測(cè)試等。正常來(lái)說,我們會(huì)受到資源的限制,無(wú)法應(yīng)用所有層級(jí)的測(cè)試,效果也未必最佳。因此,我們需要有策略性地根據(jù)收益-成本的原則,考慮項(xiàng)目的實(shí)際情況和痛點(diǎn)來(lái)定制測(cè)試策略:比如三方依賴多的項(xiàng)目可以多寫些契約測(cè)試,業(yè)務(wù)場(chǎng)景多、復(fù)雜或經(jīng)?;貧w的場(chǎng)景可以多寫些端到端測(cè)試,等。但不論如何,整個(gè)測(cè)試金字塔體系中,你還是應(yīng)該擁有更多低層次的單元測(cè)試,因?yàn)樗鼈兂杀鞠鄬?duì)最低,運(yùn)行速度最快(通常是毫秒級(jí)別),而對(duì)單元的保護(hù)價(jià)值相對(duì)更大。
以上是對(duì)「為什么我們需要的是單元測(cè)試」這個(gè)問題的回答。接下來(lái)一小節(jié),就可以正式進(jìn)入如何做的環(huán)節(jié)了:「如何寫好單元測(cè)試」。
關(guān)于測(cè)試金字塔的補(bǔ)充閱讀:測(cè)試金字塔實(shí)戰(zhàn)。
如何寫好單元測(cè)試:好測(cè)試的特征寫單元測(cè)試僅僅是第一步,下面還有個(gè)更關(guān)鍵的問題,就是怎樣寫出好的、容易維護(hù)的單元測(cè)試。好的測(cè)試有其特征,雖然它并不是什么新的東西,但總需要時(shí)時(shí)拿出來(lái)溫故知新。很多時(shí)候,同學(xué)感覺測(cè)試難寫、難維護(hù)、不穩(wěn)定、價(jià)值不大等,可能都是因?yàn)閱卧獪y(cè)試寫不好所導(dǎo)致的。那么我們就來(lái)看看,一個(gè)好的單元測(cè)試,應(yīng)該遵循哪幾點(diǎn)原則。
首先,我們先來(lái)看個(gè)簡(jiǎn)單的例子,一個(gè)最簡(jiǎn)單的 JavaScript 的單元測(cè)試長(zhǎng)什么樣:
// production code const computeSumFromObject = (a, b) => { return a.value + b.value } // testing code it("should return 5 when adding object a with value 2 and b with value 3", () => { // given - 準(zhǔn)備數(shù)據(jù) const a = { value: 2 } const b = { value: 3 } // when - 調(diào)用被測(cè)函數(shù) const result = computeSumFromObject(a, b) // then - 斷言結(jié)果 expect(result).toBe(5) })
以上就是一個(gè)最簡(jiǎn)答的單元測(cè)試部分。但麻雀雖小,五臟基本全,它揭示了單元測(cè)試的一個(gè)基本結(jié)構(gòu):準(zhǔn)備輸入數(shù)據(jù)、調(diào)用被測(cè)函數(shù)、斷言輸出結(jié)果。任何單元測(cè)試都可以遵循這樣一個(gè)骨架,它是我們常說的 given-when-then 三段式。
為什么說單元測(cè)試說來(lái)簡(jiǎn)單,做到卻不簡(jiǎn)單呢?除了遵循三段式,顯然我們還需要遵循一些其他的原則。前面說到,我們對(duì)單元測(cè)試寄予了幾點(diǎn)厚望,下面就來(lái)看看,它如何能達(dá)到我們期望的效果,以此來(lái)反推單元測(cè)試的特征:
安全重構(gòu)已有代碼 -> 應(yīng)該有且僅有一個(gè)失敗的理由、不關(guān)注內(nèi)部實(shí)現(xiàn)
保存業(yè)務(wù)上下文 -> 表達(dá)力極強(qiáng)
快速回歸 -> 快、穩(wěn)定
下面來(lái)看看這三個(gè)原則都是咋回事:
有且僅有一個(gè)失敗的理由有且僅有一個(gè)失敗的理由,這個(gè)理由是什么呢?是 「當(dāng)輸入不變時(shí),當(dāng)且僅當(dāng)被測(cè)業(yè)務(wù)代碼功能被改動(dòng)了」時(shí),測(cè)試才應(yīng)該掛掉。為什么這會(huì)支持我們重構(gòu)呢,因?yàn)橹貥?gòu)的意思是,在不改動(dòng)軟件外部可觀測(cè)行為的基礎(chǔ)上,調(diào)整軟件內(nèi)部實(shí)現(xiàn)的一種手段。也就是說,當(dāng)我被測(cè)的代碼輸入輸出沒變時(shí),任我怎么倒騰重構(gòu)代碼的內(nèi)部實(shí)現(xiàn),測(cè)試都不應(yīng)該掛掉。這樣才能說是支持了重構(gòu)。有的單元測(cè)試寫得,內(nèi)部實(shí)現(xiàn)(比如數(shù)據(jù)結(jié)構(gòu))一調(diào)整,測(cè)試就掛掉,盡管它的業(yè)務(wù)本身并沒修改,這樣怎么支持重構(gòu)呢?不怪得要反過來(lái)罵測(cè)試成本高,沒有用。一般會(huì)出現(xiàn)這種情況,可能是因?yàn)槭窍葘懲甏a再補(bǔ)的測(cè)試,或者對(duì)代碼的接口和抽象不明確所導(dǎo)致。
另外,還有一些測(cè)試(比如下文要看到的 saga 官方推薦的測(cè)試),它需要測(cè)試實(shí)現(xiàn)代碼的執(zhí)行次序。這也是一種「關(guān)注內(nèi)部實(shí)現(xiàn)」的測(cè)試,這就使得除了業(yè)務(wù)目標(biāo)外,還有「執(zhí)行次序」這個(gè)因素可能使測(cè)試掛掉。這樣的測(cè)試也是很脆弱的。
表達(dá)力極強(qiáng)表達(dá)力極強(qiáng),講的是兩方面:
看到測(cè)試時(shí),你就知道它測(cè)的業(yè)務(wù)點(diǎn)是啥
測(cè)試掛掉時(shí),能清楚地知道業(yè)務(wù)、期望數(shù)據(jù)與實(shí)際輸出的差異
這些表達(dá)力體現(xiàn)在許多方面,比如測(cè)試描述、數(shù)據(jù)準(zhǔn)備的命名、與測(cè)試無(wú)關(guān)數(shù)據(jù)的清除、斷言工具能提供的比對(duì)等??湛跓o(wú)憑,請(qǐng)大家在閱讀后面測(cè)試落地時(shí)時(shí)常對(duì)照。
快、穩(wěn)定不快的單元測(cè)試還能叫單元測(cè)試嗎?一般來(lái)講,一個(gè)沒有依賴、沒有 API 調(diào)用的單元測(cè)試,都能在毫秒級(jí)內(nèi)完成。那么為了達(dá)到快、穩(wěn)定這個(gè)目標(biāo),我們需要:
隔離盡量多的依賴。依賴少,速度就快,自然也更穩(wěn)定
將依賴、集成等耗時(shí)、依賴三方返回的地方放到更高層級(jí)的測(cè)試中,有策略性地去做
測(cè)試代碼中不要包含邏輯。不然你咋知道是實(shí)現(xiàn)掛了還是你的測(cè)試掛了呢?
在后面的介紹中,我會(huì)將這些原則落實(shí)到我們寫的每個(gè)單元測(cè)試中去。大家可以時(shí)時(shí)翻到這個(gè)章節(jié)來(lái)對(duì)照,是不是遵循了我們說的這幾點(diǎn)原則,不遵循是不是確實(shí)會(huì)帶來(lái)問題。時(shí)時(shí)勤拂拭,莫使惹塵埃啊。
React 單元測(cè)試策略及落地 React 應(yīng)用的單元測(cè)試策略上個(gè)項(xiàng)目上的 React(-Native) 應(yīng)用架構(gòu)如上所述。它涉及一個(gè)常見 React 應(yīng)用的幾個(gè)層面:組件、數(shù)據(jù)管理、redux、副作用管理等,是一個(gè)常見的 React、Redux 應(yīng)用架構(gòu),也是 dva 所推薦的 66%的最佳實(shí)踐(redux+saga),對(duì)于不同的項(xiàng)目應(yīng)該有一定的適應(yīng)性。架構(gòu)中的不同元素有不同的特點(diǎn),因此即便是單元測(cè)試,我們也有針對(duì)性的測(cè)試策略:
架構(gòu)層級(jí) | 測(cè)試內(nèi)容 | 測(cè)試策略 | 解釋 |
---|---|---|---|
action(creator) 層 | 是否正確創(chuàng)建 action 對(duì)象 | 一般不需要測(cè)試,視信心而定 | 這個(gè)層級(jí)非常簡(jiǎn)單,基礎(chǔ)設(shè)施搭好以后一般不可能出錯(cuò),享受了架構(gòu)帶來(lái)的簡(jiǎn)單性 |
reducer 層 | 是否正確完成計(jì)算 | 對(duì)于有邏輯的 reducer 需要 100%覆蓋率 | 這個(gè)層級(jí)輸入輸出明確,又有業(yè)務(wù)邏輯的計(jì)算在內(nèi),天然屬于單元測(cè)試寵愛的對(duì)象 |
selector 層 | 是否正確完成計(jì)算 | 對(duì)于有較復(fù)雜邏輯的 selector 需要 100%覆蓋率 | 這個(gè)層級(jí)輸入輸出明確,又有業(yè)務(wù)邏輯的計(jì)算在內(nèi),天然屬于單元測(cè)試寵愛的對(duì)象 |
saga(副作用) 層 | 是否獲取了正確的參數(shù)去調(diào)用 API,并使用正確的數(shù)據(jù)存取回 redux 中 | 對(duì)于是否獲取了正確參數(shù)、是否調(diào)用正確的 API、是否使用了正確的返回值保存數(shù)據(jù)、業(yè)務(wù)分支邏輯、異常分支 這五個(gè)業(yè)務(wù)點(diǎn)建議 100% 覆蓋 | 這個(gè)層級(jí)也有業(yè)務(wù)邏輯,對(duì)前面所述的 5 大方面進(jìn)行測(cè)試很有重構(gòu)價(jià)值 |
component(組件接入) 層 | 是否渲染了正確的組件 | 組件的分支渲染邏輯要求 100% 覆蓋、交互事件的調(diào)用參數(shù)一般要求 100% 覆蓋、被 redux connect 過的組件不測(cè)、純 UI 不測(cè)、CSS 一般不測(cè) | 這個(gè)層級(jí)最為復(fù)雜,測(cè)試策略還是以「代價(jià)最低,收益最高」為指導(dǎo)原則進(jìn)行 |
UI 層 | 樣式是否正確 | 目前不測(cè) | 這個(gè)層級(jí)以我目前理解來(lái)說,測(cè)試較難穩(wěn)定,成本又較高 |
utils 層 | 各種幫助函數(shù) | 沒有副作用的必須 100% 覆蓋,有副作用的視項(xiàng)目情況自定 |
對(duì)于這個(gè)策略,這里做一些其他補(bǔ)充:
關(guān)于不測(cè) redux connect 過的組件這個(gè)策略。理由是成本遠(yuǎn)高于收益:要犧牲開發(fā)體驗(yàn)(搞起來(lái)沒那么快了),要配置依賴(配置 store、
關(guān)于 UI 測(cè)試這塊的策略。團(tuán)隊(duì)之前嘗試過 snapshot 測(cè)試,對(duì)它寄予厚望,理由是成本低,看起來(lái)又像萬(wàn)能藥。不過由于其難以提供精確快照比對(duì),整個(gè)工作的基礎(chǔ)又依賴于開發(fā)者盡心做好「確認(rèn)比對(duì)」這個(gè)事情,很依賴人工耐心又打斷日常的開發(fā)節(jié)奏,導(dǎo)致成本和收益不成正比。我個(gè)人目前是持保留態(tài)度的。
關(guān)于 DOM 測(cè)試這塊的策略。也就是通過 enzyme 這類工具,通過 css selector 來(lái)進(jìn)行 DOM 渲染方面的測(cè)試。這類測(cè)試由于天生需要通過 css selector 去關(guān)聯(lián) DOM 元素,除了被測(cè)業(yè)務(wù)外 css selector 本身就是掛測(cè)試的一個(gè)因素。一個(gè) DOM 測(cè)試至少有兩個(gè)原因可使它掛掉,并不符合我們上面提到的最佳實(shí)踐。但這種測(cè)試有時(shí)又確實(shí)有用,后文講組件測(cè)試時(shí)會(huì)專門提到,如何針對(duì)它制定適合的策略。
actions 測(cè)試這一層太過簡(jiǎn)單,基本都可以不用測(cè)試,獲益于架構(gòu)的簡(jiǎn)單性。當(dāng)然,如果有些經(jīng)常出錯(cuò)的 action,再針對(duì)性地對(duì)這些 action creator 補(bǔ)充測(cè)試。
export const saveUserComments = (comments) => ({ type: "saveUserComments", payload: { comments, }, })
import * as actions from "./actions" test("should dispatch saveUserComments action with fetched user comments", () => { const comments = [] const expected = { type: "saveUserComments", payload: { comments: [], }, } expect(actions.saveUserComments(comments)).toEqual(expected) })reducer 測(cè)試
reducer 大概有兩種:一種比較簡(jiǎn)單,僅一一保存對(duì)應(yīng)的數(shù)據(jù)切片;一種復(fù)雜一些,里面具有一些計(jì)算邏輯。對(duì)于第一種 reducer,寫起來(lái)非常簡(jiǎn)單,簡(jiǎn)單到甚至可以不需要用測(cè)試去覆蓋。其正確性基本由簡(jiǎn)單的架構(gòu)和邏輯去保證的。下面是對(duì)一個(gè)簡(jiǎn)單 reducer 做測(cè)試的例子:
import Immutable from "seamless-immutable" const initialState = Immutable.from({ isLoadingProducts: false, }) export default createReducer((on) => { on(actions.isLoadingProducts, (state, action) => { return state.merge({ isLoadingProducts: action.payload.isLoadingProducts, }) }) }, initialState)
import reducers from "./reducers" import actions from "./actions" test("should save loading start indicator when action isLoadingProducts is dispatched given isLoadingProducts is true", () => { const state = { isLoadingProducts: false } const expected = { isLoadingProducts: true } const result = reducers(state, actions.isLoadingProducts(true)) expect(result).toEqual(expected) })
下面是一個(gè)較為復(fù)雜、更具備測(cè)試價(jià)值的 reducer 例子,它在保存數(shù)據(jù)的同時(shí),還進(jìn)行了合并、去重的操作:
import uniqBy from "lodash/uniqBy" export default createReducers((on) => { on(actions.saveUserComments, (state, action) => { return state.merge({ comments: uniqBy( state.comments.concat(action.payload.comments), "id", ), }) }) })
import reducers from "./reducers" import actions from "./actions" test(` should merge user comments and remove duplicated comments when action saveUserComments is dispatched with new fetched comments `, () => { const state = { comments: [{ id: 1, content: "comments-1" }], } const comments = [ { id: 1, content: "comments-1" }, { id: 2, content: "comments-2" }, ] const expected = { comments: [ { id: 1, content: "comments-1" }, { id: 2, content: "comments-2" }, ], } const result = reducers(state, actions.saveUserComments(comments)) expect(result).toEqual(expected) })
reducer 作為純函數(shù),非常適合做單元測(cè)試,加之一般在 reducer 中做重邏輯處理,此處做單元測(cè)試保護(hù)的價(jià)值也很大。請(qǐng)留意,上面所說的單元測(cè)試,是不是符合我們描述的單元測(cè)試基本原則:
有且僅有一個(gè)失敗的理由:當(dāng)輸入不變時(shí),僅當(dāng)我們被測(cè)「合并去重」的業(yè)務(wù)操作不符預(yù)期時(shí),才可能掛掉測(cè)試
表達(dá)力極強(qiáng):測(cè)試描述已經(jīng)寫得清楚「當(dāng)使用新獲取到的留言數(shù)據(jù)分發(fā) action saveUserComments 時(shí),應(yīng)該與已有留言合并并去除重復(fù)的部分」;此外,測(cè)試數(shù)據(jù)只準(zhǔn)備了足夠體現(xiàn)「合并」這個(gè)操作的兩條 id 的數(shù)據(jù),而沒有放很多的數(shù)據(jù),形成雜音;
快、穩(wěn)定:沒有任何依賴,測(cè)試代碼不包含準(zhǔn)備數(shù)據(jù)、調(diào)用、斷言外的任何邏輯
selector 測(cè)試selector 同樣是重邏輯的地方,可以認(rèn)為是 reducer 到組件的延伸。它也是一個(gè)純函數(shù),測(cè)起來(lái)與 reducer 一樣方便、價(jià)值不菲,也是應(yīng)該重點(diǎn)照顧的部分。況且,稍微大型一點(diǎn)的項(xiàng)目,應(yīng)該說必然會(huì)用到 selector。原因我講在這里。下面看一個(gè) selector 的測(cè)試用例:
import { createSelector } from "reselect" // for performant access/filtering in React component export const labelArrayToObjectSelector = createSelector( [(store, ownProps) => store.products[ownProps.id].labels], (labels) => { return labels.reduce( (result, { code, active }) => ({ ...result, [code]: active, }), {} ) } )
import { labelArrayToObjectSelector } from "./selector" test("should transform label array to object", () => { const store = { products: { 10085: { labels: [ { code: "canvas", name: "帆布鞋", active: false }, { code: "casual", name: "休閑鞋", active: false }, { code: "oxford", name: "牛津鞋", active: false }, { code: "bullock", name: "布洛克", active: true }, { code: "ankle", name: "高幫鞋", active: true }, ], }, }, } const expected = { canvas: false, casual: false, oxford: false, bullock: true, ankle: false, } const productLabels = labelArrayToObjectSelector(store, { id: 10085 }) expect(productLabels).toEqual(expected) })saga 測(cè)試
saga 是負(fù)責(zé)調(diào)用 API、處理副作用的一層。在實(shí)際的項(xiàng)目上副作用還有其他的中間層進(jìn)行處理,比如 redux-thunk、redux-promise 等,本質(zhì)是一樣的,只不過 saga 在測(cè)試性上要好一些。這一層副作用怎么測(cè)試呢?首先為了保證單元測(cè)試的速度和穩(wěn)定性,像 API 調(diào)用這種不確定性的依賴我們一定是要 mock 掉的。經(jīng)過仔細(xì)總結(jié),我認(rèn)為這一層主要的測(cè)試內(nèi)容有五點(diǎn):
是否使用正確的參數(shù)(通常是從 action payload 或 redux 中來(lái)),調(diào)用了正確的 API
對(duì)于 mock 的 API 返回,是否保存了正確的數(shù)據(jù)(通常是通過 action 保存到 redux 中去)
主要的業(yè)務(wù)邏輯(比如僅當(dāng)用戶滿足某些權(quán)限時(shí)才調(diào)用 API 等)
異常邏輯
其他副作用是否發(fā)生(比如有時(shí)有需要 Emit 的事件、需要保存到 IndexDB 中去的數(shù)據(jù)等)
來(lái)自官方的錯(cuò)誤姿勢(shì)redux-saga 官方提供了一個(gè) util: CloneableGenerator 用以幫我們寫 saga 的測(cè)試。這是我們項(xiàng)目使用的第一種測(cè)法,大概會(huì)寫出來(lái)的測(cè)試如下:
import chunk from "lodash/chunk" export function* onEnterProductDetailPage(action) { yield put(actions.notImportantAction1("loading-stuff")) yield put(actions.notImportantAction2("analytics-stuff")) yield put(actions.notImportantAction3("http-stuff")) yield put(actions.notImportantAction4("other-stuff")) const recommendations = yield call(Api.get, "products/recommended") const MAX_RECOMMENDATIONS = 3 const [products = []] = chunk(recommendations, MAX_RECOMMENDATIONS) yield put(actions.importantActionToSaveRecommendedProducts(products)) const { payload: { userId } } = action const { vipList } = yield select((store) => store.credentails) if (!vipList.includes(userId)) { yield put(actions.importantActionToFetchAds()) } }
import { put, call } from "saga-effects" import { cloneableGenerator } from "redux-saga/utils" import { Api } from "src/utils/axios" import { onEnterProductDetailPage } from "./saga" const product = (productId) => ({ productId }) test(` should only save the three recommended products and show ads when user enters the product detail page given the user is not a VIP `, () => { const action = { payload: { userId: 233 } } const credentials = { vipList: [2333] } const recommendedProducts = [product(1), product(2), product(3), product(4)] const firstThreeRecommendations = [product(1), product(2), product(3)] const generator = cloneableGenerator(onEnterProductDetailPage)(action) expect(generator.next().value).toEqual( actions.notImportantAction1("loading-stuff") ) expect(generator.next().value).toEqual( actions.notImportantAction2("analytics-stuff") ) expect(generator.next().value).toEqual( actions.notImportantAction3("http-stuff") ) expect(generator.next().value).toEqual( actions.notImportantAction4("other-stuff") ) expect(generator.next().value).toEqual(call(Api.get, "products/recommended")) expect(generator.next(recommendedProducts).value).toEqual( firstThreeRecommendations ) generator.next() expect(generator.next(credentials).value).toEqual( put(actions.importantActionToFetchAds()) ) })
這個(gè)方案寫多了,大家開始感受到了痛點(diǎn),明顯違背我們前面提到的一些原則:
測(cè)試分明就是把實(shí)現(xiàn)抄了一遍。這違反上述所說「有且僅有一個(gè)掛測(cè)試的理由」的原則,改變實(shí)現(xiàn)次序也將會(huì)使測(cè)試掛掉
當(dāng)在實(shí)現(xiàn)中某個(gè)部分加入新的語(yǔ)句時(shí),該語(yǔ)句后續(xù)所有的測(cè)試都會(huì)掛掉,并且出錯(cuò)信息非常難以描述原因,導(dǎo)致常常要陷入「調(diào)試測(cè)試」的境地,這也是依賴于實(shí)現(xiàn)次序帶來(lái)的惡果,根本無(wú)法支持「重構(gòu)」這種改變內(nèi)部實(shí)現(xiàn)但不改變業(yè)務(wù)行為的代碼清理行為
為了測(cè)試兩個(gè)重要的業(yè)務(wù)「只保存獲取回來(lái)的前三個(gè)推薦產(chǎn)品」、「對(duì)非 VIP 用戶推送廣告」,不得不在前面先按次序先斷言許多個(gè)不重要的實(shí)現(xiàn)
測(cè)試沒有重點(diǎn),隨便改點(diǎn)什么都會(huì)掛測(cè)試
正確姿勢(shì)針對(duì)以上痛點(diǎn),我們理想中的 saga 測(cè)試應(yīng)該是這樣:1) 不依賴實(shí)現(xiàn)次序;2) 允許僅對(duì)真正關(guān)心的、有價(jià)值的業(yè)務(wù)進(jìn)行測(cè)試;3) 支持不改動(dòng)業(yè)務(wù)行為的重構(gòu)。如此一來(lái),測(cè)試的保障效率和開發(fā)者體驗(yàn)都將大幅提升。
于是,我們發(fā)現(xiàn)官方提供了這么一個(gè)跑測(cè)試的工具,剛好可以用來(lái)完美滿足我們的需求:runSaga。我們可以用它將 saga 全部執(zhí)行一遍,搜集所有發(fā)布出去的 action,由開發(fā)者自由斷言其感興趣的 action!基于這個(gè)發(fā)現(xiàn),我們推出了我們的第二版 saga 測(cè)試方案:runSaga + 自定義拓展 jest 的 expect 斷言。最終,使用這個(gè)工具寫出來(lái)的 saga 測(cè)試,幾近完美:
import { put, call } from "saga-effects" import { Api } from "src/utils/axios" import { testSaga } from "../../../testing-utils" import { onEnterProductDetailPage } from "./saga" const product = (productId) => ({ productId }) test(` should only save the three recommended products and show ads when user enters the product detail page given the user is not a VIP `, async () => { const action = { payload: { userId: 233 } } const store = { credentials: { vipList: [2333] } } const recommendedProducts = [product(1), product(2), product(3), product(4)] const firstThreeRecommendations = [product(1), product(2), product(3)] Api.get = jest.fn().mockImplementations(() => recommendedProducts) await testSaga(onEnterProductDetailPage, action, store) expect(Api.get).toHaveBeenCalledWith("products/recommended") expect( actions.importantActionToSaveRecommendedProducts ).toHaveBeenDispatchedWith(firstThreeRecommendations) expect(actions.importantActionToFetchAds).toHaveBeenDispatched() })
這個(gè)測(cè)試已經(jīng)簡(jiǎn)短了許多,沒有了無(wú)關(guān)斷言的雜音,依然遵循 given-when-then 的結(jié)構(gòu)。并且同樣是測(cè)試「只保存獲取回來(lái)的前三個(gè)推薦產(chǎn)品」、「對(duì)非 VIP 用戶推送廣告」兩個(gè)關(guān)心的業(yè)務(wù)點(diǎn),其中自有簡(jiǎn)潔的規(guī)律:
當(dāng)輸入不變時(shí),無(wú)論你怎么優(yōu)化內(nèi)部實(shí)現(xiàn)、調(diào)整內(nèi)部次序,這個(gè)測(cè)試關(guān)心的業(yè)務(wù)場(chǎng)景都不會(huì)掛,真正做到了測(cè)試保護(hù)重構(gòu)、支持重構(gòu)的作用
可以僅斷言你關(guān)心的點(diǎn),忽略不重要或不關(guān)心的中間過程(比如上例中,我們就沒有斷言其他 notImportant 的 action 是否被 dispatch 出去),消除無(wú)關(guān)斷言的雜音,提升了表達(dá)力
使用了 product 這樣的測(cè)試數(shù)據(jù)創(chuàng)建套件(fixtures),精簡(jiǎn)測(cè)試數(shù)據(jù),消除無(wú)關(guān)數(shù)據(jù)的雜音,提升了表達(dá)力
自定義的 expect(action).toHaveBeenDispatchedWith(payload) matcher 很有表達(dá)力,且出錯(cuò)信息友好
這個(gè)自定義的 matcher 是通過 jest 的 expect.extend 擴(kuò)展實(shí)現(xiàn)的:
expect.extend({ toHaveBeenDispatched(action) { ... }, toHaveBeenDispatchedWith(action, payload) { ... }, })
上面是我們認(rèn)為比較好的副作用測(cè)試工具、測(cè)試策略和測(cè)試方案。使用時(shí),需要牢記你真正關(guān)心的業(yè)務(wù)價(jià)值點(diǎn)(本節(jié)開始提到的 5 點(diǎn)),以及做到在較為復(fù)雜的單元測(cè)試中始終堅(jiān)守三大基本原則。唯如此,單元測(cè)試才能真正提升開發(fā)速度、支持重構(gòu)、充當(dāng)業(yè)務(wù)上下文的文檔。
component 測(cè)試組件測(cè)試其實(shí)是實(shí)踐最多,測(cè)試實(shí)踐看法和分歧也最多的地方。React 組件是一個(gè)高度自治的單元,從分類上來(lái)看,它大概有這么幾類:
展示型業(yè)務(wù)組件
容器型業(yè)務(wù)組件
通用 UI 組件
功能型組件
先把這個(gè)分類放在這里,待會(huì)回過頭來(lái)談。對(duì)于 React 組件測(cè)什么不測(cè)什么,我有一些思考,也有一些判斷標(biāo)準(zhǔn):除去功能型組件,其他類型的組件一般是以渲染出一個(gè)語(yǔ)法樹為終點(diǎn)的,它描述了頁(yè)面的 UI 內(nèi)容、結(jié)構(gòu)、樣式和一些邏輯 component(props) => UI。內(nèi)容、結(jié)構(gòu)和樣式,比起測(cè)試,直接在頁(yè)面上調(diào)試反饋效果更好。測(cè)也不是不行,但都難免有不穩(wěn)定的成本在;邏輯這塊,還是有一測(cè)的價(jià)值,但需要控制好依賴。綜合「好的單元測(cè)試標(biāo)準(zhǔn)」作為原則進(jìn)行考慮,我的建議是:兩測(cè)兩不測(cè)。
組件分支渲染邏輯必須測(cè)
事件調(diào)用和參數(shù)傳遞一般要測(cè)
純 UI 不在單元測(cè)試層級(jí)測(cè)
連接 redux 的高階組件不測(cè)
其他的一般不測(cè)(比如 CSS,官方文檔有反例)
組件的分支邏輯,往往也是有業(yè)務(wù)含義和業(yè)務(wù)價(jià)值的分支,添加單元測(cè)試既能保障重構(gòu),還可順便做文檔用;事件調(diào)用同樣也有業(yè)務(wù)價(jià)值和文檔作用,而事件調(diào)用的參數(shù)調(diào)用有時(shí)可起到保護(hù)重構(gòu)的作用。
純 UI 不在單元測(cè)試級(jí)別測(cè)試的原因,純粹就是因?yàn)椴缓脭嘌?。所謂快照測(cè)試有意義的前提在于兩個(gè):必須是視覺級(jí)別的比對(duì)、必須開發(fā)者每次都認(rèn)真檢查。jest 有個(gè) snapshot 測(cè)試的概念,但那個(gè) UI 測(cè)試是代碼級(jí)的比對(duì),不是視覺級(jí)的比對(duì),最終還是繞了一圈,去除了雜音還不如看 Git 的 commit diff。每次要求開發(fā)者自覺檢查,既打亂工作流,也難以堅(jiān)持??紤]到這些成本,我不推薦在單元測(cè)試的級(jí)別來(lái)做 UI 類型的測(cè)試。對(duì)于我們之前中等規(guī)模的項(xiàng)目,訴諸手工還是有一定的可控性。
連接 redux 的高階組件不測(cè)。原因是,connect 過的組件從測(cè)試的角度看無(wú)非幾個(gè)測(cè)試點(diǎn):
mapStateToProps 中是否從 store 中取得了正確的參數(shù)
mapDispatchToProps 中是否地從 actions 中取得了正確的參數(shù)
map 過的 props 是否正確地被傳遞給了組件
redux 對(duì)應(yīng)的數(shù)據(jù)切片更新時(shí),是否會(huì)使用新的 props 觸發(fā)組件進(jìn)行一次更新
這四個(gè)點(diǎn),react-redux 已經(jīng)都幫你測(cè)過了,已經(jīng)證明 work 了,為啥要重復(fù)測(cè)試自尋煩惱呢?當(dāng)然,不測(cè)這個(gè)東西的話,還是有這么一種可能,就是你 export 的純組件測(cè)試都是過的,但是代碼實(shí)際運(yùn)行出錯(cuò)。窮盡下來(lái)主要可能是這幾種問題:
你在 mapStateToProps 中打錯(cuò)了字或打錯(cuò)了變量名
你寫了 mapStateToProps 但沒有 connect 上去
你在 mapStateToProps 中取的路徑是錯(cuò)的,在 redux 中已經(jīng)被改過
第一、二種可能,無(wú)視。測(cè)試不是萬(wàn)能藥,不能預(yù)防人主動(dòng)犯錯(cuò),這種場(chǎng)景如果是小步提交發(fā)現(xiàn)起來(lái)是很快的,如果不小步提交那什么測(cè)試都幫不了你的;如果某段數(shù)據(jù)獲取的邏輯多處重復(fù),則可以考慮將該邏輯抽取到 selector 中并進(jìn)行多帶帶測(cè)試。
第三種可能,確實(shí)是問題,但發(fā)生頻率目前看來(lái)較低。為啥呢,因?yàn)闆]有類型系統(tǒng)我們不會(huì)也不敢隨意改 redux 的數(shù)據(jù)結(jié)構(gòu)啊…(這侵入性重的框架喲)所以針對(duì)這些少量出現(xiàn)的場(chǎng)景,不必要采取錯(cuò)殺一千的方式進(jìn)行完全覆蓋。默認(rèn)不測(cè),出了問題或者經(jīng)常可能出問題的部分,再策略性地補(bǔ)上測(cè)試進(jìn)行固定即可。
綜上,@connect 組件不測(cè),因?yàn)榭蚣鼙旧硪炎隽舜蟛糠譁y(cè)試,剩下的場(chǎng)景出 bug 頻率不高,而施加測(cè)試的話提高成本(準(zhǔn)備依賴和數(shù)據(jù)),降低開發(fā)體驗(yàn),模糊測(cè)試場(chǎng)景,性價(jià)比不大,所以強(qiáng)烈建議省了這份心。不測(cè) @connect 過的組件,其實(shí)也是 官方文檔 推薦的做法。
然后,基于上面第 1、2 個(gè)結(jié)論,映射回四類組件的結(jié)構(gòu)當(dāng)中去,我們可以得到下面的表格,然后發(fā)現(xiàn)…每種組件都要測(cè)渲染分支和事件調(diào)用,跟組件類型根本沒必然的關(guān)聯(lián)…不過,功能型組件有可能會(huì)涉及一些其他的模式,因此又大致分出一小節(jié)來(lái)談。
組件類型 / 測(cè)試內(nèi)容 | 分支渲染邏輯 | 事件調(diào)用 | @connect | 純 UI |
---|---|---|---|---|
展示型組件 | ? | ? | - | ?? |
容器型組件 | ? | ? | ?? | ?? |
通用 UI 組件 | ? | ? | - | ?? |
功能型組件 | ? | ? | ?? | ?? |
export const CommentsSection = ({ comments }) => ({comments.length > 0 && ()Comments
)} {comments.map((comment) => ()}
對(duì)應(yīng)的測(cè)試如下,測(cè)試的是不同的分支渲染邏輯:沒有評(píng)論時(shí),則不渲染 Comments header。
import { CommentsSection } from "./index" import { Comment } from "./Comment" test("should not render a header and any comment sections when there is no comments", () => { const component = shallow(業(yè)務(wù)型組件 - 事件調(diào)用) const header = component.find("h2") const comments = component.find(Comment) expect(header).toHaveLength(0) expect(comments).toHaveLength(0) }) test("should render a comments section and a header when there are comments", () => { const contents = [ { id: 1, author: "男***8", comment: "價(jià)廉物美,相信奧康旗艦店" }, { id: 2, author: "雨***成", comment: "所以一雙合腳的鞋子..." }, ] const component = shallow( ) const header = component.find("h2") const comments = component.find(Comment) expect(header.html()).toBe("Comments") expect(comments).toHaveLength(2) })
測(cè)試事件的一個(gè)場(chǎng)景如下:當(dāng)某條產(chǎn)品被點(diǎn)擊時(shí),應(yīng)該將產(chǎn)品相關(guān)的信息發(fā)送給埋點(diǎn)系統(tǒng)進(jìn)行埋點(diǎn)。
export const ProductItem = ({ id, productName, introduction, trackPressEvent, }) => (trackPressEvent(id, productName)}> )
import { ProductItem } from "./index" test(` should send product id and name to analytics system when user press the product item `, () => { const trackPressEvent = jest.fn() const component = shallow() component.find(TouchableWithoutFeedback).simulate("press") expect(trackPressEvent).toHaveBeenCalledWith( 100832, "iMac Pro - Power to the pro." ) })
簡(jiǎn)單得很吧。這里的幾個(gè)測(cè)試,在你改動(dòng)了樣式相關(guān)的東西時(shí),不會(huì)掛掉;但是如果你改動(dòng)了分支邏輯或函數(shù)調(diào)用的內(nèi)容時(shí),它就會(huì)掛掉了。而分支邏輯或函數(shù)調(diào)用,恰好是我覺得接近業(yè)務(wù)的地方,所以它們對(duì)保護(hù)代碼邏輯、保護(hù)重構(gòu)是有價(jià)值的。當(dāng)然,它們多少還是依賴了組件內(nèi)部的實(shí)現(xiàn)細(xì)節(jié),比如說 find(TouchableWithoutFeedback),還是做了「組件內(nèi)部使用了 TouchableWithoutFeedback 組件」這樣的假設(shè),而這個(gè)假設(shè)很可能是會(huì)變的。也就是說,如果我換了一個(gè)組件來(lái)接受點(diǎn)擊事件,盡管點(diǎn)擊時(shí)的行為依然發(fā)生,但這個(gè)測(cè)試仍然會(huì)掛掉。這就違反了我們所說了「有且僅有一個(gè)使測(cè)試失敗的理由」。這對(duì)于組件測(cè)試來(lái)說,是不夠完美的地方。
但這個(gè)問題無(wú)法避免。因?yàn)榻M件本質(zhì)是渲染組件樹,那么測(cè)試中要與組件樹關(guān)聯(lián),必然要通過 組件名、id 這樣的 selector,這些 selector 的關(guān)聯(lián)本身就是使測(cè)試掛掉的「另一個(gè)理由」。但對(duì)組件的分支、事件進(jìn)行測(cè)試又有一定的價(jià)值,無(wú)法避免。所以,我認(rèn)為這個(gè)部分還是要用,只不過同時(shí)需要一些限制,以控制這些假設(shè)為維護(hù)測(cè)試帶來(lái)的額外成本:
不要斷言組件內(nèi)部結(jié)構(gòu)。像那些 expect(component.find("div > div > p").html().toBe("Content") 的真的就算了吧
正確拆分組件樹。一個(gè)組件盡量只負(fù)責(zé)一個(gè)功能,不允許堆疊太多的函數(shù)和功能。要符合單一職責(zé)原則
如果你的每個(gè)組件都十分清晰直觀、邏輯分明,那么像上面這樣的組件測(cè)起來(lái)也就很輕松,一般就遵循 shallow -> find(Component) -> 斷言的三段式,哪怕是了解了一些組件的內(nèi)部細(xì)節(jié),通常也在可控的范圍內(nèi),維護(hù)起來(lái)成本并不高。這是目前我覺得平衡了表達(dá)力、重構(gòu)意義和測(cè)試成本的實(shí)踐。
功能型組件 - children 型高階組件功能型組件,指的是跟業(yè)務(wù)無(wú)關(guān)的另一類組件:它是功能型的,更像是底層支撐著業(yè)務(wù)組件運(yùn)作的基礎(chǔ)組件,比如路由組件、分頁(yè)組件等。這些組件一般偏重邏輯多一點(diǎn),關(guān)心 UI 少一些。其本質(zhì)測(cè)法跟業(yè)務(wù)組件是一致的:不關(guān)心 UI 具體渲染,只測(cè)分支渲染和事件調(diào)用。但由于它偏功能型的特性,使得它在設(shè)計(jì)上常會(huì)出現(xiàn)一些業(yè)務(wù)型組件不常出現(xiàn)的設(shè)計(jì)模式,如高階組件、以函數(shù)為子組件等。下面分別針對(duì)這幾種進(jìn)行分述。
export const FeatureToggle = ({ features, featureName, children }) => { if (!features[featureName]) { return null } return children } export default connect( (store) => ({ features: store.global.features }) )(FeatureToggle)
import React from "react" import { shallow } from "enzyme" import { View } from "react-native" import FeatureToggles from "./featureToggleStatus" import { FeatureToggle } from "./index" const DummyComponent = () =>utils 測(cè)試test("should not render children component when remote toggle is empty", () => { const component = shallow( ) expect(component.find(DummyComponent)).toHaveLength(0) }) test("should render children component when remote toggle is present and stated on", () => { const features = { promotion618: FeatureToggles.on, } const component = shallow( ) expect(component.find(DummyComponent)).toHaveLength(1) }) test("should not render children component when remote toggle object is present but stated off", () => { const features = { promotion618: FeatureToggles.off, } const component = shallow( ) expect(component.find(DummyComponent)).toHaveLength(0) })
每個(gè)項(xiàng)目都會(huì)有 utils。一般來(lái)說,我們期望 util 都是純函數(shù),即是不依賴外部狀態(tài)、不改變參數(shù)值、不維護(hù)內(nèi)部狀態(tài)的函數(shù)。這樣的函數(shù)測(cè)試效率也非常高。測(cè)試原則跟前面所說的也并沒什么不同,不再贅述。不過值得一提的是,因?yàn)?util 函數(shù)多是數(shù)據(jù)驅(qū)動(dòng),一個(gè)輸入對(duì)應(yīng)一個(gè)輸出,并且不需要準(zhǔn)備任何依賴,這使得它非常適合采用參數(shù)化測(cè)試的方法。這種測(cè)試方法,可以提升數(shù)據(jù)準(zhǔn)備效率,同時(shí)依然能保持詳細(xì)的用例信息、錯(cuò)誤提示等優(yōu)點(diǎn)。jest 從 23 后就內(nèi)置了對(duì)參數(shù)化測(cè)試的支持了,如下:
test.each([ [["0", "99"], 0.99, "(整數(shù)部分為0時(shí)也應(yīng)返回)"], [["5", "00"], 5, "(小數(shù)部分不足時(shí)應(yīng)該補(bǔ)0)"], [["5", "10"], 5.1, "(小數(shù)部分不足時(shí)應(yīng)該補(bǔ)0)"], [["4", "38"], 4.38, "(小數(shù)部分不足時(shí)應(yīng)該補(bǔ)0)"], [["4", "99"], 4.994, "(超過默認(rèn)2位的小數(shù)的直接截?cái)啵凰纳嵛迦?"], [["4", "99"], 4.995, "(超過默認(rèn)2位的小數(shù)的直接截?cái)?,不四舍五?"], [["4", "99"], 4.996, "(超過默認(rèn)2位的小數(shù)的直接截?cái)啵凰纳嵛迦?"], [["-0", "50"], -0.5, "(整數(shù)部分為負(fù)數(shù)時(shí)應(yīng)該保留負(fù)號(hào))"], ])( "should return %s when number is %s (%s)", (expected, input, description) => { expect(truncateAndPadTrailingZeros(input)).toEqual(expected) } )總結(jié)
好,到此為止,本文的主要內(nèi)容也就講完了。總結(jié)下來(lái),本文主要覆蓋到的內(nèi)容如下:
單元測(cè)試對(duì)于任何 React 項(xiàng)目(及其他任何項(xiàng)目)來(lái)說都是必須的
我們需要自動(dòng)化的測(cè)試套件,根本目標(biāo)是為了提升企業(yè)和團(tuán)隊(duì)的 IT「響應(yīng)力」
之所以優(yōu)先選擇單元測(cè)試,是依據(jù)測(cè)試金字塔的成本收益比原則確定得到的
好的單元測(cè)試具備三大特征:有且僅有一個(gè)失敗的理由、表達(dá)力極強(qiáng)、快、穩(wěn)定
單元測(cè)試也有測(cè)試策略:在 React 的典型架構(gòu)下,一個(gè)測(cè)試體系大概分為六層:組件、action、reducer、selector、副作用層、utils。它們分別的測(cè)試策略為:
reducer、selector 的重邏輯代碼要求 100% 覆蓋
utils 層的純函數(shù)要求 100% 覆蓋
副作用層主要測(cè)試:是否拿到了正確的參數(shù)、是否調(diào)用了正確的 API、是否保存了正確的數(shù)據(jù)、業(yè)務(wù)邏輯、異常邏輯 五個(gè)層面
組件層兩測(cè)兩不測(cè):分支渲染邏輯必測(cè)、事件、交互調(diào)用必測(cè);純 UI(包括 CSS)不測(cè)、@connect 過的高階組件不測(cè)
action 層選擇性覆蓋:可不測(cè)
其他高級(jí)技巧:定制測(cè)試工具(jest.extend)、參數(shù)化測(cè)試等
未盡話題 & 歡迎討論講完 React 下的單元測(cè)試尚且已經(jīng)這么花費(fèi)篇幅,文章中難免還有些我十分想提又意猶未盡的地方。比如完整的測(cè)試策略、比如 TDD、比如重構(gòu)、比如整潔代碼設(shè)計(jì)模式等。如果讀者有由此文章而生發(fā)、而疑慮、而不吐不快的種種興趣和分享,都十分歡迎留下你的想法和指點(diǎn)。寫文交流,樂趣如此。感謝。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://www.ezyhdfw.cn/yun/98783.html
摘要:?jiǎn)卧獪y(cè)試針對(duì)程序模塊進(jìn)行測(cè)試。是開源的單元測(cè)試工具。一個(gè)好的單元測(cè)試應(yīng)該具備的條件安全重構(gòu)已有代碼單元測(cè)試一個(gè)很重要的價(jià)值是為重構(gòu)保駕護(hù)航。斷言外部依賴單元測(cè)試的一個(gè)重要原則就是無(wú)依賴和隔離。 前端測(cè)試金字塔 對(duì)于一個(gè) Web 應(yīng)用來(lái)說,理想的測(cè)試組合應(yīng)該包含大量單元測(cè)試(unit tests),部分快照測(cè)試(snapshot tests),以及少量端到端測(cè)試(e2e tests)。參...
摘要:以下兩個(gè)要點(diǎn)將會(huì)對(duì)任何微服務(wù)重構(gòu)策略產(chǎn)生重大影響。批量替換通過批發(fā)更換,您可以一次性重構(gòu)整個(gè)應(yīng)用程序,直接從單體式轉(zhuǎn)移到一組微服務(wù)器。如果您通過使用破解您的微服務(wù)器,那么每個(gè)域?qū)@一個(gè)用例,或者更常見的,一組相互關(guān)聯(lián)的用例。 在決定使用微服務(wù)之后,為了將微服務(wù)付諸實(shí)踐,也許你已經(jīng)開始重構(gòu)你的應(yīng)用程序或把重構(gòu)工作列入了待辦事項(xiàng)清單。 無(wú)論是哪種情況,如果這是你第一次重構(gòu)應(yīng)用程序,那么您...
摘要:前端每周清單第期現(xiàn)狀分析與優(yōu)化策略單元測(cè)試爬蟲作者王下邀月熊編輯徐川前端每周清單專注前端領(lǐng)域內(nèi)容,以對(duì)外文資料的搜集為主,幫助開發(fā)者了解一周前端熱點(diǎn)分為新聞熱點(diǎn)開發(fā)教程工程實(shí)踐深度閱讀開源項(xiàng)目巔峰人生等欄目。 showImg(https://segmentfault.com/img/remote/1460000011008022); 前端每周清單第 29 期:Web 現(xiàn)狀分析與優(yōu)化策略...
摘要:感受構(gòu)建工具給前端優(yōu)化工作帶來(lái)的便利。多多益處邏輯清晰,程序注重?cái)?shù)據(jù)與表現(xiàn)分離,可讀性強(qiáng),利于規(guī)避和排查問題構(gòu)建工具層出不窮。其實(shí)工具都能滿足需求,關(guān)鍵是看怎么用,工具的使用背后是對(duì)前端性能優(yōu)化的理解程度。 這篇主要介紹一下我在玩Webpack過程中的心得。通過實(shí)例介紹WebPack的安裝,插件使用及加載策略。感受構(gòu)建工具給前端優(yōu)化工作帶來(lái)的便利。 showImg(https://se...
摘要:月日,首期沙龍海量運(yùn)維實(shí)踐大曝光在騰訊大廈圓滿舉行??椩聘咝У膶?shí)踐是,它是以運(yùn)維標(biāo)準(zhǔn)化為基石,以為核心的自動(dòng)化運(yùn)維平臺(tái)。 作者丨周小軍,騰訊SNG資深運(yùn)維工程師,負(fù)責(zé)社交產(chǎn)品分布式存儲(chǔ)的運(yùn)維及團(tuán)隊(duì)管理工作。對(duì)互聯(lián)網(wǎng)網(wǎng)站架構(gòu)、數(shù)據(jù)中心、云計(jì)算及自動(dòng)化運(yùn)維等領(lǐng)域有深入研究和理解。 12月16日,首期沙龍海量運(yùn)維實(shí)踐大曝光在騰訊大廈圓滿舉行。沙龍出品人騰訊運(yùn)維技術(shù)總監(jiān)、復(fù)旦大學(xué)客座講師、De...
閱讀 3574·2021-11-24 09:38
閱讀 3272·2021-11-22 09:34
閱讀 2169·2021-09-22 16:03
閱讀 2464·2019-08-29 18:37
閱讀 444·2019-08-29 16:15
閱讀 1834·2019-08-26 13:56
閱讀 931·2019-08-26 12:21
閱讀 2272·2019-08-26 12:15