從爬⾏到奔跑 - 我們為什麼需要單元測試?

碼農談IT發表於2023-09-22

從爬⾏到奔跑 - 我們為什麼需要單元測試?

來源:阿里雲開發者

阿里妹導讀

本文從測試體系的歷史入手,講述了從手動測試 -> 靠別人自動化測試 -> 靠自己自動化測試的歷史演化程式,也嘗試著從這個視角解釋為什麼大家過去不重視單元測試。之後我們分別講述了什麼是單元測試,業界的金字塔測試最佳實踐,並且深入講解了單元測試的種種好處。最後我們列舉了常見的反面模式和誤區,幫助大家快速識別規避常見的錯誤。

前⾔

剎⻋是降低了⻋速還是提升了⻋速?我們通常認為寫單測費⼒耗時、耽誤研發進度,彷彿在給項⽬“踩剎⻋”。⼤家不妨帶著這個問題往下看,詳細聊聊為什麼單元測試可以讓軟體開發跑得更快。

什麼是單元測試

⼤家對於單測應該並不陌⽣,擷取⼀段維基百科的定義幫⼤家喚醒⼀下記憶:
在計算機程式設計中,單元測試(Unit Testing)⼜稱為模組測試,是針對程式模組(軟體設計的最⼩單位)來進⾏正確性檢驗的測試⼯作。
單元測試的理念其實⼀直是程式設計的⼀部分。我們第⼀次編寫計算機程式時,肯定會輸⼊⼀些樣本數據,檢視其是否按照你的期望執⾏。如果結果不符合預期,你肯定在程式碼⾥穿插過⼤量的System.out.println,確保每個原⼦節點都符合預期。這個過程其實就是把複雜問題拆解成原⼦化的問題、逐⼀攻破的過程。單元測試的⽬的也⼀樣,是保障軟體程式中每個最⼩單位的正確性,從⽽保障由最⼩單位構建起來的複雜系統的正確性。
深⼊展開單元測試的必要性之前,我們先去考考古,看⼀下測試體系是如何演進的。

測試體系的演進

從爬⾏到奔跑 - 我們為什麼需要單元測試?

過去的很⻓⼀段時間⾥,軟體測試⼤量依賴⼈⼯檢測。軟體測試甚⾄是⼀個獨⽴的⼯種(QA、Tester),QA/tester的⽇常任務就是進⾏⼤量的⼿⼯測試、繁瑣易錯。
⾃2000年代初以來,軟體⾏業的測試⽅法已經發⽣了巨⼤的變化。為了應對現代軟體系統的規模和複雜性,業界演變出了開發⼈員驅動的⾃動化測試實踐。我們終於可以擺脫⼿動測試的繁瑣,⽤軟體來測試軟體。但過去的實踐仍然留下了深遠的影響,軟體測試還是⼀個獨⽴的⼯種,過去的QA演進成了SDET(Software Developer Engineer in Test),我們雖然進化到會使⽤⼯具了,但我們還只是會⽤⼯具的猴⼦。為什麼這麼講?因為這種研發/測試分離的模式本身就留下了很多問題。當研發和測試是兩個崗位時,交付的邊界是軟體整體的功能性(functional requirements)和可⽤性。研發只要保證軟體整體上功能完備、可⽤就⾏,測試也會聚焦在整合測試和端到端測試上。但軟體是由⽆數個最⼩單位構成的,在這種體系下⼈們會忽視最⼩單位的質量、是否可讀可測可演進,最終難免“⾦⽟其外,敗絮其中”。
基於種種弊端,⾕歌、微軟這些對研發質量⾮常重視的公司都在從SDET的2.0時代過渡到 all-in-one 的3.0時代:微軟在2015年去掉SDET⼯種,在陸奇帶領的Bing中率先提出“combined engineering” 的概念;⾕歌也將SETI替換成EngProd(Engineering Productivity),專⻔負責測試平臺和⼯具的搭建,不負責具體的業務邏輯測試。

為什麼需要單元測試

