實際軟體工程中是否真的需要100%代碼覆蓋率(code coverage)?


以下是我的個人經歷,可以作為一個案例參考。最後嘗試總結。

我近年展開新軟體項目時,都盡量以測試驅動的形式開發,常常有不少的單元測試。然而,之前嘗試用gcov/lcov的結果有點問題,也沒有加入連續整合(Continous Integration, CI)中,並不太關注覆蓋率。或者更坦白地說,寫程序二十多年來,也沒怎麼做覆蓋率的分析。

我問這個問題之時,正在為RapidJSON的正式版做準備。剛剛上周末看到別的GitHub項目 nlohmann/json,它用上了一個叫Coveralls 的免費服務,並聲稱該項目是 100% coverage。

好奇心驅使下,我也把這服務整合至RapidJSON中,經調整一些設定後,miloyip/rapidjson 跑出91% coverage。

「這也算是不錯了吧?」我最初是這麼想的。RapidJSON 本身有 5400 多行代碼(LOC),而單元測試也另有 3500 多行。「這個測試代碼量的比例已經不錯了吧?」然而,當我仔細分析沒被覆蓋的代碼,並嘗試把它們都覆蓋,就發現一些情況:

  1. 找到了1個bug(Fixed a bug in trimming long number sequence),這是最嚴重的問題。原因是開發期間改變了一些參數,但忘了在一個條件下做出相應的改動。

  2. 找到了1個不應存在的公開API(Remove an invalid Document::ParseInsitu() API),這是在重構的時候加上的,當時忘了insitu parsing只允許來源編碼和目的編碼必須一致。
  3. 找到了一些未被執行、可以消去的代碼(擬似死代碼),主要出現於與浮點數轉換相關的複雜演算法里。這是由於那些代碼都是參考別人的實現,逐漸變成現在的模樣。例如原來strtod()的實現里,只有FastPath和BigNumber演算法,後來在兩者中間加入了DiyFP的演算法,導致BigNumber中的一些分支條件不會再被觸發。刪去這些冗餘的代碼可以優化性能,並減少代碼量。但這些改動會存在風險,因為不知道是剛好單元測試沒有覆蓋到,還是真的冗餘。為了增強信心,唯有再審視代碼的邏輯,並增加更多的測試數據及代碼。
  4. 一些應該測試的分支,例如中途返回錯誤的地方。
  5. 一些鎖碎的編譯器相關問題。例如gcc開了-Wswitch-default,但在default里做assert(false)這種沒有覆蓋的情況。

經過幾天努力,60多個commit、新增1000多行(不少新文件的license header??)、刪減200多行,終於完成了 100% line of code coverage 的PR。

如其他答案所談及,代碼覆蓋並不等於測試質量、代碼質量。除非使用Formal method,一般來說測試是無法證明代碼的正確性的。但即使代碼覆蓋(甚至只是最簡單的line of code coverage)有許多潛在問題,適當地使用它作為一種手段/工具,也是有相當有幫助的,以下列出一些原因:

  1. 我是粗心大意、善忘的。在這個案例中,發現有許多問題是因為改動代碼後,忘記了增加、修改相關測試。當項目開發持續很長時間,也會忘記許多約定、細節,修改代碼時只求通過測試,而沒考慮到修改會否令其他代碼變成死代碼。
  2. 我是懶惰的(較堂皇的說法是時間所限)。雖然理想地應該為每個函數去做單元測試,但有時候只測試了上一層的代碼。並沒有深究每個函數中是否完全覆蓋。尤其通常只測試正常路徑,而容易忽略一些異常路徑。如果參考了別人的演算法實現,更容易因為一知半解而忽略各種情況。

總括而言,我現在認為,最好是在CI中加入代碼覆蓋分析。如果能在開始開發時,以代碼覆蓋作為工具粗略分析測試是否充分,可以避免以上的一些問題。並且通過CI,我們能追蹤每個commit是否有潛在問題。尤其是分析開源項目中社區的PR時,能客觀地看到是否缺乏最低限度的測試。

