前言
哈嘍,大家好,我是海怪。
最近在組里我又領(lǐng)了一個(gè)新任務(wù):前端單元測(cè)試。
關(guān)于這個(gè)話題在很早的時(shí)候就想和大家聊了,奈何一直沒(méi)機(jī)會(huì)。對(duì)于我個(gè)人來(lái)說(shuō),我是非常喜歡寫單測(cè)的。最近還買了本《軟件測(cè)試》的書,算是再次復(fù)習(xí)一下大學(xué)時(shí)學(xué)過(guò)的專業(yè)課,平時(shí)在搗鼓一些個(gè)人項(xiàng)目的時(shí)候也會(huì)做一些基礎(chǔ)的單測(cè)。

一談到單測(cè),可能大家的第一反應(yīng)都是敬而遠(yuǎn)之。
沒(méi)啥用,沒(méi)時(shí)間,我不會(huì)
我承認(rèn)寫單測(cè)是個(gè)非常有挑戰(zhàn)性,且難度不小的活,但 我依然推薦大家嘗試去寫一寫單元測(cè)試,因?yàn)樗鶐?lái)的好處不僅僅是大家想的那么簡(jiǎn)單:“只是 Bug 少了一點(diǎn)”。 所以,今天我會(huì)嘗試從另外一些角度來(lái)討論單測(cè)可以給我們帶來(lái)哪些好處。
優(yōu)化流程
接著剛剛說(shuō)到的 “只是 Bug 少一點(diǎn)” 這句話,可能大多數(shù)覺(jué)得單測(cè)就是在提測(cè)前減少一點(diǎn) Bug 而已:

這樣的想法確實(shí)是最直觀的。但這只是想到了第一層,如果我們把 開(kāi)發(fā)流程所有步驟 都加進(jìn)來(lái),會(huì)發(fā)現(xiàn)是這樣的:

在 開(kāi)發(fā)過(guò)程 后面,幾乎每個(gè)流程都可能拋出 Bug。越是到后面流程才拋出的 Bug,程序員就越是要投入比開(kāi)發(fā)階段更大的時(shí)間和業(yè)務(wù),而且所承受的風(fēng)險(xiǎn)也是最高的。
或許大家會(huì)想:不就改個(gè) Bug,改幾行而已。 可是大家有沒(méi)有想過(guò)在跟測(cè)的過(guò)程中,很可能你已經(jīng)開(kāi)始另一個(gè)需求的評(píng)審了! 此時(shí)的你在解決突然插入的 Bug 的時(shí)候,心態(tài)還會(huì)像剛開(kāi)始寫代碼時(shí)候那么輕松么?
實(shí)際上,還有更多的隱性成本沒(méi)有考慮,比如反復(fù)確認(rèn)產(chǎn)品邏輯、反復(fù)確認(rèn)交互設(shè)計(jì)、反復(fù)確認(rèn)前后端接口設(shè)計(jì)、各端對(duì)產(chǎn)品的理解。 有的時(shí)候,你就會(huì)發(fā)現(xiàn)這樣很魔幻的場(chǎng)景:明明是一個(gè)字段的展示問(wèn)題,竟然要花上一上午,拉了 4、5 個(gè)人來(lái)開(kāi)會(huì)核對(duì)的情況。
下面這張圖,也在說(shuō)明兩個(gè)問(wèn)題:一是 85% 的缺陷都在代碼設(shè)計(jì)階段產(chǎn)生;二是發(fā)現(xiàn) Bug 的階段越靠后,耗費(fèi)成本就越高,呈指數(shù)級(jí)別的增長(zhǎng)。這種 “指數(shù)成本” 的案例也經(jīng)常發(fā)生,當(dāng)我們改正一個(gè) Bug 的時(shí)候,可能隨之而來(lái)又會(huì)多出 3 個(gè) Bug,俗稱:改崩了。