在如今的互聯⽹時代,軟體迭代的速度越來越快,研發的職責也越來越多。DevOps的理念是"you build it, you run it",研發/測試合⼆為⼀的趨勢也可以理解為對"you build it, you test it"的呼籲。當研發要對⾃⼰寫的程式碼質量和測試負責的時候,好的測試實踐就必不可少了。

測試⾦字塔

就像蓋樓需要從打地基、豎鋼筋、灌⽔泥層層往上構建⼀樣,測試也有類似的測試⾦字塔架構。下圖出⾃《Software Engineering at Google》的測試章節,總結了Google在測試⽅⾯的最佳實踐。我們可以看到測試⾦字塔由三層構成,最底層就是單元測試、佔⽐80%,是軟體系統的地基。再往上是整合測試和端到端測試,分別佔15%和5%。因為從下往上佔⽐逐層縮減,因此被稱為測試⾦字塔(跟蓋⾼樓⼀樣)。⾕歌推薦的這個⽐例是多年實踐出來的結果,意在提升研發的效率(productivity)並提升對產品的信⼼(product confidence)。
測試⾦字塔的核⼼理念之⼀就是“Unit Test First“,每個軟體項⽬⾥的第⼀⾏測試應該是單測(TDD甚⾄認為第⼀⾏程式碼就應該是單測),⽽且⼀個項⽬⾥佔⽐最⾼的測試也應該是單測。

從爬⾏到奔跑 - 我們為什麼需要單元測試?圖⽚來源:Software Engineering at Google

優秀的軟體離不開單元測試

為什麼業界都把單元測試放在這麼重要的位置?“抓⼤放⼩”,只寫端到端測試不⾹嗎?這⾥我們來展開講講單測的好處。
提升debug效率
單元測試是軟體⼯程極佳的地基,因為它們快速、穩定,並且極⼤地縮⼩了問題範圍,提升故障診斷的效率。

  • 測試更快:單測沒有其他外部依賴,跑的快,可以提供更快的反饋環,更快的發現並修復問題。

  • 測試更穩定:同樣因為0依賴,單測相⽐於其他型別的測試更穩定,不會受外部其他模組的不相容變更影響。因此單測也是最能帶給開發者信⼼的測試型別。

  • 問題更容易定位:單測以最⼩軟體單位為邊界,出了問題可以縮⼩定位範圍。相⽐之下,越是⾦字塔上層的測試型別,定位問題的困難度越⼤。複雜的端到端測試涉及眾多的模組,需要⼀⼀排查定位問題。

提升程式碼質量

程式碼是寫給⼈看的,好的程式碼應該是易讀、易改、易維護的。寫單測的過程其實就是吃⾃⼰程式碼狗糧(dogfood)的過程,從⽤戶/研發視⻆去使⽤⾃⼰的程式碼,幫助我們提升程式碼質量。

  • 好的程式碼是易測的:業界很早就提出了圈複雜度(Cyclomatic complexity)的概念,⽤來衡量⼀個模組判定結構的複雜程度,其數量上表現為獨⽴路徑的條數,也可理解為覆蓋所有的可能情況最少使⽤的測試⽤例個數。圈複雜度⼤說明程式程式碼的判斷邏輯複雜,可能質量低,且難於測試和維護。因此好的程式碼⼀定是圈複雜度低的,也是易於測試的。

  • 易於迭代演進:沒有什麼軟體是⼀成不變的,好的軟體系統應該是易於演進的。單測覆蓋⾼的項⽬模組更原⼦化,邊界更清晰,修改起來更容易。單測覆蓋更全的項⽬重構的⻛險也相對更⼩,相反⼀個沒有單測覆蓋的複雜項⽬是沒⼈敢碰的。

  • 更優質的設計:前⾯也提到,好的單測能夠提升程式碼的質量。如果⼀個研發需要給⾃⼰的程式碼寫單測,他就會注重程式碼的模組化分割,減少過⻓、圈複雜度過⾼的method。下⾯的例⼦就是⼀段沒有單測的程式碼的認知複雜度值(可以理解是圈複雜度的⼀個改良版,從程式碼是否容易理解的⻆度衡量),超標了⾜⾜三倍。現在回過頭來想補單測,腦袋都⼤。