如果要簡單地回答自己設下的問題,就是盡量達到100%的覆蓋率。許多程序因為各種原因難以覆蓋,或是所費精力太大,不必強求達至100%,但需要清楚了解箇中原因。然後,除了關注覆蓋率的絕對值,也要關注它的變化,求進步。


把測試覆蓋作為質量目標沒有任何意義,我們應該把它作為一種發現未被測試覆蓋的代碼的手段。

所以100%的代碼覆蓋率還值得追求嗎?

當然,這應該是每個程序員畢生的追求之一,但是如果從項目角度考慮ROI(投入產出比),對於需要快速上線的短期項目,需要注重的是讓測試覆蓋核心功能代碼。如果你的項目是一個長期項目,那麼高覆蓋率是非常有必要的,它意味著高可維護性,以及更少的bug。(前提是你的測試採用TDD/BDD方式編寫,我見過將測試代碼寫的一團糟的人,看著他的代碼,我寧願重新寫一遍)

那麼對於一個項目來說,覆蓋率應該達到多少?

其實沒有適用於所有項目的數值,每個項目都應有自己的閾值,但共性是,測試必須覆蓋主要業務場景,代碼的邏輯分支也必須儘可能的覆蓋。

如何改進你的項目代碼覆蓋率?

首先我們要閱讀和理解項目代碼,找出其中需要測試並且與業務強相關的代碼,結合sonar等代碼質量管理平台,從代碼編寫規範、複雜度、重複代碼等方面進行代碼重構,進一步提高項目的可維護性與可讀性。

這也意味著重構,重構的同時,你需要更多的測試來保證你重構代碼的正確性。

其次要對code coverage進行度量分析,那麼我們應該怎麼度量code coverage?

一般來說我們從以下四個維度來度量,如上圖所示:

  1. 行覆蓋率(line coverage):度量被測代碼中每個可執行語句是否都被執行到,但不包括java import,空行,注釋等。
  2. 函數覆蓋率(function coverage):度量被測代碼中每個定義的函數是否都被調用。
  3. 分支覆蓋率(branch coverage):度量被測代碼中每一個判定的分支是否都被測試到。
  4. 語句覆蓋率(statement coverage):度量被測代碼是否每個語句都被執行。

所以行覆蓋率的高低不能說明項目的好壞,我們要從多方面進行思考,一般我們遵循的標準應是:函數覆蓋率 &> 分支覆蓋率 &> 語句覆蓋率**

代碼覆蓋率最重要的意義在於:

  • 閱讀分析之前項目中未覆蓋部分的代碼,進而反推在前期QA以及相關測試人員在進行黑盒測試設計時是否考慮充分,沒有覆蓋到的代碼是否是測試設計的盲點,為什麼沒有考慮到?是需求或者UX設計不夠清晰,還是測試設計的理解有誤。
  • 檢測出程序中的廢代碼,可以逆向反推代碼設計中不合理的地方,提醒設計/開發人員理清代碼邏輯關係,提升代碼質量。
  • 代碼覆蓋率高不能說明代碼質量高,但是反過來看,代碼覆蓋率低,代碼質量絕對不會高到哪裡去,可以作為測試自我審視的重要工具之一。

結束語

單元測試的覆蓋率並不只是為了取悅客戶或者管理層的數據,它能夠實實在在反應項目中代碼的健康程度,幫助我們更好的改善了代碼的質量,增加了我們對所編寫代碼的信心。

文/ThoughtWorks齊磊 來源:都100%代碼覆蓋了,還會有什麼問題? - ThoughtWorks洞見


對於單元測試來說當然不需要,因為單元測試的其中一個要求就是要跑的快。但是單元測試也只是所有測試的1%而已。開發的時候,找一個伺服器24小時不斷地從source control抓代碼跑100%的scenario test,掛了的話自動抄送所有人高亮相關的check in,是很重要的。