所以,在早期的單元測(cè)試就能發(fā)現(xiàn)bug,不僅可以省時(shí)省力,在開(kāi)發(fā)流程上提高效率,也能降低反復(fù)修改出現(xiàn)的風(fēng)險(xiǎn)和時(shí)間成本。
保證質(zhì)量
這一節(jié)主題就是大家經(jīng)常想的:減少 Bug 率。我們不妨來(lái)想一個(gè)問(wèn)題:什么才是 Bug? 相信所有開(kāi)發(fā)人員都不愿意寫 Bug,在 《軟件測(cè)試》 這本書中將 Bug 描述成 “軟件缺陷”,里面說(shuō)道:
大多數(shù)的 “軟件缺陷” 并非源自編程錯(cuò)誤,對(duì)眾多從小到大的項(xiàng)目進(jìn)行研究而得出的結(jié)論往往是一致的,導(dǎo)致軟件缺陷最大的原因是產(chǎn)品說(shuō)明書!見(jiàn)下圖

大多數(shù)的產(chǎn)品還是能夠?qū)懗鲆环萸逦髁说男枨髥蔚?,奈?ta 也不可能把所有情況想都枚舉出來(lái),這也導(dǎo)致了開(kāi)發(fā)時(shí)很容易出現(xiàn)考慮不周全的情況。往往能夠發(fā)現(xiàn)異常情況的人要么是測(cè)試、要么是交互視覺(jué)、要么是后期產(chǎn)品體驗(yàn)。 那到這個(gè)時(shí)候才發(fā)現(xiàn)的問(wèn)題,然后再去修復(fù)又會(huì)出現(xiàn)的 指數(shù)爆炸的成本。
如果把實(shí)現(xiàn)功能看成走迷宮,把找到通路看成上線需求, 那么編碼實(shí)現(xiàn)的過(guò)程就像從入口找出口,而單元測(cè)試則像從出口找入口。 這種開(kāi)兩個(gè)線程 “雙向奔赴” 的找通路方法能夠用最精準(zhǔn)最快的方式找到通路。

單測(cè)所保障的不僅僅只是代碼的正確性,畢竟大家在邊開(kāi)發(fā)邊 Debug 的時(shí)候已經(jīng)能驗(yàn)證 99% 的正確性了,而單測(cè)更大的地方在于 讓我們不得不去思考到一些異常情況 ,這無(wú)形中就能增強(qiáng)代碼的質(zhì)量。
優(yōu)化更新項(xiàng)目的后盾
可能大家對(duì)上面這一節(jié)也不以為意,我能理解大家的僥幸心理。畢竟在公司里,開(kāi)發(fā)寫完 Bug,然后交給測(cè)試找出來(lái)是大家其樂(lè)融融表現(xiàn)。而且不寫測(cè)試大家過(guò)得還挺好的,也沒(méi)出什么大亂子。
造成這樣的錯(cuò)覺(jué)在兩個(gè)方面:一是測(cè)試找 Bug,開(kāi)發(fā)再 Debug,這確實(shí)能解決燃眉之急,短期內(nèi)很有效果。二是需求一直不斷快速迭代,一期的 Bug,二期還能合著去改,二期改不了還有三期,三期結(jié)束了還有四期...... 。
這種永無(wú)止境的測(cè)試 + 開(kāi)發(fā)模式能在一定程度上讓我們的代碼 “看起來(lái)是有保障的” 。

人肉測(cè)試固然好用,但是也有下面的缺點(diǎn):
- 使用一次成本非常高
- 回歸測(cè)試成本更高
- 只有到上線功能的時(shí)候才會(huì)使用一次人力測(cè)試來(lái)轟炸
由于成本很高,人肉測(cè)試一般只會(huì)用來(lái)測(cè)業(yè)務(wù)功能,并沒(méi)有太多測(cè)試資源可以分配到優(yōu)化需求、技術(shù)需求上。 所以對(duì)于這類需求只能通過(guò)前端開(kāi)發(fā)人員自測(cè),到目前為止也只是優(yōu)化一個(gè)點(diǎn),然后點(diǎn)點(diǎn)鼠標(biāo)來(lái)自測(cè),效率并不高。一旦優(yōu)化過(guò)程中改出了問(wèn)題,回滾、和修復(fù)的成本又會(huì)非常高,這也會(huì)助長(zhǎng)大家 “不敢優(yōu)化”、“能不動(dòng)就不動(dòng)” 的思想。