從爬⾏到奔跑 - 我們為什麼需要單元測試?

提升總體研發效率
磨⼑不誤砍柴⼯,⾼質量、完善的單測可以提升研發質量和效率,加快項⽬總體交付速度。這句話乍⼀看是反常識的,寫單測往往⽐寫實現邏輯要更耗時,怎麼還能提⾼效率?這也是⼤家不寫單測最常⻅的理由:“項⽬趕進度,來不及寫單測”。如果我們的項⽬⽣命週期是以⽉計算的,寫個原型很快就下線了,那寫單測的確ROI不⾼。但阿⾥有很多to B的業務,提供給⽤戶的能⼒都是以年計算⽣命週期的,⾼質量程式碼的ROI隨著時間推移會越來越⾼,具體體現在以下⽅⾯:

  • 減少debug時間:上⾯提到種種提升debug效率的原因,這⾥不再重複。⼀⽅⾯更⾼的單測覆蓋可以節省debug所花費的時間,另⼀⽅⾯有充⾜測試覆蓋的項⽬本身bug數量就會更少。舉個現實中的例⼦:某團隊由於歷史上⽋的種種債務,基本全靠端到端測試,毫⽆單元測試覆蓋。造成的後果也⾮常嚴重,團隊oncall的同學 > 50%的時間都是在修復各種奇怪的bug,沒法投⼊寶貴的精⼒到架構升級等⻓期更重要的項⽬上。

  • 增加程式碼變更的信⼼:前⾯提到沒有測試覆蓋的程式碼沒⼈敢碰,有充⾜單測覆蓋的程式碼可以顯著提升改造程式碼的信⼼和意願。再給⼤家舉個例⼦:我加⼊阿⾥之前在Google總部⼯作過將近⼗年。如果你在Google⼯作過就會發現,你的程式碼經常會收到毫不相關團隊成員發起的code change。⼤多數情況下這些都是同學們⾃發的去做⼤⾯積重構(mass refactor),⽐如看你的Java程式碼沒有⽤Builder模式,就會幫你做個重構(Google⾥有⼤量⾃動化⼯具簡化這些重構⼯作)。我們拋開主觀意願不談,如果是沒有測試覆蓋的程式碼、還是毫不相關組的,你敢這麼重構嗎?我們都希望能有像⾕歌那樣整潔的程式碼,但沒⼈敢碰的程式碼怎麼變得更好?

  • 提升程式碼⾃解釋性:⽂檔能夠提升程式碼的⾃解釋性,讓研發效率更⾼。好的單測其實也可以被看作程式碼的⽂檔,透過讀測試就能快速理解程式碼的作⽤(參⻅TDD)。單測作為⽂檔同時還完美的解決了⽂檔保鮮的難題,給開發者提供了⼀套⾼質量、隨著程式碼不斷更新的⽂檔。

  • 更⾼效的code review:不是所有的問題和設計上的缺陷都能透過靜態檢查發現,這也是為什麼需要⼈⼯code review作為程式碼質量的最後⼀道防線。在Google,程式碼評審是程式碼合併最重要的⼀個環節,因此評審的效率直接影響總體的研發效率。好的單測覆蓋能夠減輕評審⼈的負擔,讓他們把精⼒投⼊到更重要的部分(⽐如程式碼設計)。

  • 更頻繁的發版:敏捷開發倡導的持續整合、持續部署的前提就是全⾯、⾼質量的⾃動化測試。敏捷開發對於研發的提效就不多展開了。但光是能夠更快速的發版本身就已經⾮常有價值了。

反⾯模式和常⻅誤區

上⾯提到了寫單元測試的種種好處和業界的最佳實踐。我們也列舉⼀下常⻅的反⾯模式和誤區,幫助⼤家更好的規避類似錯誤。

測試的反⾯模式(anti-pattern)

