亚洲中字慕日产2020,大陆极品少妇内射AAAAAA,无码av大香线蕉伊人久久,久久精品国产亚洲av麻豆网站

資訊專欄INFORMATION COLUMN

React 單元測(cè)試策略及落地

nifhlheimr / 3463人閱讀

摘要:寫好的單元測(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、 ,在大型或遺留系統(tǒng)中補(bǔ)測(cè)試還很可能遇到 @connect 組件里套 @connect 組件的場(chǎng)景);然后收益也只是可能覆蓋到了幾個(gè)極少數(shù)出現(xiàn)的場(chǎng)景。得不償失,果斷不測(cè)。

關(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 組件 ? ? - ??
功能型組件 ? ? ?? ??
業(yè)務(wù)型組件 - 分支渲染
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()

  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)
})
業(yè)務(wù)型組件 - 事件調(diào)用

測(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)}>
    
      
      <Introduction introduction={introduction} />
    </View>
  </TouchableWithoutFeedback>
)</pre>
<pre>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(
    <ProductItem
      id={100832}
      introduction="iMac Pro - Power to the pro."
      trackPressEvent={trackPressEvent}
    />
  )

  component.find(TouchableWithoutFeedback).simulate("press")

  expect(trackPressEvent).toHaveBeenCalledWith(
    100832,
    "iMac Pro - Power to the pro."
  )
})</pre>
<p>簡(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é),比如說 <b>find(TouchableWithoutFeedback)</b>,還是做了「組件內(nèi)部使用了 <b>TouchableWithoutFeedback</b> 組件」這樣的假設(shè),而這個(gè)假設(shè)很可能是會(huì)變的。也就是說,如果我換了一個(gè)組件來(lái)接受點(diǎn)擊事件,盡管點(diǎn)擊時(shí)的行為依然發(fā)生,但這個(gè)測(cè)試仍然會(huì)掛掉。這就違反了我們所說了「有且僅有一個(gè)使測(cè)試失敗的理由」。這對(duì)于組件測(cè)試來(lái)說,是不夠完美的地方。</p>
<p>但這個(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)的額外成本:</p>

<p>不要斷言組件內(nèi)部結(jié)構(gòu)。像那些 <b>expect(component.find("div > div > p").html().toBe("Content")</b> 的真的就算了吧</p>
<p>正確拆分組件樹。一個(gè)組件盡量只負(fù)責(zé)一個(gè)功能,不允許堆疊太多的函數(shù)和功能。要符合單一職責(zé)原則</p>

<p>如果你的每個(gè)組件都十分清晰直觀、邏輯分明,那么像上面這樣的組件測(cè)起來(lái)也就很輕松,一般就遵循 <b>shallow</b> -> <b>find(Component)</b> -> 斷言的三段式,哪怕是了解了一些組件的內(nèi)部細(xì)節(jié),通常也在可控的范圍內(nèi),維護(hù)起來(lái)成本并不高。這是目前我覺得平衡了表達(dá)力、重構(gòu)意義和測(cè)試成本的實(shí)踐。</p>
<b>功能型組件 - <b>children</b> 型高階組件</b>
<p>功能型組件,指的是跟業(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)行分述。</p>
<pre>export const FeatureToggle = ({ features, featureName, children }) => {
  if (!features[featureName]) {
    return null
  }

  return children
}

export default connect(
  (store) => ({ features: store.global.features })
)(FeatureToggle)</pre>
<pre>import React from "react"
import { shallow } from "enzyme"
import { View } from "react-native"

import FeatureToggles from "./featureToggleStatus"
import { FeatureToggle } from "./index"

const DummyComponent = () => <View />

test("should not render children component when remote toggle is empty", () => {
  const component = shallow(
    <FeatureToggle features={{}} featureName="promotion618">
      <DummyComponent />
    </FeatureToggle>
  )

  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(
    <FeatureToggle features={features} featureName="promotion618">
      <DummyComponent />
    </FeatureToggle>
  )

  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(
    <FeatureToggle features={features} featureName="promotion618">
      <DummyComponent />
    </FeatureToggle>
  )

  expect(component.find(DummyComponent)).toHaveLength(0)
})</pre>
<b>utils 測(cè)試</b>
<p>每個(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è)試的支持了,如下:</p>
<pre>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)
  }
)</pre>
<p><script type="text/javascript">showImg("https://segmentfault.com/img/remote/1460000016828940?w=2674&h=1224");</script></p>
<b>總結(jié)</b>
<p>好,到此為止,本文的主要內(nèi)容也就講完了。總結(jié)下來(lái),本文主要覆蓋到的內(nèi)容如下:</p>

<p>單元測(cè)試對(duì)于任何 React 項(xiàng)目(及其他任何項(xiàng)目)來(lái)說都是必須的</p>
<p>我們需要自動(dòng)化的測(cè)試套件,根本目標(biāo)是為了提升企業(yè)和團(tuán)隊(duì)的 IT「響應(yīng)力」</p>
<p>之所以優(yōu)先選擇單元測(cè)試,是依據(jù)測(cè)試金字塔的成本收益比原則確定得到的</p>
<p>好的單元測(cè)試具備三大特征:<strong>有且僅有一個(gè)失敗的理由</strong>、<strong>表達(dá)力極強(qiáng)</strong>、<strong>快、穩(wěn)定</strong>
</p>
<p>
<p>單元測(cè)試也有測(cè)試策略:在 React 的典型架構(gòu)下,一個(gè)測(cè)試體系大概分為六層:組件、action、reducer、selector、副作用層、utils。它們分別的測(cè)試策略為:</p>