如果能有一定量的測(cè)試,則有足夠強(qiáng)大的信心來(lái)支撐項(xiàng)目的優(yōu)化,也有助于整個(gè)項(xiàng)目的未來(lái)發(fā)展和改進(jìn)。
測(cè)試驅(qū)動(dòng)開(kāi)發(fā)
測(cè)試驅(qū)動(dòng)開(kāi)發(fā)(Testing-Driven Development)是敏捷開(kāi)發(fā)中的一項(xiàng)核心實(shí)踐和技術(shù),也是一種設(shè)計(jì)方法論。
上面說(shuō)的單測(cè)特點(diǎn)比較偏向于 “防守”,而 TDD 中的測(cè)試則偏向于 “進(jìn)攻”。 TDD 的原理是在開(kāi)發(fā)功能代碼之前,先編寫單元測(cè)試用例代碼,在此基礎(chǔ)上再補(bǔ)充產(chǎn)品代碼。比如要實(shí)現(xiàn) getUserById 這個(gè)服務(wù),那么可以先寫如下測(cè)試,然后再補(bǔ)充 getUserById 的實(shí)現(xiàn):
describe('getUserById', () => {
it('可以根據(jù) id 返回用戶信息', () => {
// TODO: getUserById 未實(shí)現(xiàn)
const user = getUserById('122');
expect(user.id).toEqual('122');
})
})
這種方法在 Node 端非常實(shí)用。由于 Node 端要依賴的項(xiàng)非常多,比如數(shù)據(jù)庫(kù)、各方接口、配置中心等等。每次用 Postman 去測(cè)接口,就會(huì)一次性將多個(gè)模塊以及服務(wù)一起測(cè)了。 如果別的服務(wù)還在開(kāi)發(fā)或者有問(wèn)題,就會(huì)直接阻塞了接口的開(kāi)發(fā)。

雖然 Postman 在接口測(cè)試的時(shí)候很好用,但是它也有如下缺點(diǎn):
- 用例不足。 由于 Postman 一般只做簡(jiǎn)單的接口測(cè)試,并不像單測(cè)那樣會(huì)把所有分支情況都枚舉
- 用例無(wú)法共享。 雖然 Postman 也能寫簡(jiǎn)單的用例,但是現(xiàn)在每個(gè)人的 Postman 會(huì)有自己的用例,難以覆蓋所有情況
- 用例無(wú)法保鮮。 當(dāng)接口更新了之后,Postman 的用例可能存在過(guò)期的情況
單元測(cè)試則很好地填補(bǔ)了這一塊,利用單測(cè)強(qiáng)大的 Mock 能力先將依賴項(xiàng)都 Mock 掉,開(kāi)發(fā)時(shí)可以只關(guān)注某個(gè)函數(shù)、服務(wù)的開(kāi)發(fā),不會(huì)受其依賴項(xiàng)干擾:

由于每次提交代碼都應(yīng)該保證測(cè)試通過(guò)率 100%,所以我們也不會(huì)擔(dān)心這些例子是否過(guò)期的問(wèn)題。
用例即例子
測(cè)試用例還有個(gè)很好的功能:將使用案例記錄在案。
很多時(shí)候別人寫一些工具函數(shù)和方法,使用者是不能一眼就能學(xué)會(huì)怎么用的。往往這時(shí)寫函數(shù)的人就會(huì)說(shuō):你看 XXX 文件就知道怎么用了。 但這些 “真實(shí)例子” 中通常會(huì)夾雜著很多依賴項(xiàng),無(wú)法作用一個(gè)最小 Use Case 來(lái)理解。

