淺析前端測試的反模式
過于關(guān)注實(shí)現(xiàn)細(xì)節(jié)的測試
在為前端項(xiàng)目編寫測試用例的時(shí)候,你也許和我一樣,曾遇到過以下困擾:
(1) 明明進(jìn)行了功能正確的改動,測試卻掛了。修復(fù)測試有時(shí)候得認(rèn)真閱讀各種mock的細(xì)節(jié),或者去了解很多本沒有必要知道的代碼邏輯。最后修測試花的時(shí)間比進(jìn)行業(yè)務(wù)改動花的時(shí)間還要長(甚至長很多)。
(2) 對代碼進(jìn)行提取抽象之后,為各個(gè)組件或函數(shù)添加測試,實(shí)際上是用測試工具的API去重復(fù) 業(yè)務(wù)代碼的內(nèi)部實(shí)現(xiàn)邏輯(有時(shí)候還很麻煩!)。任何正常的重構(gòu)都會導(dǎo)致測試失敗,你本來希望測試能告訴你什么樣的修改是對的,結(jié)果現(xiàn)在測試只能告訴你代碼確實(shí)有被修改。
(3) 測試寫好,覆蓋率提高,本應(yīng)信心十足地認(rèn)為代碼變得健壯了,可是捫心自問,你知道自己寫的這個(gè)測試弱點(diǎn)在什么地方,或者說還有多少細(xì)節(jié)沒有涵蓋。你精心模擬了一個(gè)條件,去觸發(fā)邏輯流程,并且測試通過,可是在真實(shí)的瀏覽器交互中用戶也許并不能觸發(fā)這個(gè)條件。因此,同樣的道理,你在自己的代碼通過了他人寫的測試之后,也不能確定真實(shí)場景下沒有問題,只好把后續(xù)的重任交給QA。
造成上面三個(gè)問題的原因不止一個(gè),但測試過于關(guān)注實(shí)現(xiàn)細(xì)節(jié)在我看來是最主要的。
第一個(gè)問題,明明是正確的改動,可是測試不止是驗(yàn)證業(yè)務(wù)功能,還對實(shí)現(xiàn)細(xì)節(jié)提出了不該提出的要求,比如要求你的函數(shù)接受跟以前一樣的參數(shù),返回值必須是字符串而不能是數(shù)組等等。可是這個(gè)函數(shù)只是實(shí)現(xiàn)流程中一個(gè)小小的環(huán)節(jié),也許在下次重構(gòu)時(shí)就會不復(fù)存在。
第二個(gè)問題很類似,如果測試代碼去重復(fù)實(shí)現(xiàn)細(xì)節(jié),不管進(jìn)行正確還是錯(cuò)誤的重構(gòu),你都得把測試改一遍,那原先的測試又能提供什么價(jià)值呢?
第三個(gè)問題有時(shí)發(fā)生在,測試的實(shí)現(xiàn)細(xì)節(jié),不能覆蓋整個(gè)真實(shí)交互流程的時(shí)候。用戶點(diǎn)擊的是屏幕上的button按鈕,而測試的起點(diǎn)是onClick事件被觸發(fā)。后面的邏輯被驗(yàn)證成功,可問題偏偏發(fā)生在點(diǎn)擊環(huán)節(jié),真實(shí)的點(diǎn)擊也許因?yàn)榘粹o狀態(tài)而無法觸發(fā)onClick事件。
因此,才會有人提出前端的測試應(yīng)盡量去模擬真實(shí)的用戶行為,Testing-Library就在其官網(wǎng)的“指導(dǎo)原則”章節(jié),鼓勵(lì)使用者盡量仿照應(yīng)用真實(shí)的使用方式去編寫測試,并明確提出,你的測試越接近用戶的真實(shí)使用方式,它就能給你越多的信心。換句話說,你的測試應(yīng)該盡量少用函數(shù)去手動觸發(fā),而要盡量多地利用測試框架給你的API,去模擬Input框的輸入,按鈕的點(diǎn)擊,表單的提交等等。
如此一來,有的函數(shù),你也無需寫測試證明它的返回值如你所愿,需要寫的,是頁面顯示了期待的文字,發(fā)生了預(yù)期的變化,進(jìn)行了對應(yīng)的跳轉(zhuǎn)。你會發(fā)現(xiàn),這時(shí)的測試就像寫在卡里的AC一樣。只要測試是通過的,你就有理由相信主體功能沒有破壞,而不只是函數(shù)工作正常。
沒有獨(dú)立業(yè)務(wù)含義的測試單元
看到上面的方案,你可能會立馬會想到一些問題。
首先就是測試流程可能會很長,從用戶填完表單,點(diǎn)擊提交,到期待的變化出現(xiàn),當(dāng)中可能經(jīng)歷了好幾個(gè)函數(shù)的執(zhí)行,連帶著一系列的副作用。模擬這一系列行為,似乎是集成測試與E2E測試該干的事情。如果項(xiàng)目中大部分邏輯都是由這種測試去覆蓋,看起來與測試金字塔所說的由單元測試作為地基是矛盾的。
我認(rèn)為,當(dāng)真實(shí)遇到的問題碰到了某種教條規(guī)范時(shí),后者該適當(dāng)?shù)刈尣健?/p>
鼓勵(lì)多寫單元測試的原因在于它們成本低,有針對性。可是在前端項(xiàng)目里面,很多形式上的單元并沒有獨(dú)立的業(yè)務(wù)含義。
拿React項(xiàng)目舉例,好多函數(shù)只是因?yàn)樗鼈冊谛问缴峡梢员怀槿〕鰜?,就被拎到一個(gè)單獨(dú)的文件里,從而降低主函數(shù)的復(fù)雜度。如果給它寫單元測試,你就不得不手動觸發(fā)它的參數(shù)變化,或者檢測它的參數(shù)函數(shù)是否有被調(diào)用。
我們寫的React hook尤其如此。很多時(shí)候抽取自定義的hook是出于邏輯上的原因,把相關(guān)的邏輯和數(shù)據(jù)聚合到一起,減輕UI組件的負(fù)擔(dān),但這些hook往往沒有一個(gè)可以輕易解釋清楚的業(yè)務(wù)含義,而且它們也不會被其它地方使用。
所以這類 “單元”只是長得像單元而已,它們其實(shí)只是一個(gè)實(shí)現(xiàn)環(huán)節(jié)。這里完整的UI操作流程,才更像一個(gè)有價(jià)值的單元,盡管它們在形式上可能超越了單個(gè)函數(shù)的范疇。
但我不想矯枉過正,確實(shí)有不少情況下,一個(gè)util函數(shù),一個(gè)hook,一個(gè)很小的公共組件,都是有獨(dú)立存在的價(jià)值的,因此,它們也應(yīng)當(dāng)被視為真正的單元,確實(shí)“有資格”擁有自己的專屬測試。
testing-library下面有一個(gè)單獨(dú)的庫,叫react-hooks-testing-library,讓你無需通過UI行為層面,而是直接以hook的方式去測試它們。它的GitHub頁面上,明確提出了使用以及不使用它的場景:當(dāng)你的hook不與組件強(qiáng)相關(guān),擁有獨(dú)立含義時(shí)可以使用;當(dāng)你的hook只被一個(gè)組件使用,且和它的定義強(qiáng)相關(guān)時(shí),則不建議使用。
【插入一段:盡管存在react-hooks-testing-library這樣的工具,但像SWR這樣優(yōu)秀的三方庫,在用testing-library為自己的hook API做測試的時(shí)候,依然選擇在UI層面進(jìn)行。方法是,把自己的hook置于一個(gè)臨時(shí)的div標(biāo)簽里進(jìn)行render,把數(shù)據(jù)的變化映射成html文字的變化,最后對文字內(nèi)容做斷言。其實(shí)對于獨(dú)立性強(qiáng)的函數(shù),個(gè)人覺得放置在UI里面做測試倒沒有太大區(qū)別,但SWR的例子體現(xiàn)了對“仿照真實(shí)使用場景去測試”這一原則的尊重?!?/p>
將上面的規(guī)律套用到Angular項(xiàng)目中,也是類似的。對于獨(dú)立性和通用性不強(qiáng)的pipe,directive,reducer,effect,service,都可以認(rèn)為它們是實(shí)現(xiàn)流程的一部分,從UI行為層面寫好測試即可。
總之,在構(gòu)思前端測試的時(shí)候,與其死守“單元測試”的字面含義,不如結(jié)合實(shí)際場景,重新思考什么才是真正有價(jià)值的“單元”,因地制宜地去寫。換種角度表述,與其在意我們寫的測試是不是“單元測試”,不如追求更核心的東西——我們的測試有沒有以合適的方式去校驗(yàn)邏輯。
另外,當(dāng)我們的“單元”過大,一些邏輯可能就會覆蓋不上。像sonar這類工具,不僅會檢查你的行數(shù)覆蓋率,還會檢查你的各項(xiàng)條件語句是否有被測試執(zhí)行。當(dāng)一套測試的行為流程囊括了多個(gè)函數(shù),而且每個(gè)函數(shù)都有好幾個(gè)if…else語句時(shí),想要在UI操作與mock數(shù)據(jù)上把所有情況都覆蓋到,成本就會變得非常高昂。
對于此,我們得承認(rèn),無論用什么方式組織測試,覆蓋所有的條件分支都是不太現(xiàn)實(shí)的,而且價(jià)值也不大。對于“滿足條件A就執(zhí)行XXX”之類的語句,條件為非A時(shí)沒有業(yè)務(wù)上的規(guī)定,如果為了刻意覆蓋函數(shù)的所有條件,就強(qiáng)行測它在非A的情況下返回一個(gè)undefined,則沒有太多價(jià)值。對這類情況,用UI行為測試主要條件即可,如果你實(shí)在覺得有重要的邏輯沒有被覆蓋,不妨回過頭來想想,是不是漏掉了某種輸入條件,例如特定的用戶鍵入或者特殊的API mock返回值。但是,當(dāng)有過多的條件分支很難用業(yè)務(wù)場景去表述和模擬的時(shí)候,我們可能需要重新思考代碼的實(shí)現(xiàn)邏輯是否合理了。
當(dāng)然,即使按上面這樣做,有時(shí)候還是會發(fā)現(xiàn)要覆蓋的條件組合太多,從行為流程上寫測試太復(fù)雜,這時(shí)就不得不做一定的妥協(xié),為那些沒有獨(dú)立性的部分去單獨(dú)寫測試。如果這類測試不太好寫,可以參照剛才提到的SWR官方測試用到的技巧,把要測的函數(shù)或者是對象放置在一個(gè)臨時(shí)的UI組件下,以最小的成本做UI行為測試。
最后
總結(jié)一下上面談到的幾個(gè)原則:
(1) 從真實(shí)用戶的行為流程去測試,往往比測函數(shù)本身,能給你帶來更多的信心。
(2) 對于沒有獨(dú)立性和通用性的函數(shù)或?qū)ο?,把它們視作?shí)現(xiàn)的一部分,一般沒有必要為它們?nèi)憜为?dú)的測試。不要拘泥于對“單元測試”的字面理解,不要被形式上的規(guī)律所束縛。
(3) 不要把測試覆蓋率視為太過重要的指標(biāo),它的目的還是幫助提升代碼的穩(wěn)定。有的代碼沒有覆蓋也沒關(guān)系,有的代碼值得你覆蓋好多遍。畢竟,我們不是為了寫測試而寫測試。
【本文是51CTO專欄作者“ThoughtWorks”的原創(chuàng)稿件,微信公眾號:思特沃克,轉(zhuǎn)載請聯(lián)系原作者】