這麼說讓我想起了一個我開發 https://github.com/vczh/herodb 的故事。資料庫文件都是按page分配的,那當然會有一個bit數組的page的鏈表來記錄什麼page被釋放了,什麼page沒有被釋放。如果你的page是64K,那麼當你要填滿一個bit的數組page,從而開闢一個新的bit數組page的話,就需要那個文件至少超過32G。顯然在單元測試裡面這麼干是不合適的,但是不這麼干就會有一大段代碼沒有被覆蓋。那這說明一個什麼問題呢?說明我的class職責太多了,本應該可以單獨拿出來運行的bit數組page的幾個函數必須要通過分配page才能跑,這是不對的。後來我就重構了,輕鬆100%。

當然了,為了解決上述問題而測試private函數是不正確的。因為如果你沒有把那幾個函數單獨拿出來,你永遠都不知道這些函數依賴的狀態到底會不會跟別人耦合。你連代碼的耦合都不明白,測試顯然只是白測試,有可能最後的結果是負負得正,你的case看起來過了,但是代碼實際上是錯誤的。所以這就是為什麼一定要重構。把一個class拆成若干偏序依賴的class,有助於把沒關係的狀態分開,你測試他們就相當放心。

private函數就是private的,永遠都不應該直接測試。


100% coverage 是個 holy grail,它並不能證明代碼質量。分幾個方面:

  1. 對於稍微複雜一點的純計算任務,100% converge 並不能證明代碼質量。比方說,對於 binary search,我估計只需要兩個 test cases 就能做到 100% coverage(一個找到,一個找不到),但這不能證明演算法是正確的,能處理各種 corner cases。
  2. 跟 IO error handling 相關的代碼很難做到 100% coverage,因為很多異常情況很難復現(EINTR、disk full、bad sectors、等等),要想做到 100% coverage 通常要寫一堆 mock,這樣一來,就算被測代碼做到了 100% coverage,也不能保證實際遇到 IO error 程序能正常工作。
  3. 對於多線程代碼,難的是測試各種 interleaving 的情況,這時候 coverage 根本幫不上什麼忙。

就怕 coverage 變成和考勤一樣的東西,管理部門喜歡抓,因為有數字,好看,而員工明白那玩意兒沒啥實際意義。因此我反對把 100% coverage 作為硬性指標,其實刷 coverage 不難,大家這麼聰明,有的是辦法。相反,衡量自動化測試的質量是很難的,你只知道做了什麼,不知道還缺什麼。


每當提到code coverage不提到底是哪種coverage的都是耍流氓嗯。

如果說的coverage是line coverage的話,連這個都達不到100%說明:

  • 測試沒有覆蓋所有功能:是驢是馬拉出來溜溜,如果有代碼連一次都沒在測試中被跑過的話,大家心裡有底不?(當然強力的語言/強力的類型系統是能讓大家「更有底」…)
  • 有死代碼

前者要補充測試。其實有時候某些代碼沒被測過表面上有很好的理由:狀況太邊角了,或者說例如錯誤處理要應對的錯誤太奇怪了不好構造測試用例。這些可能都是實際的困難,但是越是「用來保障可靠性」的代碼有可能越是因此沒被測試過,這心裡能有底么…

後者其實很多人都有習慣把一些尚不能用或者已經不再使用的代碼留在源碼中,這實在沒必要。有版本控制的話老代碼總能挖出來,暫時還用不了的代碼就別放進主線版本里。

然而line coverage太弱了。更強力的coverage可能很多公司都做不到100%。

Branch coverage可能有些地方尚且能做到100%,condition coverage能做到100%的那簡直贊到爆。