而單測(cè)里的每個(gè)用例都可以看成一個(gè)最小的 example,通過(guò)閱讀 Test Case 就能馬上知道這個(gè)函數(shù)怎么使用了。 這里舉 redux 的 compose 函數(shù)的例子:
describe('Utils', () => {
describe('compose', () => {
it('composes from right to left', () => {
const double = (x: number) => x * 2
const square = (x: number) => x * x
expect(compose(square)(5)).toBe(25)
expect(compose(square, double)(5)).toBe(100)
expect(compose(double, square, double)(5)).toBe(200)
})
})
}
就算我們不知道 compose 是用來(lái)干嘛的,但是我們很清楚地知道,使用方法就是從右到左地執(zhí)行回調(diào)。
由于每次發(fā)布時(shí)我們都要保證單測(cè) 100% 通過(guò)率,所以永遠(yuǎn)不用擔(dān)心這個(gè) Use Case 無(wú)法使用、過(guò)期的問(wèn)題。
提升個(gè)人能力
拋開(kāi)這些項(xiàng)目質(zhì)量、優(yōu)化流程的原因,推薦大家寫單測(cè)的另一重要原因就是 提升個(gè)人能力。
幾乎所有 Jest 的入門文章的開(kāi)頭都會(huì)有一個(gè)非常簡(jiǎn)單的 Test Case:
expect(1 + 1).toEqual(2)
這很容易讓人誤以為單測(cè)很簡(jiǎn)單,以為不就是學(xué)一個(gè)框架那樣嘛。然而,只有在真正編寫測(cè)試用例的時(shí)候才會(huì)發(fā)現(xiàn)單測(cè)的難度呈指數(shù)級(jí)上漲。 因?yàn)闇y(cè)試的本身是另一個(gè)領(lǐng)域,是需要通過(guò)不斷練習(xí)才能掌握測(cè)試技巧的。 對(duì)前端單測(cè)來(lái)說(shuō),它的難度包括但不限于如下幾點(diǎn):
- 測(cè)試框架與開(kāi)發(fā)框架的不配合。 比如版本沖突問(wèn)題
-
模擬環(huán)境問(wèn)題。 比如模擬瀏覽器環(huán)境,往往項(xiàng)目一出現(xiàn)
localStorage,cookie這些瀏覽器獨(dú)有的東西時(shí),Jest 就會(huì)報(bào)錯(cuò),很多人受不了直接放棄了 -
不同框架、庫(kù)的測(cè)試方法都是需要學(xué)習(xí)的。有的框架 Nest.js 有
@nestjs/testing,React.js 有react-testing-library。有的庫(kù) Redux 又會(huì)有自己獨(dú)特的 testing guide
總的來(lái)說(shuō),寫單測(cè)并不像大家想的這么簡(jiǎn)單,jest 只是個(gè)開(kāi)始的地方。不過(guò),從另一個(gè)角度來(lái)看,如果你能堅(jiān)持寫好單測(cè),對(duì)個(gè)人能力也大有裨益:
- 提升不同環(huán)境的 Mock 能力。 掌握不同測(cè)試框架的測(cè)試技巧
- 提升異常分支的感知能力。 寫代碼的時(shí)候也能代入測(cè)試者視角,在開(kāi)發(fā)時(shí)能馬上發(fā)現(xiàn)并處理異常分支
- 了解并實(shí)踐更多的測(cè)試策略。 如自上而下、自下而上,影子數(shù)據(jù)庫(kù)等
- 為簡(jiǎn)歷增光添彩。 在寫測(cè)試的過(guò)程中我們也可以深入測(cè)試這個(gè)領(lǐng)域,將編程知識(shí)融會(huì)貫通
總結(jié)
稍微總結(jié)一下,單測(cè)可以在 優(yōu)化開(kāi)發(fā)流程、保證項(xiàng)目質(zhì)量、給項(xiàng)目?jī)?yōu)化上保險(xiǎn)、驅(qū)動(dòng)開(kāi)發(fā)、提供 Use Case、提升個(gè)人能力 方面有著非常大的益處。
當(dāng)然,本文也并非要讓大家馬上給項(xiàng)目上單測(cè),只是希望大家能夠多嘗試自己領(lǐng)域之外的東西,不要固步自封。對(duì)個(gè)人而言,多練習(xí)寫單測(cè)能力肯定是好處多于壞處。
好了,這篇文章就給大家?guī)У竭@里。如果你喜歡我的文章,可以來(lái)一波關(guān)注,一鍵三連我也不介意,比心 ??