作者 | 張霄翀
前言
我曾經(jīng)在好幾個項目里都近乎完整參與過補齊前端測試的工作,也收集到不同項目的同事很多關(guān)于前端測試的困惑和痛點,這其中大部分都很相似,我也感同身受,在這篇文章里,我會針對大家和自己常遇到的痛點分享一些自己的經(jīng)驗,如果你也有如下相似的困擾,那希望這篇文章能對你有些幫助~
常見問題(排名不分先后):
- 前端測試感覺寫起來很復(fù)雜,會花很多時間,甚至經(jīng)常是業(yè)務(wù)代碼時間的好幾倍
- 前端測試怎么TDD?
- 測試一些第三方UI控件時,特別難模擬與之的交互
- 有些東西不知道怎么mock,比如時間,瀏覽器全局變量(window.location,local storage)等
- 測試?yán)餃?zhǔn)備數(shù)據(jù)的代碼特別長,真正的測試代碼很靠后,要翻很久,不容易定位
- 跑測試時會冒出很多Error或Warn Log,好像不影響測試通過,修起來也很花時間,還用修么?
?
在分享問題的相關(guān)經(jīng)驗之前,我們先來梳理一下前端測試體系~
前端測試體系
前端測試的重要性
這其實跟所有測試的重要性是一樣的,大家有這么多的痛點也是因為知道覆蓋全面的測試可以對代碼質(zhì)量更有保證,讓我們更有信心地去重構(gòu)代碼,也能幫助我們更方便地了解現(xiàn)有的功能細(xì)節(jié),甚至是一些極端的邊界情況。而且在大家合作開發(fā)項目代碼的過程中,測試可以幫助我們更早地發(fā)現(xiàn)錯誤,減少時間成本,提高交付效率。
前端測試方法論(TDD vs. BDD)
這兩個常見的測試方法論在這里簡單介紹一下,就不大篇幅展開了。TDD - (Test-Driven Development 測試驅(qū)動開發(fā))簡單地說就是先根據(jù)需求寫測試用例,然后實現(xiàn)代碼,通過后再接著寫下一個測試和實現(xiàn),循環(huán)直到全部功能和重構(gòu)完成。基本思路就是通過測試來推動整個開發(fā)的進行。BDD - (Behavior Driven Development 行為驅(qū)動開發(fā)) 其實可以看做是TDD的一個分支。簡單地說就是先從外部定義業(yè)務(wù)行為,也就是測試用例,然后由外入內(nèi)的實現(xiàn)這些行為,最后得到的測試用例也是相應(yīng)業(yè)務(wù)行為的驗收標(biāo)準(zhǔn)。
前端測試的分層
在這里借一下前端大牛Kent C. Dodds的獎杯分層法來引出常見的分類:
(圖片出處:https://kentcdodds.com/blog/static-vs-unit-vs-integration-vs-e2e-tests)
端到端測試 End to End Test
端到端測試一般會運行在完整的應(yīng)用系統(tǒng)上(包括前端和后端),包含用戶完整的使用場景,比如打開瀏覽器,從注冊或登錄開始,在頁面內(nèi)導(dǎo)航,完成系統(tǒng)提供的功能,最后登出。
有時,我們也會在這里引入可視化用戶界面測試,即一種通過像素級比較屏幕截屏來驗證頁面顯示是否正確的測試。目的是確保界面在不同設(shè)備、瀏覽器、分辨率和操作系統(tǒng)下與預(yù)期的樣式一致。可以設(shè)置一定的偏差容忍值。這一層的測試成本較高,所以通常重心會放在確保主流程的功能正常上。常用工具:Cypress、Playwright、Puppeteer、TestCafe、Nightwatch (下載量對比)
集成測試主要是測試當(dāng)單元模塊組合到一起之后是否功能正常。在不同的測試上下文下可能有不同的定義,在前端測試這里通常指測試集成多個單元組件到一起的組件。
單元測試 Unit Test
單元測試就是對沒有依賴或依賴都被mock掉了的測試單元的測試。在前端代碼里,它可能是:
- 沒有依賴或依賴都被mock掉了的單元組件
- 功能代碼如Utils/Helpers等公共方法集合的測試
- 輔助組件功能如React Hook / Selector等公共方法的測試
靜態(tài)代碼測試 Static Test
主要是指利用一些代碼規(guī)范工具(Lint Tool)來及時捕獲代碼中潛在的語句錯誤,統(tǒng)一代碼格式等。這里就不展開了。常見工具和實踐有:
- Eslint + Prettier 代碼規(guī)范和樣式統(tǒng)一
- husky + lint-staged (gitHooks工具)可以自動在commit和push之前進行代碼掃描,阻止不規(guī)范代碼進入代碼庫,也可以設(shè)置在push之前跑一遍前端測試
前端測試策略
還是這張圖,我標(biāo)記了一下:
- 越往上成本越高
- 越往上得到反饋的速度越慢
- 但越往上,越貼近最終用戶的行為,越能發(fā)現(xiàn)真實的問題,能給到的信心就更多
在獎杯的形狀上每一層占的面積代表了應(yīng)該投入的重心比例。
這里集成測試的比重比單元測試大是因為集成測試可以在成本很高的e2e測試和離最終用戶行為較遠(yuǎn)的單元測試之間取的一個平衡,它可以寫的很接近最終用戶的行為,成本又相對的沒那么高,屬于性價比很高的一部分。
所以集成測試有一些原則:
- 可以根據(jù)每個頁面的復(fù)雜程度決定是只有一個全頁面的集成測試還是可以劃分成幾大塊分別有集成測試,但一旦作為集成測試,就要盡可能的少mock依賴,盡量的渲染全子組件
- 盡量測試用戶的所見和交互,而不是背后的實現(xiàn),否則就會遠(yuǎn)離最終用戶行為,降低信心值,而且隨著代碼的重構(gòu),測試也需要頻繁的修改。比如Enzyme可以把component里的方法、props、state等都提供出來單獨測試,但這里的測試并不貼近真實用戶的交互,很容易就會因為重構(gòu)而破壞測試,更好的方法是真的去測試當(dāng)props和state變化后頁面的變動,或交互的變化
- 準(zhǔn)備的測試數(shù)據(jù)盡量豐富且貼近真實數(shù)據(jù)(用戶敏感信息要替換掉),越貼近真實的數(shù)據(jù)越能覆蓋到更多真正的問題
- 對于核心的業(yè)務(wù)行為,要重點測試
對于單元測試來說:
- UI組件類的測試:因為有了集成測試的覆蓋,可以簡單的測試一下不同props的渲染,如果有一些集成測試覆蓋不到的特殊數(shù)據(jù)引發(fā)的交互行為,可以測試一下
- 非UI組件類的測試:通常會覆蓋一些復(fù)雜的業(yè)務(wù)邏輯,需要全面的測試一下不同的分支條件
前端測試工具的分類
測試啟動工具 (Test Launchers)
測試啟動工具負(fù)責(zé)將測試運行在Node.js或瀏覽器環(huán)境。形式可能是CLI或UI,并結(jié)合一定的配置。常見工具有:Jest / Karma / Jasmine / Cypress / TestCafe 等。
測試結(jié)構(gòu)工具 (Structure Providers)
測試結(jié)構(gòu)工具提供一些方法和結(jié)構(gòu)將測試組織的更好,擁有更好的可讀性和可擴展性。如今,測試結(jié)構(gòu)通常以BDD形式來組織。測試結(jié)構(gòu)如下方Jest例子:
常見工具有:Jest / Mocha / Cucumber / Jasmine / Cypress / TestCafe 等。
斷言庫 (Assertion Functions)
斷言庫會提供一系列的方法來幫助驗證測試的結(jié)果是否符合預(yù)期。如下方的例子:
常見工具有:Jest / Chai / Assert / TestCafe 等。
Mock工具
有的時候我們在測試的時候需要隔離一些代碼,模擬一些返回值,或監(jiān)控一些行為的調(diào)用次數(shù)和參數(shù),比如網(wǎng)絡(luò)請求的返回值,一些瀏覽器提供的功能,時間計時等,Mock工具會幫助我們更容易的去完成這些功能。
常見工具有:Sinon / Jest (spyOn, mock, useFakeTimers…) 等。
快照測試工具 (Snapshot Comparison)
快照測試對于UI組件的渲染測試十分有效。原理是第一次運行時生成一張快照文件,需要開發(fā)人員確認(rèn)快照的正確性,之后每一次運行測試都會生成一張快照并與之前的快照做比較,如果不匹配,則測試失敗。這時如果新的快照確實是更新代碼后的正確內(nèi)容,則可以更新之前保存的快照。(這里的快照通常都是框架渲染器生成的序列化后的字符串,而不是真實的圖片,這樣的測試效率比較高)。
這里可以參考Jest官方的用例。
常見工具有:Jest / Ava / Cypress
測試覆蓋率工具(Test Coverage)
測試覆蓋率工具可以產(chǎn)出測試覆蓋率報告,通常會包含行、分支、函數(shù)、語句等各個維度的代碼覆蓋率,還可以生成可視化的html報告來可視化代碼覆蓋率。如以下的Jest內(nèi)置的代碼覆蓋率報告:
(圖片出處:??https://jestjs.io/)??
常見工具有:Jest內(nèi)置 / Istanbul。
E2E 測試工具(End to End Test)
上面在測試分層里介紹過的。
可視化用戶界面測試(Visual Regression)
也在上面的測試分層里介紹過。通常會和e2e測試工具組合在一起使用,一般主流的e2e測試工具也會有對應(yīng)的庫去進行可視化用戶界面測試。
前端框架專屬測試庫
不同的前端框架還會有一些自帶的或推薦的測試庫,比如:
- React: React官方的Test Utils / Testing Library - React(推薦) / Enzyme (基于上面的測試策略,更推薦React Testing Library,Enzyme暴露了太多內(nèi)部元素用來測試,雖然一時方便,但遠(yuǎn)離了用戶行為,之后帶來的修改頻率也比較高,性價比低)
- Vue: Vue官方的Test Utils / Testing Library - Vue
- Angular: Angular內(nèi)置的測試框架(Jasmine) / Testing Library - Angular
前端測試框架
基于上面的分類,大家可能發(fā)現(xiàn)幾乎哪哪都有Jest,這類大而全的前端測試工具我們也可以稱為前端測試框架。
常見的有:
- Jest:大力推薦,幾乎有測試需要的所有工具,社區(qū)活躍,網(wǎng)上資源豐富,也是React官方推薦的測試框架
- Mocha:雖然也功能豐富,但沒有斷言庫、測試覆蓋率工具和Mock工具,需要和其他第三方庫配合使用
- Jasmine:比較老派的工具,功能也沒有Jest豐富,下載率逐年下降
最后附上一張stateOfJS網(wǎng)站2021年的測試庫滿意度圖表供大家參考 :
(圖片出處:https://2021.stateofjs.com/en-US/libraries/testing/)
前端測試的常見問題
終于回到最開始的問題了,分享一下我的經(jīng)驗和通常的解決辦法:
前端測試感覺寫起來很復(fù)雜,會花很多時間,甚至經(jīng)常是業(yè)務(wù)代碼時間的好幾倍,這個問題可以分成三部分來下手:
優(yōu)化測試策略
可以根據(jù)剛才的測試策略部分,結(jié)合自己項目的實際情況,調(diào)整一下在不同的測試層分配的重心,定一下自己項目每個層級的測試粒度,這樣才能在保證交付的前提下達(dá)到測試信心值收益的最大化。
提升寫測試效率
(1) 抽取公共的部分,使具體的測試文件簡潔
- 準(zhǔn)備數(shù)據(jù)的fixture庫,可以輕松的生成想要的store數(shù)據(jù)或請求返回數(shù)據(jù)
- 公共的render方法,可以支持自定義store, stub子組件, mock框架全局方法等
- 公共的第三方UI組件交互方法,可以輕松的觸發(fā)第三方控件的事件,不用再關(guān)心實現(xiàn)細(xì)節(jié)
- 公共的api mock方法,可以在測試文件里不用關(guān)心api細(xì)節(jié),輕松mock
(2) 統(tǒng)一測試規(guī)范,有優(yōu)化及時重構(gòu)所有測試,這樣大家可以放心的參考已有測試,不會有多種寫法影響可讀性
提升運行測試的效率
- 并行跑測試
- 測試?yán)锍S萌缦路椒ㄊ勾郎y的異步請求返回,通常也會給setTimeout一個等待時間,大部分的情況0就可以達(dá)到目的了,除非是邏輯真的要等待一定的時間,如果默認(rèn)值都設(shè)置的比較大,每個測試都會耽誤一些時間,加起來對測試運行性能的影響是很大的
前端測試怎么TDD
通常問這個問題背后隱藏的問題是前端很難先寫測試,再寫實現(xiàn)。確實我也有同感,如果是一些util/helper方法是可以很容易的遵循TDD的步驟的,但當(dāng)涉及頁面結(jié)構(gòu)和樣式的時候,很難在寫測試的時候就想清楚頁面到底有哪些具體的元素,用到哪些需要mock的模塊。
所以在測試UI組件時,我通常會使用BDD的方式,具體步驟是:
- 建立組件文件,渲染返回空
- 建立測試文件,先寫一個snapshot測試,測試會通過,生成一個snapshot文件
- 再根據(jù)這個頁面mockup上已知的交互寫好test case,通常這個時候不太容易寫實現(xiàn),就先把測試用例都寫好,test先skip起來,eslint可以設(shè)置成skip的test用warn來展示,這樣之后方便補全
- 隨著頁面重構(gòu),可能會給組件添加props,這時也需要給不同的props添加snapshot測試或交互測試
- 最后可以根據(jù)測試跑完的測試覆蓋率報告看看是否覆蓋全面了,防止有遺漏
當(dāng)然隨著前端代碼寫的越來越熟練,為了提升效率,有時會簡化步驟,等一個小功能的組件都重構(gòu)完了,樣式調(diào)好了,所有的子組件都抽完了,再根據(jù)每個組件的props和交互的點批量加測試,最后用測試覆蓋率來驗證是否都覆蓋到了,保證自己新寫的組件都盡可能是100%的覆蓋率。
測試一些第三方UI控件時,特別難模擬與之的交互
這個是我也很頭疼的問題,有的時候一些第三方組件因為要實現(xiàn)一些復(fù)雜的效果,會使用不一樣的方式去監(jiān)聽事件。
比如我們有一個Vue項目上用到了element-ui的select組件,這個組件可以通過:remote-method 屬性開啟異步發(fā)請求加載選項的功能,測試?yán)锵肽M異步拿到選項后并選擇某選項,就需要想辦法觸發(fā)它的@change 事件,通常一條await fireEvent.update(input, 'S'); 就搞定了,但這個怎么都不生效,仔細(xì)的查看它的實現(xiàn)才發(fā)現(xiàn)需要這么一串操作才能觸發(fā)到@change 事件。
這里我總結(jié)的經(jīng)驗就是:
- 如果發(fā)現(xiàn)常用的交互方法不能生效,需要去研究第三方組件的源碼
- 更重要的是如果大家研究出來了方法,及時的把相關(guān)代碼抽到一個公共的util文件里,這樣之后就不會有人也花費很多時間在上面了,確實經(jīng)常遇到大家重復(fù)卡在相同的第三方組件交互問題上而不知道已經(jīng)有代碼解決了的場景
有些東西不知道怎么mock,比如時間,瀏覽器全局變量(window.location,local storage)等
這個可以結(jié)合使用的測試工具去搜索,一般都會有很多現(xiàn)成的解決方案,在這里舉兩個例子:
Mock navigator.userAgent::
Mock window.open:
測試?yán)餃?zhǔn)備數(shù)據(jù),mock依賴的代碼特別長,真正的測試代碼很靠后,要翻很久,不容易定位
上面有介紹,可以將公共的部分抽取出去,又能減少代碼重復(fù),又能提升寫測試的效率,比如準(zhǔn)備數(shù)據(jù)的部分可以抽成公共的fixture文件,提供方法生成默認(rèn)的數(shù)據(jù),也可以通過參數(shù)去覆蓋修改部分?jǐn)?shù)據(jù),達(dá)到定制化的目的:
跑測試時會冒出很多Error或Warn Log,好像不影響測試通過,修起來也很花時間,還用修么?
測試?yán)锏膱箦e通常都很有價值,需要重視。這里面的錯誤有可能是:
- 前端框架相關(guān)的,比如被測的組件有寫的或調(diào)用的不合理的情況,這種有的時候不僅是測試調(diào)用組件方式的問題,有可能業(yè)務(wù)代碼也寫的有問題;或者是測試語句寫的不合理,如React的 not wrapped in act(...)
- 測試運行相關(guān)的,比如有些請求沒有mock,測試?yán)镆恢钡炔坏椒祷刂刀鴗imeout了,但又不是主測的業(yè)務(wù),所以測試還是會通過,之前有遇到很多次測試并行跑時會互相影響,隨機掛,如果log里有類似這種timeout的內(nèi)容,很有可能就是原因,mock好了所有的請求后問題就解決了
雖然有的時候也會有一些由于第三方庫的原因引起的無法修復(fù)又沒有影響的log,可以忽略,但測試?yán)锎蟛糠志鍸og其實都是可以修復(fù)的,甚至在修復(fù)后可能得到意想不到的受益,比如發(fā)現(xiàn)真正業(yè)務(wù)代碼的問題,測試不再隨機掛了,測試運行性能提升了等等。
總結(jié)
對于前端測試,我覺得重心不是機械的去追求測試覆蓋率,而是盡可能的在成本和信心值中間找到一個平衡,應(yīng)用一些好的實踐去降低寫測試的成本,提升寫測試帶來的回報,讓大家對于項目質(zhì)量越來越有信心。
原文鏈接:??前端測試體系和最佳實踐 (qq.com)??