posts - 11, comments - 3, trackbacks - 0, articles - 0
            BlogJava :: 首頁(yè) :: 新隨筆 :: 聯(lián)系 :: 聚合  :: 管理

          http://www-128.ibm.com/developerworks/cn/java/j-cq01316/#N10149

          追求代碼質(zhì)量: 不要被覆蓋報(bào)告所迷惑

          您是否曾被測(cè)試覆蓋度量引入歧途?

          developerWorks
          文檔選項(xiàng)
          將此頁(yè)作為電子郵件發(fā)送

          將此頁(yè)作為電子郵件發(fā)送

          未顯示需要 JavaScript 的文檔選項(xiàng)

          討論


          最新推薦

          Java 應(yīng)用開(kāi)發(fā)源動(dòng)力 - 下載免費(fèi)軟件,快速啟動(dòng)開(kāi)發(fā)


          級(jí)別: 初級(jí)

          Andrew Glover , CTO, Vanward Technologies

          2006 年 2 月 06 日

          測(cè)試覆蓋工具對(duì)單元測(cè)試具有重要的意義,但是經(jīng)常被誤用。這個(gè)月,Andrew Glover 會(huì)在他的新系列 —— 追求代碼質(zhì)量 中向您介紹值得參考的專家意見(jiàn)。第一部分深入地介紹覆蓋報(bào)告中數(shù)字的真實(shí)含義。然后他會(huì)提出您可以盡早并經(jīng)常地利用覆蓋來(lái)確保代碼質(zhì)量的三個(gè)方法。

          您還記得以前大多數(shù)開(kāi)發(fā)人員是如何追求代碼質(zhì)量的嗎。在那時(shí),有技巧地放置 main() 方法被視為靈活且適當(dāng)?shù)臏y(cè)試方法。經(jīng)歷了漫長(zhǎng)的道路以后,現(xiàn)在自動(dòng)測(cè)試已經(jīng)成為高質(zhì)量代碼開(kāi)發(fā)的基本保證,對(duì)此我很感謝。但是這還不是我所要感謝的全部。Java? 開(kāi)發(fā)人員現(xiàn)在擁有很多通過(guò)代碼度量、靜態(tài)分析等方法來(lái)度量代碼質(zhì)量的工具。我們甚至已經(jīng)設(shè)法將重構(gòu)分類成一系列便利的模式!

          要獲得有關(guān)代碼質(zhì)量問(wèn)題的答案,您可以訪問(wèn)由 Andrew Glover 主持的 Code Quality 論壇。

          所有的這些新的工具使得確保代碼質(zhì)量比以前簡(jiǎn)單得多,不過(guò)您還需要知道如何使用它們。在這個(gè)系列中,我將重點(diǎn)闡述有關(guān)保證代碼質(zhì)量的一些有時(shí)看上去有點(diǎn)神秘的東西。除了帶您一起熟悉有關(guān)代碼質(zhì)量保證的眾多工具和技術(shù)之外,我還將為您說(shuō)明:

          • 定義并有效度量最影響質(zhì)量的代碼方面。
          • 設(shè)定質(zhì)量保證目標(biāo)并照此規(guī)劃您的開(kāi)發(fā)過(guò)程。
          • 確定哪個(gè)代碼質(zhì)量工具和技術(shù)可以滿足您的需要。
          • 實(shí)現(xiàn)最佳實(shí)踐(清除不好的),使確保代碼質(zhì)量及早并經(jīng)常地 成為開(kāi)發(fā)實(shí)踐中輕松且有效的方面。

          在這個(gè)月,我將首先看看 Java 開(kāi)發(fā)人員中最流行也是最容易的質(zhì)量保證工具包:測(cè)試覆蓋度量。

          謹(jǐn)防上當(dāng)

          這是一個(gè)晚上鏖戰(zhàn)后的早晨,大家都站在飲水機(jī)邊上。開(kāi)發(fā)人員和管理人員們了解到一些經(jīng)過(guò)良好測(cè)試的類可以達(dá)到超過(guò) 90% 的覆蓋率,正在高興地互換著 NFL 風(fēng)格的點(diǎn)心。團(tuán)隊(duì)的集體信心空前高漲。從遠(yuǎn)處可以聽(tīng)到 “放任地重構(gòu)吧” 的聲音,似乎缺陷已成為遙遠(yuǎn)的記憶,響應(yīng)性也已微不足道。但是一個(gè)很小的反對(duì)聲在說(shuō):

          女士們,先生們,不要被覆蓋報(bào)告所愚弄

          現(xiàn)在,不要誤解我的意思:并不是說(shuō)使用測(cè)試覆蓋工具是愚蠢的。對(duì)單元測(cè)試范例,它是很重要的。不過(guò)更重要的是您如何理解所得到的信息。許多開(kāi)發(fā)團(tuán)隊(duì)會(huì)在這兒犯第一個(gè)錯(cuò)。

          高覆蓋率只是表示執(zhí)行了很多的代碼,并不意味著這些代碼被很好地 執(zhí)行。如果您關(guān)注的是代碼的質(zhì)量,就必須精確地理解測(cè)試覆蓋工具能做什么,不能做什么。然后您才能知道如何使用這些工具去獲取有用的信息。而不是像許多開(kāi)發(fā)人員那樣,只是滿足于高覆蓋率。


          測(cè)試覆蓋度量

          測(cè)試覆蓋工具通常可以很容易地添加到確定的單元測(cè)試過(guò)程中,而且結(jié)果可靠。下載一個(gè)可用的工具,對(duì)您的 Ant 和 Maven 構(gòu)建腳本作一些小的改動(dòng),您和您的同事就有了在飲水機(jī)邊上談?wù)摰囊环N新報(bào)告:測(cè)試覆蓋報(bào)告。當(dāng) foobar 這樣的程序包令人驚奇地顯示 覆蓋率時(shí),您可以得到不小的安慰。如果您相信至少您的部分代碼可以保證是 “沒(méi)有 BUG” 的,您會(huì)覺(jué)得很安心。但是這樣做是一個(gè)錯(cuò)誤。

          存在不同類型的覆蓋度量,但是絕大多數(shù)的工具會(huì)關(guān)注行覆蓋,也叫做語(yǔ)句覆蓋。此外,有些工具會(huì)報(bào)告分支覆蓋。通過(guò)用一個(gè)測(cè)試工具執(zhí)行代碼庫(kù)并捕獲整個(gè)測(cè)試過(guò)程中與被 “觸及” 的代碼對(duì)應(yīng)的數(shù)據(jù),就可以獲得測(cè)試覆蓋度量。然后這些數(shù)據(jù)被合成為覆蓋報(bào)告。在 Java 世界中,這個(gè)測(cè)試工具通常是 JUnit 以及名為 Cobertura、Emma 或 Clover 等的覆蓋工具。

          行覆蓋只是指出代碼的哪些行被執(zhí)行。如果一個(gè)方法有 10 行代碼,其中的 8 行在測(cè)試中被執(zhí)行,那么這個(gè)方法的行覆蓋率是 80%。這個(gè)過(guò)程在總體層次上也工作得很好:如果一個(gè)類有 100 行代碼,其中的 45 行被觸及,那么這個(gè)類的行覆蓋率就是 45%。同樣,如果一個(gè)代碼庫(kù)包含 10000 個(gè)非注釋性的代碼行,在特定的測(cè)試運(yùn)行中有 3500 行被執(zhí)行,那么這段代碼的行覆蓋率就是 35%。

          報(bào)告分支覆蓋 的工具試圖度量決策點(diǎn)(比如包含邏輯 ANDOR 的條件塊)的覆蓋率。與行覆蓋一樣,如果在特定方法中有兩個(gè)分支,并且兩個(gè)分支在測(cè)試中都被覆蓋,那么您可以說(shuō)這個(gè)方法有 100% 的分支覆蓋率。

          問(wèn)題是,這些度量有什么用?很明顯,很容易獲得所有這些信息,不過(guò)您需要知道如何使用它們。一些例子可以闡明我的觀點(diǎn)。





          回頁(yè)首


          代碼覆蓋在活動(dòng)

          我在清單 1 中創(chuàng)建了一個(gè)簡(jiǎn)單的類以具體表述類層次的概念。一個(gè)給定的類可以有一連串的父類,例如 Vector,它的父類是 AbstractListAbstractList 的父類又是 AbstractCollectionAbstractCollection 的父類又是 Object


          清單 1. 表現(xiàn)類層次的類
          												
          														package com.vanward.adana.hierarchy;
          
          import java.util.ArrayList;
          import java.util.Collection;
          import java.util.Iterator;
          
          public class Hierarchy {
            private Collection classes;
            private Class baseClass;
          
            public Hierarchy() {
              super();
              this.classes = new ArrayList();
            }
          
            public void addClass(final Class clzz){
              this.classes.add(clzz);
            }
            /**
             * @return an array of class names as Strings
             */
            public String[] getHierarchyClassNames(){
              final String[] names = new String[this.classes.size()];        
              int x = 0;
              for(Iterator iter = this.classes.iterator(); iter.hasNext();){
                 Class clzz = (Class)iter.next();
                 names[x++] = clzz.getName();
              }        
              return names;
            }
          
            public Class getBaseClass() {
              return baseClass;
            }
          
            public void setBaseClass(final Class baseClass) {
              this.baseClass = baseClass;
            }
          }
          
          												
          										

          正如您看到的,清單 1 中的 Hierarchy 類具有一個(gè) baseClass 實(shí)例以及它的父類的集合。清單 2 中的 HierarchyBuilder 通過(guò)兩個(gè)復(fù)制 buildHierarchy 的重載的 static 方法創(chuàng)建了 Hierarchy 類。


          清單 2. 類層次生成器
          												
          														package com.vanward.adana.hierarchy;
          
          public class HierarchyBuilder {  
          
            private HierarchyBuilder() {
              super();		
            }
          
            public static Hierarchy buildHierarchy(final String clzzName) 
              throws ClassNotFoundException{
                final Class clzz = Class.forName(clzzName, false, 
                    HierarchyBuilder.class.getClassLoader());        
                return buildHierarchy(clzz);
            }
          
            public static Hierarchy buildHierarchy(Class clzz){
              if(clzz == null){
                throw new RuntimeException("Class parameter can not be null");
              }
          
              final Hierarchy hier = new Hierarchy();
              hier.setBaseClass(clzz);
          
              final Class superclass = clzz.getSuperclass();
          
              if(superclass != 
                null && superclass.getName().equals("java.lang.Object")){
                 return hier; 
              }else{      
                 while((clzz.getSuperclass() != null) && 
                    (!clzz.getSuperclass().getName().equals("java.lang.Object"))){
                       clzz = clzz.getSuperclass();
                       hier.addClass(clzz);
                 }	        
                 return hier;
              }
            }      
          }
          
          												
          										





          回頁(yè)首


          現(xiàn)在是測(cè)試時(shí)間!

          有關(guān)測(cè)試覆蓋的文章怎么能缺少測(cè)試案例呢?在清單 3 中,我定義了一個(gè)簡(jiǎn)單的有三個(gè)測(cè)試案例的 JUnit 測(cè)試類,它將試圖執(zhí)行 Hierarchy 類和 HierarchyBuilder 類:


          清單 3. 測(cè)試 HierarchyBuilder!
          												
          														package test.com.vanward.adana.hierarchy;
          
          import com.vanward.adana.hierarchy.Hierarchy;
          import com.vanward.adana.hierarchy.HierarchyBuilder;
          import junit.framework.TestCase;
          
          public class HierarchyBuilderTest extends TestCase {
            
            public void testBuildHierarchyValueNotNull() {        
               Hierarchy hier = HierarchyBuilder.buildHierarchy(HierarchyBuilderTest.class);
               assertNotNull("object was null", hier);
            }
          
            public void testBuildHierarchyName() {        
               Hierarchy hier = HierarchyBuilder.buildHierarchy(HierarchyBuilderTest.class);
               assertEquals("should be junit.framework.Assert", 
                 "junit.framework.Assert", 
                   hier.getHierarchyClassNames()[1]);      
            }
          
            public void testBuildHierarchyNameAgain() {        
               Hierarchy hier = HierarchyBuilder.buildHierarchy(HierarchyBuilderTest.class);
               assertEquals("should be junit.framework.TestCase", 
                 "junit.framework.TestCase", 
                   hier.getHierarchyClassNames()[0]);      
            }
           
          }
          
          												
          										

          因?yàn)槲沂且粋€(gè)狂熱的測(cè)試人員,我自然希望運(yùn)行一些覆蓋測(cè)試。對(duì)于 Java 開(kāi)發(fā)人員可用的代碼覆蓋工具中,我比較喜歡用 Cobertura,因?yàn)樗膱?bào)告很友好。而且,Corbertura 是開(kāi)放源碼項(xiàng)目,它派生出了 JCoverage 項(xiàng)目的前身。





          回頁(yè)首


          Cobertura 的報(bào)告

          運(yùn)行 Cobertura 這樣的工具和運(yùn)行您的 JUnit 測(cè)試一樣簡(jiǎn)單,只是有一個(gè)用專門(mén)邏輯在測(cè)試時(shí)檢查代碼以報(bào)告覆蓋率的中間步驟(這都是通過(guò)工具的 Ant 任務(wù)或 Maven 的目標(biāo)完成的)。

          正如您在圖 1 中看到的,HierarchyBuilder 的覆蓋報(bào)告說(shuō)明部分代碼沒(méi)有 被執(zhí)行。事實(shí)上,Cobertura 認(rèn)為 HierarchyBuilder 的行覆蓋率為 59%,分支覆蓋率為 75%。


          圖 1. Cobertura 的報(bào)告

          這樣看來(lái),我的第一次覆蓋測(cè)試是失敗的。首先,帶有 String 參數(shù)的 buildHierarchy() 方法根本沒(méi)有被測(cè)試。其次,另一個(gè) buildHierarchy() 方法中的兩個(gè)條件都沒(méi)有被執(zhí)行。有趣的是,所要關(guān)注的正是第二個(gè)沒(méi)有被執(zhí)行的 if 塊。

          因?yàn)槲宜枰龅闹皇窃黾右恍y(cè)試案例,所以我并不擔(dān)心這一點(diǎn)。一旦我到達(dá)了所關(guān)注的區(qū)域,我就可以很好地完成工作。注意我這兒的邏輯:我使用測(cè)試報(bào)告來(lái)了解什么沒(méi)有 被測(cè)試。現(xiàn)在我已經(jīng)可以選擇使用這些數(shù)據(jù)來(lái)增強(qiáng)測(cè)試或者繼續(xù)工作。在本例中,我準(zhǔn)備增強(qiáng)我的測(cè)試,因?yàn)槲疫€有一些重要的區(qū)域未覆蓋。

          Cobertura:第二輪

          清單 4 是一個(gè)更新過(guò)的 JUnit 測(cè)試案例,增加了一些附加測(cè)試案例,以試圖完全執(zhí)行 HierarchyBuilder


          清單 4. 更新過(guò)的 JUnit 測(cè)試案例
          												
          														package test.com.vanward.adana.hierarchy;
          
          import com.vanward.adana.hierarchy.Hierarchy;
          import com.vanward.adana.hierarchy.HierarchyBuilder;
          import junit.framework.TestCase;
          
          public class HierarchyBuilderTest extends TestCase {
            
            public void testBuildHierarchyValueNotNull() {        
               Hierarchy hier = HierarchyBuilder.buildHierarchy(HierarchyBuilderTest.class);
               assertNotNull("object was null", hier);
            }
          
            public void testBuildHierarchyName() {        
               Hierarchy hier = HierarchyBuilder.buildHierarchy(HierarchyBuilderTest.class);
               assertEquals("should be junit.framework.Assert", 
                 "junit.framework.Assert", 
                   hier.getHierarchyClassNames()[1]);      
            }
          
            public void testBuildHierarchyNameAgain() { zo       
               Hierarchy hier = HierarchyBuilder.buildHierarchy(HierarchyBuilderTest.class);
               assertEquals("should be junit.framework.TestCase", 
                 "junit.framework.TestCase", 
                   hier.getHierarchyClassNames()[0]);      
            }
          
            public void testBuildHierarchySize() {        
               Hierarchy hier = HierarchyBuilder.buildHierarchy(HierarchyBuilderTest.class);
               assertEquals("should be 2", 2, hier.getHierarchyClassNames().length);
            }
          
            public void testBuildHierarchyStrNotNull() throws Exception{
              Hierarchy hier = 
                 HierarchyBuilder.
                 buildHierarchy("test.com.vanward.adana.hierarchy.HierarchyBuilderTest");
              assertNotNull("object was null", hier);
            }
          
            public void testBuildHierarchyStrName() throws Exception{        
              Hierarchy hier = 
                 HierarchyBuilder.
                 buildHierarchy("test.com.vanward.adana.hierarchy.HierarchyBuilderTest");
              assertEquals("should be junit.framework.Assert", 
                "junit.framework.Assert",
                  hier.getHierarchyClassNames()[1]);
            }
          
            public void testBuildHierarchyStrNameAgain() throws Exception{
              Hierarchy hier = 
                 HierarchyBuilder.
                 buildHierarchy("test.com.vanward.adana.hierarchy.HierarchyBuilderTest");
              assertEquals("should be junit.framework.TestCase", 
                "junit.framework.TestCase",
                  hier.getHierarchyClassNames()[0]);      
            }
          
            public void testBuildHierarchyStrSize() throws Exception{        
               Hierarchy hier = 
                  HierarchyBuilder.
                  buildHierarchy("test.com.vanward.adana.hierarchy.HierarchyBuilderTest");
               assertEquals("should be 2", 2, hier.getHierarchyClassNames().length);        
            }
          
            public void testBuildHierarchyWithNull() {
               try{
                 Class clzz = null;
                 HierarchyBuilder.buildHierarchy(clzz);
                 fail("RuntimeException not thrown");
               }catch(RuntimeException e){}
            }
          }
          
          												
          										

          當(dāng)我使用新的測(cè)試案例再次執(zhí)行測(cè)試覆蓋過(guò)程時(shí),我得到了如圖 2 所示的更加完整的報(bào)告。現(xiàn)在,我覆蓋了未測(cè)試的 buildHierarchy() 方法,也處理了另一個(gè) buildHierarchy() 方法中的兩個(gè) if 塊。然而,因?yàn)?HierarchyBuilder 的構(gòu)造器是 private 類型的,所以我不能通過(guò)我的測(cè)試類測(cè)試它(我也不關(guān)心)。因此,我的行覆蓋率仍然只有 88%。


          圖 2. 誰(shuí)說(shuō)沒(méi)有第二次機(jī)會(huì)

          正如您看到的,使用一個(gè)代碼覆蓋工具可以 揭露重要的沒(méi)有相應(yīng)測(cè)試案例的代碼。重要的事情是,在閱讀報(bào)告(特別 是覆蓋率高的)時(shí)需要小心,它們也許隱含危險(xiǎn)的信息。讓我們看看兩個(gè)例子,看看在高覆蓋率后面隱藏著什么。





          回頁(yè)首


          條件帶來(lái)的麻煩

          正如您已經(jīng)知道的,代碼中的許多變量可能有多種狀態(tài);此外,條件的存在使得執(zhí)行有多條路徑。在留意這些問(wèn)題之后,我將在清單 5 中定義一個(gè)極其簡(jiǎn)單只有一個(gè)方法的類:


          清單 5.您能看出下面的缺陷嗎?
          												
          														package com.vanward.coverage.example01;
          
          public class PathCoverage {
          
            public String pathExample(boolean condition){
              String value = null;
              if(condition){
                value = " " + condition + " ";
              }
              return value.trim();
            }
          }
          
          												
          										

          您是否發(fā)現(xiàn)了清單 5 中有一個(gè)隱藏的缺陷呢?如果沒(méi)有,不要擔(dān)心,我會(huì)在清單 6 中寫(xiě)一個(gè)測(cè)試案例來(lái)執(zhí)行 pathExample() 方法并確保它正確地工作:


          清單 6. JUnit 來(lái)救援!
          												
          														package test.com.vanward.coverage.example01;
          
          import junit.framework.TestCase;
          import com.vanward.coverage.example01.PathCoverage;
          
          public class PathCoverageTest extends TestCase {
          
            public final void testPathExample() {
              PathCoverage clzzUnderTst = new PathCoverage();
              String value = clzzUnderTst.pathExample(true);
              assertEquals("should be true", "true", value);
            }
          }
          
          												
          										

          我的測(cè)試案例正確運(yùn)行,我的神奇的代碼覆蓋報(bào)告(如下面圖 3 所示)使我看上去像個(gè)超級(jí)明星,測(cè)試覆蓋率達(dá)到了 100%!


          圖 3. 覆蓋率明星

          我想現(xiàn)在應(yīng)該到飲水機(jī)邊上去說(shuō)了,但是等等,我不是懷疑代碼中有什么缺陷呢?認(rèn)真檢查清單 5 會(huì)發(fā)現(xiàn),如果 conditionfalse,那么第 13 行確實(shí)會(huì)拋出 NullPointerExceptionYeesh,這兒發(fā)生了什么?

          這表明行覆蓋的確不能很好地指示測(cè)試的有效性。





          回頁(yè)首


          路徑的恐怖

          在清單 7 中,我定義了另一個(gè)包含 indirect 的簡(jiǎn)單例子,它仍然有不能容忍的缺陷。請(qǐng)注意 branchIt() 方法中 if 條件的后半部分。(HiddenObject 類將在清單 8 中定義。)


          清單 7. 這個(gè)代碼足夠簡(jiǎn)單
          												
          														package com.vanward.coverage.example02;
          
          import com.acme.someotherpackage.HiddenObject;
          
          public class AnotherBranchCoverage {
             
            public void branchIt(int value){
              if((value > 100) || (HiddenObject.doWork() == 0)){
                this.dontDoIt();
              }else{
                this.doIt();
              }
            }                             
          
            private void dontDoIt(){
              //don't do something...
            }
          
            private void doIt(){
              //do something!
            }   
          }
          
          												
          										

          呀!清單 8 中的 HiddenObject有害的。與清單 7 中一樣,調(diào)用 doWork() 方法會(huì)導(dǎo)致 RuntimeException


          清單 8. 上半部分!
          												
          														package com.acme.someotherpackage.HiddenObject;
          
          public class HiddenObject {
          
            public static int doWork(){
              //return 1;
              throw new RuntimeException("surprise!");
            }
          }
          
          												
          										

          但是我的確可以通過(guò)一個(gè)良好的測(cè)試捕獲這個(gè)異常!在清單 9 中,我編寫(xiě)了另一個(gè)好的測(cè)試,以圖挽回我的超級(jí)明星光環(huán):


          清單 9. 使用 JUnit 規(guī)避風(fēng)險(xiǎn)
          												
          														package test.com.vanward.coverage.example02;
          
          import junit.framework.TestCase;
          import com.vanward.coverage.example02.AnotherBranchCoverage;
          
          public class AnotherBranchCoverageTest extends TestCase {
              
            public final void testBranchIt() {
              AnotherBranchCoverage clzzUnderTst = new AnotherBranchCoverage();
              clzzUnderTst.branchIt(101);
            }    
          }
          
          												
          										

          您對(duì)這個(gè)測(cè)試案例有什么想法?您也許會(huì)寫(xiě)出更多的測(cè)試案例,但是請(qǐng)?jiān)O(shè)想一下清單 7 中不確定的條件有不止一個(gè)的縮短操作會(huì)如何。設(shè)想如果前半部分中的邏輯比簡(jiǎn)單的 int 比較更復(fù)雜,那么 需要寫(xiě)多少測(cè)試案例才能滿意?

          僅僅給我數(shù)字

          現(xiàn)在,對(duì)清單 7、8、9 的測(cè)試覆蓋率的分析結(jié)果不再會(huì)使您感到驚訝。在圖 4 的報(bào)告中顯示我達(dá)到了 75% 的行覆蓋率和 100% 的分支覆蓋率。最重要的是,我執(zhí)行了第 10 行!


          圖 4.愚弄的報(bào)酬

          從第一印象看,這讓我驕傲。但是這個(gè)報(bào)告有什么誤導(dǎo)嗎?只是粗略地看一看報(bào)告中的數(shù)字,會(huì)導(dǎo)致您相信代碼是經(jīng)過(guò)良好測(cè)試的。基于這一點(diǎn),您也許會(huì)認(rèn)為出現(xiàn)缺陷的風(fēng)險(xiǎn)很低。這個(gè)報(bào)告并不能幫助您確定 or 縮短操作的后半部分是一個(gè)定時(shí)炸彈!





          回頁(yè)首


          質(zhì)量測(cè)試

          我不止一次地說(shuō):您可以(而且應(yīng)該)使用測(cè)試覆蓋工具作為您的測(cè)試過(guò)程的一部分。但是不要被覆蓋報(bào)告所愚弄。關(guān)于覆蓋報(bào)告您需要了解的主要事情是,覆蓋報(bào)告最好用來(lái)檢查哪些代碼沒(méi)有經(jīng)過(guò) 充分的測(cè)試。當(dāng)您檢查覆蓋報(bào)告時(shí),找出較低的值,并了解為什么特定的代碼沒(méi)有經(jīng)過(guò)充分的測(cè)試。知道這些以后,開(kāi)發(fā)人員、管理人員以及 QA 專業(yè)人員就可以在真正需要的地方使用測(cè)試覆蓋工具。通常有下列三種情況:

          • 估計(jì)修改已有代碼所需的時(shí)間
          • 評(píng)估代碼質(zhì)量
          • 評(píng)定功能測(cè)試

          現(xiàn)在我可以斷定對(duì)測(cè)試覆蓋報(bào)告的一些使用方法會(huì)將您引入歧途,下面這些最佳實(shí)踐可以使得測(cè)試覆蓋報(bào)告可以真正為您所用。

          1. 估計(jì)修改已有代碼所需的時(shí)間

          對(duì)一個(gè)開(kāi)發(fā)團(tuán)隊(duì)而言,針對(duì)代碼編寫(xiě)測(cè)試案例自然可以增加集體的信心。與沒(méi)有相應(yīng)測(cè)試案例的代碼相比,經(jīng)過(guò)測(cè)試的代碼更容易重構(gòu)、維護(hù)和增強(qiáng)。測(cè)試案例因?yàn)榘凳玖舜a在測(cè)試工作中是如何 工作的,所以還可以充當(dāng)內(nèi)行的文檔。此外,如果被測(cè)試的代碼發(fā)生改變,測(cè)試案例通常也會(huì)作相應(yīng)的改變,這與諸如注釋和 Javadoc 這樣的靜態(tài)代碼文檔不同。

          在另一方面,沒(méi)有經(jīng)過(guò)相應(yīng)測(cè)試的代碼更難于理解和安全地 修改。因此,知道代碼有沒(méi)有被測(cè)試,并看看實(shí)際的測(cè)試覆蓋數(shù)值,可以讓開(kāi)發(fā)人員和管理人員更準(zhǔn)確地預(yù)知修改已有代碼所需的時(shí)間。

          再次回到飲水機(jī)邊上,可以更好地闡明我的觀點(diǎn)。

          市場(chǎng)部的 Linda:“我們想讓系統(tǒng)在用戶完成一筆交易時(shí)做 x 工作。這需要多長(zhǎng)時(shí)間。我們的用戶需要盡快實(shí)現(xiàn)這一功能。”

          管理人員 Jeff:“讓我看看,這個(gè)代碼是 Joe 在幾個(gè)月前編寫(xiě)的,需要對(duì)業(yè)務(wù)層和 UI 做一些變動(dòng)。Mary 也許可以在兩天內(nèi)完成這項(xiàng)工作。”

          Linda:“Joe?他是誰(shuí)?”

          Jeff:“哦,Joe,因?yàn)樗恢雷约涸诟墒裁矗员晃医夤土恕!?

          情況似乎有點(diǎn)不妙,不是嗎?盡管如此,Jeff 還是將任務(wù)分配給了 Mary,Mary 也認(rèn)為能夠在兩天內(nèi)完成工作 —— 確切地說(shuō),在看到代碼之前她是這么認(rèn)為的。

          Mary:“Joe 寫(xiě)這些代碼時(shí)是不是睡著了?這是我所見(jiàn)過(guò)的最差的代碼。我甚至不能確認(rèn)這是 Java 代碼。除非推倒重來(lái),要不我根本沒(méi)法修改。”

          情況對(duì) “飲水機(jī)” 團(tuán)隊(duì)不妙,不是嗎?但是我們假設(shè),如果在這個(gè)不幸的事件的當(dāng)初,Jeff 和 Mary 就擁有一份測(cè)試報(bào)告,那么情況會(huì)如何呢?當(dāng) Linda 要求實(shí)現(xiàn)新功能時(shí),Jeff 做的第一件事就是檢查以前生成的覆蓋報(bào)告。注意到需要改動(dòng)的軟件包幾乎沒(méi)有被覆蓋,然后他就會(huì)與 Mary 商量。

          Jeff:“Joe 編寫(xiě)的這個(gè)代碼很差,絕大多數(shù)沒(méi)經(jīng)過(guò)測(cè)試。您認(rèn)為要支持 Linda 所說(shuō)的功能需要多長(zhǎng)時(shí)間?”

          Mary:“這個(gè)代碼很混亂。我甚至都不想看到它。為什么不讓 Mark 來(lái)做呢?”

          Jeff:“因?yàn)?Mark 不編寫(xiě)測(cè)試,剛被我解雇了。我需要您測(cè)試這個(gè)代碼并作一些改動(dòng)。告訴我您需要多長(zhǎng)時(shí)間。”

          Mary:“我至少需要兩天編寫(xiě)測(cè)試,然后我會(huì)重構(gòu)這個(gè)代碼,增加新的功能。我想總共需要四天吧。”

          正如他們所說(shuō)的,知識(shí)的力量是強(qiáng)大的。開(kāi)發(fā)人員可以在試圖修改代碼之前 使用覆蓋報(bào)告來(lái)檢查代碼質(zhì)量。同樣,管理人員可以使用覆蓋數(shù)據(jù)更好地估計(jì)開(kāi)發(fā)人員實(shí)際所需的時(shí)間。

          2. 評(píng)估代碼質(zhì)量

          開(kāi)發(fā)人員的測(cè)試可以降低代碼中存在缺陷的風(fēng)險(xiǎn),因此現(xiàn)在很多開(kāi)發(fā)團(tuán)隊(duì)在新開(kāi)發(fā)和更改代碼的同時(shí)需要編寫(xiě)單元測(cè)試。然而正如前面所提到的 Mark 一樣,并不總是在編碼的同時(shí)進(jìn)行單元測(cè)試,因而會(huì)導(dǎo)致低質(zhì)量代碼的出現(xiàn)。

          監(jiān)控覆蓋報(bào)告可以幫助開(kāi)發(fā)團(tuán)隊(duì)迅速找出不斷增長(zhǎng)的沒(méi)有 相應(yīng)測(cè)試的代碼。例如,在一周開(kāi)始時(shí)運(yùn)行覆蓋報(bào)告,顯示項(xiàng)目中一個(gè)關(guān)鍵的軟件包的覆蓋率是 70%。如果幾天后,覆蓋率下降到了 60%,那么您可以推斷:

          • 軟件包的代碼行增加了,但是沒(méi)有為新代碼編寫(xiě)相應(yīng)的測(cè)試(或者是新增加的測(cè)試不能有效地覆蓋新代碼)。

          • 刪除了測(cè)試案例。

          • 上述兩種情況都發(fā)生了。

          能夠監(jiān)控事情的發(fā)展,無(wú)疑是件好事。定期地查閱報(bào)告使得設(shè)定目標(biāo)(例如獲得覆蓋率、維護(hù)代碼行的測(cè)試案例的比例等)并監(jiān)控事情的發(fā)展變得更為容易。如果您發(fā)現(xiàn)測(cè)試沒(méi)有如期編寫(xiě),您可以提前采取一些行動(dòng),例如對(duì)開(kāi)發(fā)人員進(jìn)行培訓(xùn)、指導(dǎo)或幫助。與其讓用戶 “在使用中” 發(fā)現(xiàn)程序缺陷(這些缺陷本應(yīng)該在幾個(gè)月前通過(guò)簡(jiǎn)單的測(cè)試暴露出來(lái)),或者等到管理人員發(fā)現(xiàn)沒(méi)有編寫(xiě)單元測(cè)試時(shí)再感到驚訝(和憤怒),還不如采取一些預(yù)防性的措施。

          使用覆蓋報(bào)告來(lái)確保正確的測(cè)試是一項(xiàng)偉大的實(shí)踐。關(guān)鍵是要訓(xùn)練有素地完成這項(xiàng)工作。例如,使每晚生成并查閱覆蓋報(bào)告成為連續(xù)累計(jì) 過(guò)程的一部分。

          3. 評(píng)定功能測(cè)試

          假設(shè)覆蓋報(bào)告在指出沒(méi)有經(jīng)過(guò) 足夠測(cè)試的代碼部分方面非常有效,那么質(zhì)量保證人員可以使用這些數(shù)據(jù)來(lái)評(píng)定與功能測(cè)試有關(guān)的關(guān)注區(qū)域。讓我們回到 “飲水機(jī)” 團(tuán)隊(duì)來(lái)看看 QA 的負(fù)責(zé)人 Drew 是如何評(píng)價(jià) Joe 的代碼的:

          Drew 對(duì) Jeff 說(shuō):“我們?yōu)橄乱粋€(gè)版本編寫(xiě)了測(cè)試案例,我們注意到很多代碼沒(méi)有被覆蓋。那好像是與股票交易有關(guān)的代碼。”

          Jeff:“哦,我們?cè)谶@個(gè)領(lǐng)域有好些問(wèn)題。如果我是一個(gè)賭徒的話,我會(huì)對(duì)這個(gè)功能區(qū)域給予特別的關(guān)注。Mary 正在對(duì)這個(gè)應(yīng)用程序做一些其他的修改 —— 她在編寫(xiě)單元測(cè)試方面做得很好,但是這個(gè)代碼也太差了點(diǎn)。”

          Drew:“是的,我正在確定工作的資源和級(jí)別,看上去我沒(méi)必要那么擔(dān)心了,我估計(jì)我們的團(tuán)隊(duì)會(huì)對(duì)股票交易模塊引起足夠的關(guān)注。”

          知識(shí)再次顯示了其強(qiáng)大的力量。與其他軟件生命周期中的風(fēng)險(xiǎn)承擔(dān)者(例如 QA)配合,您可以利用覆蓋報(bào)告所提供的信息來(lái)降低風(fēng)險(xiǎn)。在上面的場(chǎng)景中,也許 Jeff 可以為 Drew 的團(tuán)隊(duì)提供一個(gè)早期的不包含 Mary 的所有修改的版本。不過(guò)無(wú)論如何,Drew 的團(tuán)隊(duì)都應(yīng)該關(guān)注應(yīng)用程序的股票交易方面,與其他具有相應(yīng)單元測(cè)試的代碼相比,這個(gè)地方似乎存在更大的缺陷風(fēng)險(xiǎn)。





          回頁(yè)首


          測(cè)試有什么好處

          對(duì)單元測(cè)試范例而言,測(cè)試覆蓋度量工具是一個(gè)有點(diǎn)奇怪的組成部分。對(duì)于一個(gè)已存在的有益的過(guò)程,覆蓋度量可以增加其深度和精度。然而,您應(yīng)該仔細(xì)地閱讀代碼覆蓋報(bào)告。單獨(dú)的高覆蓋率并不能確保代碼的質(zhì)量。對(duì)于減少缺陷,代碼的高覆蓋并不是必要條件,盡管高覆蓋的代碼的確更少 有缺陷。

          測(cè)試覆蓋度量的竅門(mén)是使用覆蓋報(bào)告找出未經(jīng) 測(cè)試的代碼,分別在微觀和宏觀兩個(gè)級(jí)別。通過(guò)從頂層開(kāi)始分析您的代碼庫(kù),以及分析單個(gè)類的覆蓋,可以促進(jìn)深入的覆蓋測(cè)試。一旦您能夠綜合這些原則,您和您的組織就可以在真正需要的地方使用覆蓋度量工具,例如估計(jì)一個(gè)項(xiàng)目所需的時(shí)間,持續(xù)監(jiān)控代碼質(zhì)量以及促進(jìn)與 QA 的協(xié)作。





          回頁(yè)首


          參考資料





          回頁(yè)首


          關(guān)于作者

          Andrew Glover 是 Vanward Technologies 公司的 CTO,該公司位于華盛頓特區(qū)大都會(huì)地區(qū),公司的專業(yè)領(lǐng)域是自動(dòng)測(cè)試框架的構(gòu)造,自動(dòng)測(cè)試框架可以降低軟件 bug 數(shù)量,減少集成和測(cè)試的時(shí)間,提高整體的代碼穩(wěn)定性。他還是 Java Testing Patterns(Wiley,2004 年 9 月)的合著者

          主站蜘蛛池模板: 兴安盟| 祥云县| 天祝| 宿州市| 东阳市| 丽江市| 望江县| 晴隆县| 阿拉善盟| 祁阳县| 岑巩县| 富宁县| 清原| 且末县| 浪卡子县| 尖扎县| 岳池县| 阳朔县| 合水县| 云梦县| 夏津县| 建湖县| 石家庄市| 西青区| 临武县| 故城县| 灌阳县| 台东县| 尉氏县| 盐边县| 开化县| 潼南县| 北宁市| 鹤山市| 巨鹿县| 辽阳市| 额尔古纳市| 岗巴县| 翁牛特旗| 梨树县| 连云港市|