前端單測(cè),我們應(yīng)該測(cè)什么?
相信很多前端開發(fā)在寫單測(cè)的時(shí)候,最大的問題就是:“我應(yīng)該測(cè)什么東西?” 沒錯(cuò),解決問題不是最難的,發(fā)現(xiàn)問題才是!知道要測(cè)哪個(gè)遠(yuǎn)比怎么測(cè)重要很多!
今天看了 Kent 博客的 《How to know what to test》 的這篇博客,感覺醍醐灌頂,今天就把這篇博文也分享給大家。
正片開始
知道如何做測(cè)試很好,也很重要的。我之前就教過很多人測(cè)試的基礎(chǔ)知識(shí)、如何配置工具、如何針對(duì)不用情況寫好測(cè)試,等等。但是知道如何測(cè)試只是成功的一半,知道要測(cè)什么才是更重要的另一半。
永遠(yuǎn)記住為什么我們要測(cè)試
我們寫測(cè)試是因?yàn)橐_保我們的應(yīng)用程序在用戶使用它們時(shí)能夠正常工作。 有些人可能會(huì)用測(cè)試用例來提高工作流的效率,但我對(duì)提高代碼信心更有興趣,即:我們的測(cè)試應(yīng)該能直接增強(qiáng)我們的代碼信心。這也是我希望你在編寫測(cè)試時(shí)要考慮的重點(diǎn):
別太糾結(jié)于正在測(cè)試的代碼,而要多考慮這些代碼能夠支持的真實(shí)用例。
如果你只考慮代碼本身,很容易、也很自然地走向測(cè)試代碼細(xì)節(jié)的不歸路。我們應(yīng)該要考慮那些更接近用戶的真實(shí)使用場(chǎng)景來寫測(cè)試。
Code Coverage < Use Case Coverage
在做測(cè)試時(shí),代碼覆蓋率是表示我們的代碼有多行被執(zhí)行的一個(gè)指標(biāo)。舉下面這個(gè)例子:
function arrayify(maybeArray) {
if (Array.isArray(maybeArray)) {
return maybeArray
} else if (!maybeArray) {
return []
} else {
return [maybeArray]
}
}
現(xiàn)在,我們還沒有給這個(gè)函數(shù)寫測(cè)試,所以這個(gè)函數(shù)的覆蓋率為 0%。這種情況下的代碼覆蓋率報(bào)告可以讓我們知道:得馬上寫測(cè)試了,但它沒有告訴我們這個(gè)函數(shù)有哪些重要的部分,也沒有告訴我們這個(gè)函數(shù)支持的真實(shí)用例(正是我們?cè)趯憸y(cè)試時(shí)最要重點(diǎn)關(guān)注的內(nèi)容)是哪些。
實(shí)際上,當(dāng)我們?cè)诳紤]應(yīng)該對(duì)整個(gè)應(yīng)用中哪些部分做測(cè)試時(shí),覆蓋率報(bào)告對(duì)于 “我們應(yīng)該在哪部分投入更多時(shí)間” 這個(gè)問題幫助不是很大。
覆蓋率報(bào)告只能幫助我們知道哪些代碼還沒納入測(cè)試。所以,當(dāng)你看著這份覆蓋率報(bào)告時(shí),你不要總想著那些 if/else、循環(huán)或者生命周期,而是要問問自己:
這幾行代碼實(shí)現(xiàn)對(duì)應(yīng)的是哪些使用用例?我應(yīng)該要加哪些測(cè)試用例來覆蓋它們?
“使用用例覆蓋率” 可以告訴我們當(dāng)前測(cè)試支持了哪些使用用例??上У氖?,現(xiàn)在并沒有類似 “使用用例覆蓋率報(bào)告” 這么一說。我們只能自己實(shí)現(xiàn)。不過,代碼覆蓋率報(bào)告有時(shí)候也能告訴我們哪些使用用例沒有覆蓋到。
舉上面函數(shù)為例子,看到它的第一眼,我們就能馬上想到它的第一個(gè)真實(shí)用例:“傳入數(shù)組則返回?cái)?shù)組”。這就可以作為我們測(cè)試用例的標(biāo)題了:
test('傳入數(shù)組則返回?cái)?shù)組', () => { expect(arrayify(['Elephant', 'Giraffe'])).toEqual(['Elephant', 'Giraffe'])})
有了上面的測(cè)試用例,我們的覆蓋情況如下所示(高亮部分為覆蓋部分):
現(xiàn)在,讓我們來看看還沒被覆蓋的那部分,然后發(fā)現(xiàn)還有兩種 Use Case 還沒支持:
- 傳入 falsy 值,則返回空數(shù)組
- 傳入非 falsy 值且不是數(shù)組時(shí),返回一個(gè)數(shù)組,其中包含的輸入值
現(xiàn)在再來把測(cè)試用例都加上,然后再來看覆蓋情況:
test('傳入 falsy 值,則返回空數(shù)組', () => {
expect(arrayify()).toEqual([])
})
馬上就可以覆蓋完了!
test(`傳入非 falsy 值也不是數(shù)組時(shí),返回一個(gè)數(shù)組,其中包含的輸入值`, () => { expect(arrayify('Leopard')).toEqual(['Leopard'])})
好了,現(xiàn)在只要我們保證不改變這個(gè)函數(shù)的這些使用方法,那么我們有信心地說:這些測(cè)試都是能通過的。
代碼覆蓋率并不是一個(gè)完美的指標(biāo),但它卻能幫助我們制作自己的 “使用用例覆蓋率”。
代碼覆蓋率也能隱藏使用用例
有的時(shí)候,代碼覆蓋率是 100%,但不意味著使用用例也被覆蓋了 100%。這就是為什么我有時(shí)候在寫測(cè)試前都會(huì)把所有的使用用例想清楚。
舉個(gè)例子,假設(shè)有一個(gè) arrayify 函數(shù):
test(`傳入非 falsy 值也不是數(shù)組時(shí),返回一個(gè)數(shù)組,其中包含的輸入值`, () => {
expect(arrayify('Leopard')).toEqual(['Leopard'])
})
我們用這兩個(gè)用例來實(shí)現(xiàn) 100% 的代碼覆蓋:
- 輸入數(shù)組,返回?cái)?shù)組
- 輸入非數(shù)組,返回?cái)?shù)組,其中包含輸入內(nèi)容
如果我們來思考一下真實(shí)的使用用例,會(huì)發(fā)現(xiàn)少了一種 Case:
- 輸入 Falsy 值,返回空數(shù)組
如果用戶直接用 arrayify(),那么這樣的測(cè)試用例就不能很好地給足我們代碼的信心了。雖然現(xiàn)在看起來還行,就算不給這個(gè) Case 寫測(cè)試,我們的代碼也支持這樣的用例,但是,之所以我們要寫測(cè)試,是因?yàn)槲覀円_保做了代碼變更之后,它都能支持我們想要的使用用例。
我們繼續(xù)來看這樣做測(cè)試的后果:假如現(xiàn)在有人看到這一行 filter(Boolean) ,然后覺得:這是哪個(gè) SB 想到的奇葩寫法。最終把這里去掉了。然而,我們的測(cè)試依舊是可以通過的,但所有依賴 “輸入 falsy 值” 的這個(gè) Case 的代碼就都掛了。
要對(duì)使用用例做測(cè)試,而不是代碼
如何應(yīng)用到 React 代碼的測(cè)試?
在寫測(cè)試時(shí),你應(yīng)該時(shí)刻想著要支持兩種用戶:真實(shí)用戶和開發(fā)者。 再啰嗦一句,如果做測(cè)試的時(shí)候,你還是一直想著業(yè)務(wù)代碼而不是真實(shí)用例,就會(huì)很容易陷入測(cè)試 “代碼實(shí)現(xiàn)細(xì)節(jié)” 的陷阱。而這么做的后果是,你的代碼會(huì)無形中創(chuàng)造第三種用戶:Test User。
很多人在做 React 代碼測(cè)試時(shí),經(jīng)常會(huì)想到一些讓他們不斷測(cè) “實(shí)現(xiàn)細(xì)節(jié)” 的測(cè)試點(diǎn)。對(duì)此,應(yīng)該別把太多注意點(diǎn)放在要測(cè)試的業(yè)務(wù)代碼上,多想想那些會(huì)對(duì)真實(shí)用戶以及開發(fā)者產(chǎn)生影響的東西是什么,這才是你應(yīng)該要思考的 Use Case,比如:
- 生命周期方法
- 元素事件回調(diào)
- 組件內(nèi)部狀態(tài)
相反,一些跟上面兩類用戶有關(guān)的一些東西也是要做測(cè)試的,比如,它們都會(huì)改變 DOM、發(fā) HTTP 請(qǐng)求、執(zhí)行 Prop 里的回調(diào),或者產(chǎn)生一些可觀察到的副作用,把它們拿來做測(cè)試是很有幫助的:
- 用戶交互(使用 @testing-library/user-event 里的 userEvent):用戶是否在和渲染出來的組件進(jìn)行交互?
- 修改 Prop(使用 React Testing Library 里的 rerender):如果別的開發(fā)者用新的 Props 來渲染你的組件呢?
- 修改 Context(使用 React Testing Library 里的 rerender):如果別的開發(fā)者修改了 Context 導(dǎo)致你的組件重新渲染呢?
- 修改訂閱:如果組件訂閱的事件中心做了修改呢?(比如:firebase、redux store、router、media query)
該從何測(cè)起?
現(xiàn)在我們都清楚應(yīng)該要對(duì)單測(cè)組件或者頁(yè)面組件測(cè)什么了,那你該從何測(cè)起呢?這確實(shí)是個(gè)讓人頭大的問題,尤其是你要對(duì)一個(gè)巨大無比的應(yīng)用進(jìn)行測(cè)試的時(shí)候。
好,現(xiàn)在這是你要做的事:從真實(shí)用戶的角度來看以及問:
如果應(yīng)用崩了,那么哪部分會(huì)讓人最不爽?
或者換個(gè)問法:
應(yīng)用崩了,最糟糕的地方在哪里?
我會(huì)建議你按這個(gè)標(biāo)準(zhǔn)來列出你應(yīng)用支持功能的優(yōu)先級(jí)。 你可以和你的團(tuán)隊(duì)以及 Leader 一起來做這件事,這將會(huì)是一次很好的嘗試。而且這次嘗試也會(huì)有很多好處:幫助所有人搞清楚測(cè)試的重要性,并說服他們:測(cè)試也是一件優(yōu)先級(jí)很高的事情。
一旦有了這份優(yōu)先級(jí)清單,我會(huì)建議你寫一個(gè)端對(duì)端的測(cè)試來覆蓋住用戶使用最多的場(chǎng)景。一般來說,這種方法都能覆蓋住這份清單前幾項(xiàng)功能。你可能需要多的時(shí)間來做這個(gè)測(cè)試,但是一切都是值得的。
雖然這個(gè) E2E 測(cè)試不會(huì)給你 100% 的 Use Case 覆蓋率(你千萬別嘗試去弄),也不會(huì)給你 100% 代碼覆蓋率(你甚至都別想著要記錄 E2E 的覆蓋率),但它會(huì)給你一個(gè)很好的開始,而且能立即增強(qiáng)你對(duì)當(dāng)前代碼的信心。
一旦有了幾個(gè) E2E 測(cè)試用例之后,你就可以給一些沒在 E2E 范圍內(nèi)的邊界情況做集成測(cè)試,然后再給用到的功能里更復(fù)雜的業(yè)務(wù)邏輯做單元測(cè)試。從現(xiàn)在開始,剩下的事情就是不斷加測(cè)試就好了。只是別老想著要 100% 的覆蓋率了,不值當(dāng)。
總結(jié)
如果有足夠的時(shí)間和經(jīng)驗(yàn),你會(huì)培養(yǎng)出一種知道要測(cè)試什么的直覺。你可能會(huì)犯錯(cuò)誤或者難受,不要放棄!穩(wěn)住,我們能贏。
好了,這篇外文就給大家?guī)У竭@里了??偟膩碚f,也是很贊同 “要多關(guān)注 Use Case 的覆蓋情況而不是代碼覆蓋情況”,畢竟如果完全按照代碼覆蓋率這個(gè)指標(biāo)來的話,有太多的作弊手段了,這完全和寫測(cè)試的初衷是相違背的。寫測(cè)試的目的應(yīng)該是增強(qiáng)我們對(duì)代碼的自信心,而不是功利地看某個(gè)指標(biāo)。
后面 Kent 說到要如何把測(cè)試引入團(tuán)隊(duì)的方法也很值得大家去嘗試:先按功能優(yōu)先級(jí)列出個(gè)清單,再寫 E2E 覆蓋住最重要的那部分,再加集成測(cè)試,再加單元測(cè)試,等一切就緒,那么剩下的就是時(shí)間堆測(cè)試用例,最后測(cè)試用例也能慢慢融入到代碼中了。