<p>reducer、selector 的重邏輯代碼要求 100% 覆蓋</p>
<p>utils 層的純函數(shù)要求 100% 覆蓋</p>
<p>副作用層主要測(cè)試:<strong>是否拿到了正確的參數(shù)</strong>、<strong>是否調(diào)用了正確的 API</strong>、<strong>是否保存了正確的數(shù)據(jù)</strong>、<strong>業(yè)務(wù)邏輯</strong>、<strong>異常邏輯</strong> 五個(gè)層面</p>
<p>組件層兩測(cè)兩不測(cè):<strong>分支渲染邏輯必測(cè)</strong>、<strong>事件、交互調(diào)用必測(cè)</strong>;純 UI(包括 CSS)不測(cè)、<b>@connect</b> 過的高階組件不測(cè)</p>
<p>action 層選擇性覆蓋:可不測(cè)</p>

</p>
<p>其他高級(jí)技巧:定制測(cè)試工具(<b>jest.extend</b>)、參數(shù)化測(cè)試等</p>

<b>未盡話題 & 歡迎討論</b>
<p>講完 React 下的單元測(cè)試尚且已經(jīng)這么花費(fèi)篇幅,文章中難免還有些我十分想提又意猶未盡的地方。比如完整的測(cè)試策略、比如 TDD、比如重構(gòu)、比如整潔代碼設(shè)計(jì)模式等。如果讀者有由此文章而生發(fā)、而疑慮、而不吐不快的種種興趣和分享,都十分歡迎留下你的想法和指點(diǎn)。寫文交流,樂趣如此。感謝。</p>           
               
                                           
                       
                 </div>
            
                     <div   id="dnkpnhlp"   class="mt-64 tags-seach" >
                 <div   id="dnkpnhlp"   class="tags-info">
                                                                                                                    
                         <a style="width:120px;" title="GPU云服務(wù)器" href="http://www.ezyhdfw.cn/site/product/gpu.html">GPU云服務(wù)器</a>
                                             
                         <a style="width:120px;" title="云服務(wù)器" href="http://www.ezyhdfw.cn/site/active/kuaijiesale.html?ytag=seo">云服務(wù)器</a>
                                                                                                                                                 
                                      
                     
                    
                                                                                               <a style="width:120px;" title="單元測(cè)試" href="http://www.ezyhdfw.cn/yun/tag/danyuanceshi/">單元測(cè)試</a>
                                                                                                           <a style="width:120px;" title="單元測(cè)試規(guī)范" href="http://www.ezyhdfw.cn/yun/tag/danyuanceshiguifan/">單元測(cè)試規(guī)范</a>
                                                                                                           <a style="width:120px;" title="單元測(cè)試圖片測(cè)試java" href="http://www.ezyhdfw.cn/yun/tag/danyuanceshitupianceshijava/">單元測(cè)試圖片測(cè)試java</a>
                                                                                                           <a style="width:120px;" title="php單元測(cè)試’" href="http://www.ezyhdfw.cn/yun/tag/phpdanyuanceshi/">php單元測(cè)試’</a>
                                                         
                 </div>
               
              </div>
             
               <div   id="dnkpnhlp"   class="entry-copyright mb-30">
                   <p class="mb-15"> 文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。</p>
                 
                   <p>轉(zhuǎn)載請(qǐng)注明本文地址:http://www.ezyhdfw.cn/yun/98783.html</p>
               </div>
                      
               <ul class="pre-next-page">
                 
                                  <li id="dnkpnhlp"    class="ellipsis"><a class="hpf" href="http://www.ezyhdfw.cn/yun/98782.html">上一篇:JavaScript之閉包與立即執(zhí)行函數(shù)</a></li>  
                                                
                                       <li id="dnkpnhlp"    class="ellipsis"><a class="hpf" href="http://www.ezyhdfw.cn/yun/98784.html">下一篇:深入 JavaScript 原型繼承原理——babel 編譯碼解讀</a></li>
                                  </ul>
              </div>
              <div   id="dnkpnhlp"   class="about_topicone-mid">
                <h3 class="top-com-title mb-0"><span data-id="0">相關(guān)文章</span></h3>
                <ul class="com_white-left-mid atricle-list-box">
                             
                                                                                                    <li>
                                                <div   id="dnkpnhlp"   class="atricle-list-right">
                          <h2 class="ellipsis2"><a class="hpf" href="http://www.ezyhdfw.cn/yun/8902.html"><b><em>React</em> <em>測(cè)試</em>指南</b></a></h2>
                                                     <p class="ellipsis2 good">摘要:?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)。參...</p>
                                                   
                          <div   id="dnkpnhlp"   class="com_white-left-info">
                                <div   id="dnkpnhlp"   class="com_white-left-infol">
                                    <a href="http://www.ezyhdfw.cn/yun/u-254.html"><img src="http://www.ezyhdfw.cn/yun/data/avatar/000/00/02/small_000000254.jpg" alt=""><span id="dnkpnhlp"    class="layui-hide64">Tecode</span></a>
                                    <time datetime="">2019-05-29 17:12</time>
                                    <span><i class="fa fa-commenting"></i>評(píng)論0</span> 
                                    <span><i class="fa fa-star"></i>收藏0</span> 
                                </div>
                          </div>
                      </div>
                    </li> 
                                                                                       <li>
                                                <div   id="dnkpnhlp"   class="atricle-list-right">
                          <h2 class="ellipsis2"><a class="hpf" href="http://www.ezyhdfw.cn/yun/26973.html"><b>如何用微服務(wù)重構(gòu)應(yīng)用程序</b></a></h2>
                                                     <p class="ellipsis2 good">摘要:以下兩個(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)用程序,那么您...</p>
                                                   
                          <div   id="dnkpnhlp"   class="com_white-left-info">
                                <div   id="dnkpnhlp"   class="com_white-left-infol">
                                    <a href="http://www.ezyhdfw.cn/yun/u-510.html"><img src="http://www.ezyhdfw.cn/yun/data/avatar/000/00/05/small_000000510.jpg" alt=""><span id="dnkpnhlp"    class="layui-hide64">KevinYan</span></a>
                                    <time datetime="">2019-06-28 15:53</time>
                                    <span><i class="fa fa-commenting"></i>評(píng)論0</span> 
                                    <span><i class="fa fa-star"></i>收藏0</span> 
                                </div>
                          </div>
                      </div>
                    </li> 
                                                                                       <li>
                                                <div   id="dnkpnhlp"   class="atricle-list-right">
                          <h2 class="ellipsis2"><a class="hpf" href="http://www.ezyhdfw.cn/yun/87432.html"><b>前端每周清單第 29 期:Web 現(xiàn)狀分析與優(yōu)化<em>策略</em>、Vue <em>單元</em><em>測(cè)試</em>、Headless Chrom</b></a></h2>
                                                     <p class="ellipsis2 good">摘要:前端每周清單第期現(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)化策略...</p>
                                                   
                          <div   id="dnkpnhlp"   class="com_white-left-info">
                                <div   id="dnkpnhlp"   class="com_white-left-infol">
                                    <a href="http://www.ezyhdfw.cn/yun/u-449.html"><img src="http://www.ezyhdfw.cn/yun/data/avatar/000/00/04/small_000000449.jpg" alt=""><span id="dnkpnhlp"    class="layui-hide64">HackerShell</span></a>
                                    <time datetime="">2019-08-21 12:05</time>
                                    <span><i class="fa fa-commenting"></i>評(píng)論0</span> 
                                    <span><i class="fa fa-star"></i>收藏0</span> 
                                </div>
                          </div>
                      </div>
                    </li> 
                                                                                       <li>
                                                <div   id="dnkpnhlp"   class="atricle-list-right">
                          <h2 class="ellipsis2"><a class="hpf" href="http://www.ezyhdfw.cn/yun/78853.html"><b>【前端構(gòu)建】WebPack實(shí)例與前端性能優(yōu)化</b></a></h2>
                                                     <p class="ellipsis2 good">摘要:感受構(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...</p>
                                                   
                          <div   id="dnkpnhlp"   class="com_white-left-info">
                                <div   id="dnkpnhlp"   class="com_white-left-infol">
                                    <a href="http://www.ezyhdfw.cn/yun/u-126.html"><img src="http://www.ezyhdfw.cn/yun/data/avatar/000/00/01/small_000000126.jpg" alt=""><span id="dnkpnhlp"    class="layui-hide64">QiShare</span></a>
                                    <time datetime="">2019-08-19 17:48</time>
                                    <span><i class="fa fa-commenting"></i>評(píng)論0</span> 
                                    <span><i class="fa fa-star"></i>收藏0</span> 
                                </div>
                          </div>
                      </div>
                    </li> 
                                                                                       <li>
                                                <div   id="dnkpnhlp"   class="atricle-list-right">
                          <h2 class="ellipsis2"><a class="hpf" href="http://www.ezyhdfw.cn/yun/8023.html"><b>騰訊云運(yùn)維干貨沙龍-海量運(yùn)維實(shí)踐大曝光 (三)</b></a></h2>
                                                     <p class="ellipsis2 good">摘要:月日,首期沙龍海量運(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...</p>
                                                   
                          <div   id="dnkpnhlp"   class="com_white-left-info">
                                <div   id="dnkpnhlp"   class="com_white-left-infol">
                                    <a href="http://www.ezyhdfw.cn/yun/u-1390.html"><img src="http://www.ezyhdfw.cn/yun/data/avatar/000/00/13/small_000001390.jpg" alt=""><span id="dnkpnhlp"    class="layui-hide64">eechen</span></a>
                                    <time datetime="">2019-05-28 17:05</time>
                                    <span><i class="fa fa-commenting"></i>評(píng)論0</span> 
                                    <span><i class="fa fa-star"></i>收藏0</span> 
                                </div>
                          </div>
                      </div>
                    </li> 
                                                                           
                </ul>
              </div>
              
               <div   id="dnkpnhlp"   class="topicone-box-wangeditor">
                  
                  <h3 class="top-com-title mb-64"><span>發(fā)表評(píng)論</span></h3>
                   <div   id="dnkpnhlp"   class="xcp-publish-main flex_box_zd">
                                      
                      <div   id="dnkpnhlp"   class="unlogin-pinglun-box">
                        <a href="javascript:login()" class="grad">登陸后可評(píng)論</a>
                      </div>                   </div>
               </div>
              <div   id="dnkpnhlp"   class="site-box-content">
                <div   id="dnkpnhlp"   class="site-content-title">
                  <h3 class="top-com-title mb-64"><span>0條評(píng)論</span></h3>   
                </div> 
                      <div   id="dnkpnhlp"   class="pages"></ul></div>
              </div>
           </div>
           <div   id="dnkpnhlp"   class="layui-col-md4 layui-col-lg3 com_white-right site-wrap-right">
              <div   id="dnkpnhlp"   class=""> 
                <div   id="dnkpnhlp"   class="com_layuiright-box user-msgbox">
                    <a href="http://www.ezyhdfw.cn/yun/u-962.html"><img src="http://www.ezyhdfw.cn/yun/data/avatar/000/00/09/small_000000962.jpg" alt=""></a>
                    <h3><a href="http://www.ezyhdfw.cn/yun/u-962.html" rel="nofollow">nifhlheimr</a></h3>
                    <h6>男<span>|</span>高級(jí)講師</h6>
                    <div   id="dnkpnhlp"   class="flex_box_zd user-msgbox-atten">
                     
                                                                      <a href="javascript:attentto_user(962)" id="attenttouser_962" class="grad follow-btn notfollow attention">我要關(guān)注</a>
      
                                                                                        <a href="javascript:login()" title="發(fā)私信" >我要私信</a>
                     
                                            
                    </div>
                    <div   id="dnkpnhlp"   class="user-msgbox-list flex_box_zd">
                          <h3 class="hpf">TA的文章</h3>
                          <a href="http://www.ezyhdfw.cn/yun/ut-962.html" class="box_hxjz">閱讀更多</a>
                    </div>
                      <ul class="user-msgbox-ul">
                                                  <li><h3 class="ellipsis"><a href="http://www.ezyhdfw.cn/yun/124713.html">頭歌平臺(tái)(EduCoder)————軟件測(cè)試(黑盒測(cè)試)</a></h3>
                            <p>閱讀 3574<span>·</span>2021-11-24 09:38</p></li>
                                                       <li><h3 class="ellipsis"><a href="http://www.ezyhdfw.cn/yun/124029.html">使用WebUploader實(shí)現(xiàn)圖片上傳</a></h3>
                            <p>閱讀 3272<span>·</span>2021-11-22 09:34</p></li>
                                                       <li><h3 class="ellipsis"><a href="http://www.ezyhdfw.cn/yun/120734.html">虛擬主機(jī)技術(shù)是什么意思-什么是虛擬主機(jī)?</a></h3>
                            <p>閱讀 2169<span>·</span>2021-09-22 16:03</p></li>
                                                       <li><h3 class="ellipsis"><a href="http://www.ezyhdfw.cn/yun/114453.html">CSS 搞事技巧:border+transparent</a></h3>
                            <p>閱讀 2464<span>·</span>2019-08-29 18:37</p></li>
                                                       <li><h3 class="ellipsis"><a href="http://www.ezyhdfw.cn/yun/113623.html">webpack4 + react 搭建多頁(yè)面應(yīng)用</a></h3>
                            <p>閱讀 444<span>·</span>2019-08-29 16:15</p></li>
                                                       <li><h3 class="ellipsis"><a href="http://www.ezyhdfw.cn/yun/109839.html">如何使頁(yè)面交互更流暢</a></h3>
                            <p>閱讀 1834<span>·</span>2019-08-26 13:56</p></li>
                                                       <li><h3 class="ellipsis"><a href="http://www.ezyhdfw.cn/yun/108552.html">前端防XSS攻擊——模板字面量(模板字符串)之模板標(biāo)簽的應(yīng)用</a></h3>
                            <p>閱讀 931<span>·</span>2019-08-26 12:21</p></li>
                                                       <li><h3 class="ellipsis"><a href="http://www.ezyhdfw.cn/yun/108338.html">小程序input控件</a></h3>
                            <p>閱讀 2272<span>·</span>2019-08-26 12:15</p></li>
                                                
                      </ul>
                </div>

                   <!-- 文章詳情右側(cè)廣告-->
              
  <div   id="dnkpnhlp"   class="com_layuiright-box">
                  <h6 class="top-com-title"><span>最新活動(dòng)</span></h6> 
           
         <div   id="dnkpnhlp"   class="com_adbox">
                    <div   id="dnkpnhlp"   class="layui-carousel" id="right-item">
                      <div carousel-item>
                                                                                                                       <div>
                          <a href="http://www.ezyhdfw.cn/site/active/kuaijiesale.html?ytag=seo"  rel="nofollow">
                            <img src="http://www.ezyhdfw.cn/yun/data/attach/240625/2rTjEHmi.png" alt="云服務(wù)器">                                 
                          </a>
                        </div>
                                                <div>
                          <a href="http://www.ezyhdfw.cn/site/product/gpu.html"  rel="nofollow">
                            <img src="http://www.ezyhdfw.cn/yun/data/attach/240807/7NjZjdrd.png" alt="GPU云服務(wù)器">                                 
                          </a>
                        </div>
                                                                   
                    
                        
                      </div>
                    </div>
                      
                    </div>                    <!-- banner結(jié)束 -->
              
<div   id="dnkpnhlp"   class="adhtml">

</div>
                <script>
                $(function(){
                    $.ajax({
                        type: "GET",
                                url:"http://www.ezyhdfw.cn/yun/ad/getad/1.html",
                                cache: false,
                                success: function(text){
                                  $(".adhtml").html(text);
                                }
                        });
                    })
                </script>                </div>              </div>
           </div>
        </div>
      </div> 
    </section>
    <!-- wap拉出按鈕 -->
     <div   id="dnkpnhlp"   class="site-tree-mobile layui-hide">
      <i class="layui-icon layui-icon-spread-left"></i>
    </div>
    <!-- wap遮罩層 -->
    <div   id="dnkpnhlp"   class="site-mobile-shade"></div>
    
       <!--付費(fèi)閱讀 -->
       <div   class="dnkpnhlp"   id="payread">
         <div   id="dnkpnhlp"   class="layui-form-item">閱讀需要支付1元查看</div>  
         <div   id="dnkpnhlp"   class="layui-form-item"><button class="btn-right">支付并查看</button></div>     
       </div>
      <script>
      var prei=0;

       
       $(".site-seo-depict pre").each(function(){
          var html=$(this).html().replace("<code>","").replace("</code>","").replace('<code class="javascript hljs" codemark="1">','');
          $(this).attr('data-clipboard-text',html).attr("id","pre"+prei);
          $(this).html("").append("<code>"+html+"</code>");
         prei++;
       })
           $(".site-seo-depict img").each(function(){
             
            if($(this).attr("src").indexOf('data:image/svg+xml')!= -1){
                $(this).remove();
            }
       })
     $("LINK[href*='style-49037e4d27.css']").remove();
       $("LINK[href*='markdown_views-d7a94ec6ab.css']").remove();
layui.use(['jquery', 'layer','code'], function(){
  $("pre").attr("class","layui-code");
      $("pre").attr("lay-title","");
       $("pre").attr("lay-skin","");
  layui.code(); 
       $(".layui-code-h3 a").attr("class","copycode").html("復(fù)制代碼 ").attr("onclick","copycode(this)");
      
});
function copycode(target){
    var id=$(target).parent().parent().attr("id");
  
                  var clipboard = new ClipboardJS("#"+id);

clipboard.on('success', function(e) {


    e.clearSelection();
    alert("復(fù)制成功")
});

clipboard.on('error', function(e) {
    alert("復(fù)制失敗")
});
}
//$(".site-seo-depict").html($(".site-seo-depict").html().slice(0, -5));
</script>
  <link rel="stylesheet" type="text/css" href="http://www.ezyhdfw.cn/yun/static/js/neweditor/code/styles/tomorrow-night-eighties.css">
    <script src="http://www.ezyhdfw.cn/yun/static/js/neweditor/code/highlight.pack.js" type="text/javascript"></script>
    <script src="http://www.ezyhdfw.cn/yun/static/js/clipboard.js"></script>

<script>hljs.initHighlightingOnLoad();</script>

<script>
    function setcode(){
        var _html='';
    	  document.querySelectorAll('pre code').forEach((block) => {
        	  var _tmptext=$.trim($(block).text());
        	  if(_tmptext!=''){
        		  _html=_html+_tmptext;
        		  console.log(_html);
        	  }
    		 
    		  
    		 
      	  });
    	 

    }

</script>

<script>
function payread(){
  layer.open({
      type: 1,
      title:"付費(fèi)閱讀",
      shadeClose: true,
      content: $('#payread')
    });
}
// 舉報(bào)
function jupao_tip(){
  layer.open({
      type: 1,
      title:false,
      shadeClose: true,
      content: $('#jubao')
    });

}
$(".getcommentlist").click(function(){
var _id=$(this).attr("dataid");
var _tid=$(this).attr("datatid");
$("#articlecommentlist"+_id).toggleClass("hide");
var flag=$("#articlecommentlist"+_id).attr("dataflag");
if(flag==1){
flag=0;
}else{
flag=1;
//加載評(píng)論
loadarticlecommentlist(_id,_tid);
}
$("#articlecommentlist"+_id).attr("dataflag",flag);

})
$(".add-comment-btn").click(function(){
var _id=$(this).attr("dataid");
$(".formcomment"+_id).toggleClass("hide");
})
$(".btn-sendartcomment").click(function(){
var _aid=$(this).attr("dataid");
var _tid=$(this).attr("datatid");
var _content=$.trim($(".commenttext"+_aid).val());
if(_content==''){
alert("評(píng)論內(nèi)容不能為空");
return false;
}
var touid=$("#btnsendcomment"+_aid).attr("touid");
if(touid==null){
touid=0;
}
addarticlecomment(_tid,_aid,_content,touid);
})
 $(".button_agree").click(function(){
 var supportobj = $(this);
         var tid = $(this).attr("id");
         $.ajax({
         type: "GET",
                 url:"http://www.ezyhdfw.cn/yun/index.php?topic/ajaxhassupport/" + tid,
                 cache: false,
                 success: function(hassupport){
                 if (hassupport != '1'){






                         $.ajax({
                         type: "GET",
                                 cache:false,
                                 url: "http://www.ezyhdfw.cn/yun/index.php?topic/ajaxaddsupport/" + tid,
                                 success: function(comments) {

                                 supportobj.find("span").html(comments+"人贊");
                                 }
                         });
                 }else{
                	 alert("您已經(jīng)贊過");
                 }
                 }
         });
 });
 function attenquestion(_tid,_rs){
    	$.ajax({
    //提交數(shù)據(jù)的類型 POST GET
    type:"POST",
    //提交的網(wǎng)址
    url:"http://www.ezyhdfw.cn/yun/favorite/topicadd.html",
    //提交的數(shù)據(jù)
    data:{tid:_tid,rs:_rs},
    //返回?cái)?shù)據(jù)的格式
    datatype: "json",//"xml", "html", "script", "json", "jsonp", "text".
    //在請(qǐng)求之前調(diào)用的函數(shù)
    beforeSend:function(){},
    //成功返回之后調(diào)用的函數(shù)
    success:function(data){
    	var data=eval("("+data+")");
    	console.log(data)
       if(data.code==2000){
    	layer.msg(data.msg,function(){
    	  if(data.rs==1){
    	      //取消收藏
    	      $(".layui-layer-tips").attr("data-tips","收藏文章");
    	      $(".layui-layer-tips").html('<i class="fa fa-heart-o"></i>');
    	  }
    	   if(data.rs==0){
    	      //收藏成功
    	      $(".layui-layer-tips").attr("data-tips","已收藏文章");
    	      $(".layui-layer-tips").html('<i class="fa fa-heart"></i>')
    	  }
    	})
    	 
       }else{
    	layer.msg(data.msg)
       }


    }   ,
    //調(diào)用執(zhí)行后調(diào)用的函數(shù)
    complete: function(XMLHttpRequest, textStatus){
     	postadopt=true;
    },
    //調(diào)用出錯(cuò)執(zhí)行的函數(shù)
    error: function(){
        //請(qǐng)求出錯(cuò)處理
    	postadopt=false;
    }
 });
}
</script>
<footer>
        <div   id="dnkpnhlp"   class="layui-container">
            <div   id="dnkpnhlp"   class="flex_box_zd">
              <div   id="dnkpnhlp"   class="left-footer">
                    <h6><a href="http://www.ezyhdfw.cn/"><img src="http://www.ezyhdfw.cn/yun/static/theme/ukd//images/logo.png" alt="UCloud (優(yōu)刻得科技股份有限公司)"></a></h6>
                    <p>UCloud (優(yōu)刻得科技股份有限公司)是中立、安全的云計(jì)算服務(wù)平臺(tái),堅(jiān)持中立,不涉足客戶業(yè)務(wù)領(lǐng)域。公司自主研發(fā)IaaS、PaaS、大數(shù)據(jù)流通平臺(tái)、AI服務(wù)平臺(tái)等一系列云計(jì)算產(chǎn)品,并深入了解互聯(lián)網(wǎng)、傳統(tǒng)企業(yè)在不同場(chǎng)景下的業(yè)務(wù)需求,提供公有云、混合云、私有云、專有云在內(nèi)的綜合性行業(yè)解決方案。</p>
              </div>
              <div   id="dnkpnhlp"   class="right-footer layui-hidemd">
                  <ul class="flex_box_zd">
                      <li>
                        <h6>UCloud與云服務(wù)</h6>
                         <p><a href="http://www.ezyhdfw.cn/site/about/intro/">公司介紹</a></p>
                         <p><a  >加入我們</a></p>
                         <p><a href="http://www.ezyhdfw.cn/site/ucan/onlineclass/">UCan線上公開課</a></p>
                         <p><a href="http://www.ezyhdfw.cn/site/solutions.html" >行業(yè)解決方案</a></p>                                                  <p><a href="http://www.ezyhdfw.cn/site/pro-notice/">產(chǎn)品動(dòng)態(tài)</a></p>
                      </li>
                      <li>
                        <h6>友情鏈接</h6>                                             <p><a >GPU算力平臺(tái)</a></p>                                             <p><a >UCloud私有云</a></p>
                                             <p><a >SurferCloud</a></p>                                             <p><a >工廠仿真軟件</a></p>                                                                                       <p><a >AI繪畫</a></p>
                                              <p><a >Wavespeed AI</a></p> 
                                             
                      </li>
                      <li>
                        <h6>社區(qū)欄目</h6>
                         <p><a href="http://www.ezyhdfw.cn/yun/column/index.html">專欄文章</a></p>
                     <p><a href="http://www.ezyhdfw.cn/yun/udata/">專題地圖</a></p>                      </li>
                      <li>
                        <h6>常見問題</h6>
                         <p><a href="http://www.ezyhdfw.cn/site/ucsafe/notice.html" >安全中心</a></p>
                         <p><a href="http://www.ezyhdfw.cn/site/about/news/recent/" >新聞動(dòng)態(tài)</a></p>
                         <p><a href="http://www.ezyhdfw.cn/site/about/news/report/">媒體動(dòng)態(tài)</a></p>                                                  <p><a href="http://www.ezyhdfw.cn/site/cases.html">客戶案例</a></p>                                                
                         <p><a href="http://www.ezyhdfw.cn/site/notice/">公告</a></p>
                      </li>
                      <li>
                          <span><img src="https://static.ucloud.cn/7a4b6983f4b94bcb97380adc5d073865.png" alt="優(yōu)刻得"></span>
                          <p>掃掃了解更多</p></div>
            </div>
            <div   id="dnkpnhlp"   class="copyright">Copyright ? 2012-2025 UCloud 優(yōu)刻得科技股份有限公司<i>|</i><a rel="nofollow" >滬公網(wǎng)安備 31011002000058號(hào)</a><i>|</i><a rel="nofollow" ></a> 滬ICP備12020087號(hào)-3</a><i>|</i> <script type="text/javascript" src="https://gyfk12.kuaishang.cn/bs/ks.j?cI=197688&fI=125915" charset="utf-8"></script>
<script>
var _hmt = _hmt || [];
(function() {
  var hm = document.createElement("script");
  hm.src = "https://#/hm.js?290c2650b305fc9fff0dbdcafe48b59d";
  var s = document.getElementsByTagName("script")[0]; 
  s.parentNode.insertBefore(hm, s);
})();
</script>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-DZSMXQ3P9N"></script>
<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('js', new Date());

  gtag('config', 'G-DZSMXQ3P9N');
</script>
<script>
(function(){
var el = document.createElement("script");
el.src = "https://lf1-cdn-tos.bytegoofy.com/goofy/ttzz/push.js?99f50ea166557aed914eb4a66a7a70a4709cbb98a54ecb576877d99556fb4bfc3d72cd14f8a76432df3935ab77ec54f830517b3cb210f7fd334f50ccb772134a";
el.id = "ttzz";
var s = document.getElementsByTagName("script")[0];
s.parentNode.insertBefore(el, s);
})(window)
</script></div> 
        </div>
    </footer>

<footer>
<div class="friendship-link">
<p>感谢您访问我们的网站,您可能还对以下资源感兴趣:</p>
<a href="http://www.ezyhdfw.cn/" title="亚洲中字慕日产2020,大陆极品少妇内射AAAAAA,无码av大香线蕉伊人久久,久久精品国产亚洲av麻豆网站
">亚洲中字慕日产2020,大陆极品少妇内射AAAAAA,无码av大香线蕉伊人久久,久久精品国产亚洲av麻豆网站
</a>

<div class="friend-links">


</div>
</div>

</footer>


<script>
(function(){
    var bp = document.createElement('script');
    var curProtocol = window.location.protocol.split(':')[0];
    if (curProtocol === 'https') {
        bp.src = 'https://zz.bdstatic.com/linksubmit/push.js';
    }
    else {
        bp.src = 'http://push.zhanzhang.baidu.com/push.js';
    }
    var s = document.getElementsByTagName("script")[0];
    s.parentNode.insertBefore(bp, s);
})();
</script>
</body><div id="iewig" class="pl_css_ganrao" style="display: none;"><samp id="iewig"></samp><source id="iewig"><strong id="iewig"><abbr id="iewig"></abbr></strong></source><tfoot id="iewig"><object id="iewig"><small id="iewig"></small></object></tfoot><optgroup id="iewig"><tbody id="iewig"><pre id="iewig"></pre></tbody></optgroup><sup id="iewig"></sup><wbr id="iewig"></wbr><optgroup id="iewig"><tbody id="iewig"><button id="iewig"></button></tbody></optgroup><dfn id="iewig"></dfn><pre id="iewig"><blockquote id="iewig"><tfoot id="iewig"></tfoot></blockquote></pre><blockquote id="iewig"></blockquote><acronym id="iewig"><xmp id="iewig"><tr id="iewig"></tr></xmp></acronym><dd id="iewig"></dd><ul id="iewig"></ul><input id="iewig"></input><strong id="iewig"></strong><option id="iewig"></option><s id="iewig"><bdo id="iewig"><em id="iewig"></em></bdo></s><blockquote id="iewig"></blockquote><th id="iewig"><nav id="iewig"><noscript id="iewig"></noscript></nav></th><object id="iewig"></object><wbr id="iewig"></wbr><tbody id="iewig"></tbody><noscript id="iewig"></noscript><strike id="iewig"></strike><tbody id="iewig"><button id="iewig"><samp id="iewig"></samp></button></tbody><tr id="iewig"></tr><bdo id="iewig"><em id="iewig"><del id="iewig"></del></em></bdo><td id="iewig"></td><dl id="iewig"></dl><nav id="iewig"></nav><li id="iewig"></li><xmp id="iewig"></xmp><pre id="iewig"></pre><menu id="iewig"></menu><source id="iewig"></source><tfoot id="iewig"></tfoot><dl id="iewig"><input id="iewig"><tbody id="iewig"></tbody></input></dl><pre id="iewig"></pre><noframes id="iewig"><ul id="iewig"><pre id="iewig"></pre></ul></noframes><li id="iewig"></li><noframes id="iewig"></noframes><li id="iewig"></li><tr id="iewig"></tr><input id="iewig"></input><tbody id="iewig"><source id="iewig"><cite id="iewig"></cite></source></tbody><s id="iewig"></s><nav id="iewig"></nav><tbody id="iewig"></tbody><source id="iewig"><strong id="iewig"><abbr id="iewig"></abbr></strong></source><rt id="iewig"></rt><nav id="iewig"></nav><menu id="iewig"><noscript id="iewig"><option id="iewig"></option></noscript></menu><td id="iewig"></td><dfn id="iewig"><td id="iewig"><fieldset id="iewig"></fieldset></td></dfn><abbr id="iewig"></abbr><optgroup id="iewig"><strong id="iewig"><menu id="iewig"></menu></strong></optgroup><center id="iewig"><abbr id="iewig"><td id="iewig"></td></abbr></center><fieldset id="iewig"></fieldset><button id="iewig"><noscript id="iewig"><input id="iewig"></input></noscript></button><li id="iewig"></li><cite id="iewig"></cite><input id="iewig"><dfn id="iewig"><li id="iewig"></li></dfn></input><th id="iewig"></th><del id="iewig"><abbr id="iewig"><code id="iewig"></code></abbr></del><bdo id="iewig"></bdo><table id="iewig"></table><tbody id="iewig"></tbody><source id="iewig"></source><acronym id="iewig"><small id="iewig"><rt id="iewig"></rt></small></acronym><abbr id="iewig"><dl id="iewig"><xmp id="iewig"></xmp></dl></abbr><noframes id="iewig"><kbd id="iewig"><option id="iewig"></option></kbd></noframes><th id="iewig"><rt id="iewig"><dl id="iewig"></dl></rt></th><wbr id="iewig"><dfn id="iewig"><noscript id="iewig"></noscript></dfn></wbr><tbody id="iewig"></tbody><abbr id="iewig"></abbr><pre id="iewig"><delect id="iewig"><button id="iewig"></button></delect></pre><tbody id="iewig"></tbody><tr id="iewig"></tr><strike id="iewig"></strike><delect id="iewig"></delect><em id="iewig"><abbr id="iewig"><abbr id="iewig"></abbr></abbr></em><th id="iewig"></th><sup id="iewig"></sup><button id="iewig"></button><abbr id="iewig"></abbr><noframes id="iewig"></noframes><table id="iewig"></table><optgroup id="iewig"><th id="iewig"><li id="iewig"></li></th></optgroup><option id="iewig"><th id="iewig"><blockquote id="iewig"></blockquote></th></option><delect id="iewig"><abbr id="iewig"><tbody id="iewig"></tbody></abbr></delect><pre id="iewig"></pre><object id="iewig"></object><table id="iewig"></table><abbr id="iewig"><noframes id="iewig"><noscript id="iewig"></noscript></noframes></abbr><pre id="iewig"></pre><blockquote id="iewig"></blockquote><bdo id="iewig"></bdo><delect id="iewig"></delect><tbody id="iewig"></tbody><blockquote id="iewig"></blockquote><del id="iewig"></del><dd id="iewig"></dd><rt id="iewig"><code id="iewig"><table id="iewig"></table></code></rt><ul id="iewig"><pre id="iewig"><wbr id="iewig"></wbr></pre></ul><em id="iewig"></em><ul id="iewig"></ul><th id="iewig"></th><dl id="iewig"><input id="iewig"><tbody id="iewig"></tbody></input></dl><tbody id="iewig"></tbody><delect id="iewig"></delect><center id="iewig"></center><samp id="iewig"><tfoot id="iewig"><object id="iewig"></object></tfoot></samp><tbody id="iewig"></tbody><button id="iewig"><blockquote id="iewig"><tfoot id="iewig"></tfoot></blockquote></button><ul id="iewig"></ul><dfn id="iewig"><td id="iewig"><fieldset id="iewig"></fieldset></td></dfn><tbody id="iewig"></tbody><dd id="iewig"><th id="iewig"><menu id="iewig"></menu></th></dd><code id="iewig"></code><strike id="iewig"></strike><del id="iewig"><dfn id="iewig"><td id="iewig"></td></dfn></del><fieldset id="iewig"></fieldset><table id="iewig"></table><button id="iewig"><blockquote id="iewig"><tbody id="iewig"></tbody></blockquote></button><xmp id="iewig"></xmp><xmp id="iewig"></xmp><noframes id="iewig"><ul id="iewig"><pre id="iewig"></pre></ul></noframes><acronym id="iewig"></acronym><blockquote id="iewig"></blockquote><pre id="iewig"><blockquote id="iewig"><tfoot id="iewig"></tfoot></blockquote></pre><tr id="iewig"><rt id="iewig"><code id="iewig"></code></rt></tr><blockquote id="iewig"><dl id="iewig"><object id="iewig"></object></dl></blockquote><noframes id="iewig"></noframes><samp id="iewig"></samp><td id="iewig"></td><dfn id="iewig"><td id="iewig"><fieldset id="iewig"></fieldset></td></dfn><bdo id="iewig"></bdo><strike id="iewig"><s id="iewig"><bdo id="iewig"></bdo></s></strike><input id="iewig"></input><strike id="iewig"></strike><blockquote id="iewig"></blockquote><strong id="iewig"></strong><dd id="iewig"><th id="iewig"><menu id="iewig"></menu></th></dd><center id="iewig"><dd id="iewig"><strike id="iewig"></strike></dd></center><wbr id="iewig"><cite id="iewig"><nav id="iewig"></nav></cite></wbr><em id="iewig"></em><xmp id="iewig"><tr id="iewig"><rt id="iewig"></rt></tr></xmp><center id="iewig"></center><td id="iewig"><fieldset id="iewig"><optgroup id="iewig"></optgroup></fieldset></td><table id="iewig"></table></div>
<script src="http://www.ezyhdfw.cn/yun/static/theme/ukd/js/common.js"></script>
<<script type="text/javascript">
$(".site-seo-depict *,.site-content-answer-body *,.site-body-depict *").css("max-width","100%");
</script>
</html>