別說100%、90%、80%…有時候接手了缺乏測試的legacy code來維護那簡直是欲哭無淚,根本沒辦法談coverage(ToT

(扭頭看向Z3…


完全沒有必要,雖然現在我正在搞的項目就是100%的單元測試代碼覆蓋率。

因為,即使達到100%的code coverage,也不能保證無bug,而達到100%的代價又很大,這樣的投入產出比(RoI)不值得。

那你可能要問,為啥我正在搞的項目是100%的code coverage呢?因為這個項目從寫第一天開始就保證了100%的coverage,當時幾位工程師一想,既然這樣,那就把threshold設為100%,低於這個threshold就算build失敗,既然開了這個頭,然後誰也不想降低這個標準,這個標準也就一直保持下來了。


  • 測試只能發現大約40%的問題,不是說不重要,而是說有個邊際效用遞減問題,為了追求數字上的完美,你可能要付出過多的代價,得不償失,還不如省下點力氣花在其它地方。
  • coverage高不代表質量一定好,coverage本身是可以騙人的。比如有人寫了"var a = getToken() + " is foo?";" 如果測試的時候忘了測試getToken()返回null,就這一行而言,覆蓋率還是100%,但其實漏了一個很重要的case,而且a不是空字元串,後面可能一系列問題也可以避免。
  • 當然如果你代碼爛,達不到很高覆蓋率,那就要重構。
  • 個人意見是重要的scenario不能遺漏,軟體工程是保證質量,不是完全消滅bug


之前某德國某公司搞汽車嵌入式,ASIL B 安全級別,C0, C1 項目交付都要求 100%。到不了 100% 的全部要給出詳細的說明。比 Coverage 更變態的其實是 MISRA。


實際項目中,項目經理和架構師往往也是不錯的測試員,一些嚴重bug,經常是他們先發現,比測試員還快一點。

項目中有很多的function, 但function之間的重要性是不同的,也就是說,是不均勻的,有的重要,有的沒那麼重要,同樣是80%的覆蓋率,一個覆蓋到最重要的function,另一個沒有,最後的結果也是天差地別的。

和覆蓋率相比,更重要的是測試的順序,確保最常用,最重要,最核心的功能先測試到,有bug,先發現,先解決,這樣測試才高效,團隊也會越測越有信心。

這也需要測試員對項目和需求有更深入的理解。

覆蓋率高,當然好,但工程類的東西往往需要妥協和平衡,時間不夠時,先測什麼,後測什麼,就更重要一些了。


100%的覆蓋率的確是好的,但成本很高。如果不是什麼長期項目,很少會要求這麼高,互聯網行業,現在的業務代碼很少有公司還去要求代碼覆蓋率。

因為往往一個功能從設計、編碼、測試、上線、下線生命周期長則一個月,短則一個星期。往往是開發、測試、產品齊聚一堂,完成全流程……

如果要求100%的分支覆蓋率那幾乎是做不到的,曾經在狼廠,QA老大拍腦袋要求單元測試分支覆蓋率達到90%,有個研發同學每次都做得挺好,我們就很好奇。

各種利誘之下他說出了真相,他寫了個隱蔽的「單側函數」void _makeQAHappy(void),它的作用就是用設定不同的argv去調用main函數……

這就是典型的「上有政策,下有對策」


要求condition coverage 100%的飄過

我真的可以保證

無論你是什麼樣的牛人

你絕對可以發現你沒想到的bug

測試不到的代碼,應該有review ,他要麼是壞的設計,要麼是某些特殊觸發條件

同樣,測試不到的代碼不應該放進分母里,所以說做不到100%的,其實並不是這樣

當然很花錢。。

line coverage 是一個比較弱的約束

一般推薦branch coverage ,能測試的代碼100%覆蓋,總體來說應該在90%左右為好。


指數爆炸:當我們有32個選項時,要做到全面覆蓋,需要2^32種可能。跑一種假設要一分鐘,那麼需要8171年才能跑完。所以結論是某些情況下不可能全面覆蓋。推薦 程序員的數學 這本書,有趣味,開視野。還有一本圖解密碼學,對稱非對稱,dh密鑰交換,rsa,gpg,https都有介紹


我來補充一種情況。

整個工程的代碼分為兩種類型,一種是類庫性質的代碼,一種是項目代碼。一般來說類庫代碼更抽象,也更通用。項目代碼貼合業務場景,相應的限制也更多。如果類庫代碼被抽取出來,獨立做一個build,那自然可以把用例分為兩部分,一部分做類庫的用例,一部分把類庫當作黑盒只做項目代碼角度的用例。這種情況下,項目代碼的line coverage是可以做到。 但是假如因為某種原因,兩種無法分離,那麼再要求整個項目都line coverage就會坑了,因為要在類庫這層增加很多與當前業務無關的工作量,而這個工作量是因為你做了抽象而生出來的。這種情況在那種初創項目然後折騰出來某個類庫/框架原型的的場景里特別多見。往小的說,不是一個類庫,僅僅是某個模式某個類或某個方法,你想了想,好像可以抽象一點,然後你調整代碼結構,果然結構更合理了,職責更單一了,但是相應的測試工作量也上來了。。。


覺得coverage要求變態的,建議去跑一個 misra 2012再來看這個問題,可能就想法完全不一樣了。

穆哈哈哈哈哈


如果覆蓋率真做到100%,那說明你是一個有追求的人,而且在提升覆蓋率的過程中肯定發現了一些意想不到的bug


我覺得分兩種情況:

1. 團隊有比較明確的指標和流程,這個時候測試覆蓋率一般只要80-90%應該就可以了

2. 就是我現在的團隊,我沒有待過大公司的團隊,待的幾個小公司測試都是手動測試,沒有自動化。所以自動測試是我從基礎搭建起來的

其實我並不贊成100%的測試,原因前面很多人已經講過了,但是我還是強迫我們團隊執行100%測試,因此還特意搞了一個HOOK API來代替MOCK

如果不強迫100%的測試,我們測試基本大家都是想寫就寫,不想寫就不寫,跟注釋一樣。大家都是覺得可有可無的東西。所以我現在是強迫寫測試,強迫寫注釋。

大概執行5個多月,效果還是比較好的,注釋基本能夠生成跟QT一樣的文檔,用到其他模塊可以直接看頭文件而不是像之前那樣找到寫這個模塊的人或者自己一行一行看代碼。測試也能做到代碼有問題立馬上報,不用再像之前天天在BUG之間疲於奔命。

所以我覺得,如果你的團隊類似很多小公司那樣,測試基本靠手,大家沒有這個意識,可以試試我這個方法。我現在這個項目基本不用每次領導要來參觀或者發布版本的時候提心弔膽...

還有,寫完代碼自己寫測試,或者寫完測試寫代碼,這種人寫出來的代碼穩定性和可讀性以及擴展性都會比較高


飛機上有很多航電設備都是這個要求,自己想像降落的時候突然不受控制了會怎麼樣。


首先,100%不是目標,只是衡量測試覆蓋率的手段

其次, 覆蓋率高不能表示代碼質量好,但覆蓋 率高的代碼通常比覆蓋率低的代碼bug要少

最後, 不是所有的代碼都能覆蓋,比如防禦代碼,但至少要知道為什麼沒有覆蓋。


對於項目中的entity,interface難道也需要去cover? 所以,去掉這些,你根本達不到100%,之前我們的項目,覆蓋率在60%~70%之間,這已經很花時間了。


我們公司要求90%以上的Code Coverage,以及80%以上的Branch Courage。實際情況是,很多測試代碼寫的非常渣,嚴重影響添加新功能 / 修改代碼。

相關:我四月22日就要離職了~目前無比期待。


推薦閱讀:

除了計算機相關專業,大學裡哪些專業也在學習編程?
什麼是「編程天賦」?你見到的最有「編程天賦」的人是什麼樣的?
什麼是 Agile Software Development(敏捷軟體開發)?
二分法調試代碼具體指什麼?
zipline和rqalpha對比?

TAG:編程 | 軟體工程 |