反⾯模式⼀:冰淇淋筒模式
只關注⽤戶視⻆的端到端測試、⼤量依賴QA測試都會產⽣如下圖所示的反⾯模式。很不幸,這也是在過去的測試體系影響下最常⻅的模式。冰淇淋筒模式下,測試套件通常運⾏緩慢、不可靠、難以使⽤。缺失底層的單測也會讓項⽬變得⾮常難維護,很難做⼤的改動。

從爬⾏到奔跑 - 我們為什麼需要單元測試?

圖⽚來源:Software Engineering at Google

反⾯模式⼆:沙漏模式
沙漏模式下,項⽬中有⼤量的單元測試和端到端測試,但缺乏整合測試。雖然它不像冰淇淋筒那麼糟糕,但仍會導致許多端到端測試失敗,這些失敗本可以透過⼀套中等範圍的測試更快更容易地捕捉到。當模組間緊密耦合,使得依賴項很難單獨例項化出來的時候,就會出現沙漏模式。

從爬⾏到奔跑 - 我們為什麼需要單元測試?

圖⽚來源:Software Engineering at Google

測試的常⻅誤區

常⻅誤區⼀:⽤戶第⼀,測試覆蓋⽤戶的需求⾜夠了
這個誤區下會認為,端到端測試是站在⽤戶視⻆做測試,把⽤戶要的功能點都覆蓋到就⾜夠了。這種誤區導致的結果就是冰淇淋筒反⾯模式。雖然軟體交付的最終功能是給客戶使⽤的,但構成軟體的程式碼本身是給⼈(研發)讀的、需要⼈去維護。外部⽤戶是⼈,內部⽤戶也是⼈。
常⻅誤區⼆:All-in端到端測試,節省了80%的測試程式碼量,贏麻了
從短期來看,不寫單測可以節省80%的測試程式碼量和⾄少50%的研發時間。但只要項⽬複雜起來,時間線拉⻓,過去⽋的歷史債務(technical debt)早晚要加倍奉還。等到真正需要還債的時候再去補,可能為時已晚。
常⻅誤區三:寫單測的⼈都弱爆了,我⻓這麼⼤還沒寫出過bug
這篇⽂章可能不適合你。不過軟體開發是個團隊項⽬,你寫的程式碼最終也會落到別⼈⼿⾥去升級維護,沒有測試覆蓋的程式碼是沒⼈敢碰的。

總結

結尾處再快速總結⼀下。本⽂從測試體系的歷史⼊⼿,講述了從⼿動測試 -> 靠別⼈⾃動化測試 -> 靠⾃⼰⾃動化測試的歷史演化程式,也嘗試著從這個視⻆解釋為什麼⼤家過去不重視單元測試。之後我們分別講述了什麼是單元測試,業界的⾦字塔測試佳實踐,並且深⼊講解了單元測試的種種好處。後我們列舉了常⻅的反⾯模式和誤區,幫助⼤家快速識別規避常⻅的錯誤。
如果把測試體系的演進類⽐為⼈類的進化,那麼我認為⽆單測覆蓋和有充分單測覆蓋的軟體就好⽐爬⾏的古猿和直⽴⾏⾛的現代⼈類。由衷希望⼤家能夠重視單元測試、寫好單元測試,讓我們的軟體儘快從爬⾏進化成奔跑,迸發出源源不斷的⽣命⼒、創造出更多價值!
參考資料:
[1] https://testing.googleblog.com/2015/04/just-say-no-to-more-end-to-end-tests.html
[2]https://arstechnica.com/information-technology/2014/08/how-microsoft-dragged-itsdevelopment-practices-into-the-21st-century/4/
[3]https://medium.com/nerd-for-tech/the-paradigm-shifts-going-from-1-1-to-10-1-to-100-1-dev-testratio-44183a734d77
[4]https://blog.testproject.io/2018/11/06/the-software-engineer-in-test/

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70024924/viewspace-2985197/,如需轉載,請註明出處,否則將追究法律責任。

相關文章