posts - 11, comments - 3, trackbacks - 0, articles - 0
            BlogJava :: 首頁(yè) :: 新隨筆 :: 聯(lián)系 :: 聚合  :: 管理
          http://www-128.ibm.com/developerworks/cn/java/j-jester/

          用 Jester 對(duì)測(cè)試進(jìn)行測(cè)試

          測(cè)試套件有缺陷,這不是玩笑

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

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

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


          最新推薦

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


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

          Elliotte Rusty Harold , 副教授, Polytechnic University

          2005 年 6 月 02 日

          全面的單元測(cè)試套件對(duì)健壯的程序是必不可少的。但是如何才能保證測(cè)試套件測(cè)試了應(yīng)當(dāng)測(cè)試的每件事呢?Ivan Moore 的 JUnit 測(cè)試的測(cè)試器 Jester,擅長(zhǎng)發(fā)現(xiàn)測(cè)試套件的問(wèn)題,并提供對(duì)代碼基本結(jié)構(gòu)的深入觀察。Elliotte Rusty Harold 介紹了 Jester 并展示如何使用它才能得到最佳結(jié)果。

          測(cè)試先行的開發(fā)是極限編程(XP)中爭(zhēng)議最少、采用最廣泛的部分。到目前為止,大多數(shù)專業(yè) Java 程序員都可能捕捉過(guò)測(cè)試 bug。(請(qǐng)參閱 參考資料 獲得有關(guān)“被測(cè)試傳染”的更多信息。) JUnit 是 Java 社區(qū)事實(shí)上的標(biāo)準(zhǔn)測(cè)試框架,沒(méi)有經(jīng)過(guò)全面的 JUnit 測(cè)試套件測(cè)試過(guò)的系統(tǒng)是不完整的。如果您的項(xiàng)目有全面的測(cè)試套件,那么恭喜您:您將制作出質(zhì)量良好的、有利于工作的軟件。但是大多數(shù)代碼基礎(chǔ)相當(dāng)復(fù)雜。您能確定每個(gè)方法都被測(cè)試到、每個(gè)分支都進(jìn)入過(guò)么?如果不能,那么當(dāng)這些方法和分支在生產(chǎn)中執(zhí)行的時(shí)候,應(yīng)用程序會(huì)如何表現(xiàn)呢?

          代碼覆蓋

          對(duì)代碼進(jìn)行測(cè)試的下一步是用 代碼覆蓋 工具對(duì)測(cè)試進(jìn)行度量。代碼覆蓋是一種查看一套測(cè)試覆蓋了多少代碼的方法。信心的獲得,不僅需要知道測(cè)試了程序整體,還要知道每個(gè)方法在全部可能情況下都得到測(cè)試。傳統(tǒng)情況下,這類度量的執(zhí)行方法是在測(cè)試執(zhí)行時(shí)對(duì)測(cè)試進(jìn)行監(jiān)視,可以通過(guò) Java 虛擬機(jī)調(diào)試接口(JVMDI)或 Java 虛擬機(jī)工具接口 (JVMTI)進(jìn)行,或者直接處理字節(jié)碼。一次都沒(méi)有執(zhí)行過(guò)的語(yǔ)句是測(cè)試不到的。

          Clover 和 EMMA(參閱 參考資料) 這類工具采用的這種方法對(duì)于發(fā)現(xiàn)測(cè)試不到的語(yǔ)句很有價(jià)值 —— 但是還不夠。知道測(cè)試套件沒(méi)有執(zhí)行某個(gè)語(yǔ)句,可以證明該語(yǔ)句沒(méi)測(cè)試到。但是,反過(guò)來(lái)不成立。如果執(zhí)行了某一行代碼,并不一定代表它得到測(cè)試。完全有可能存在這樣的情況:測(cè)試并沒(méi)有檢查代碼行是否生成正確結(jié)果。

          當(dāng)然,沒(méi)有人會(huì)編寫測(cè)試套件對(duì)每個(gè)語(yǔ)句的結(jié)果都進(jìn)行驗(yàn)證。在眾多的問(wèn)題當(dāng)中,這個(gè)問(wèn)題可能會(huì)破壞封裝。您可能認(rèn)為,針對(duì)特定輸入,只有方法中的每一行都操作正確,方法才會(huì)生成預(yù)期結(jié)果。但是這個(gè)假設(shè)并不合理。例如,如果沒(méi)有測(cè)試到所有可能輸入,也就沒(méi)有測(cè)試到為處理邊際情況而設(shè)計(jì)的代碼,這時(shí)會(huì)如何呢?有可能還會(huì)測(cè)試到每行代碼,但有可能遺漏真正的 bug。

          并不簡(jiǎn)單

          Jester 的方法并不簡(jiǎn)單。這個(gè)工具有可能會(huì)報(bào)告大量假陽(yáng)性。例如,它可能把 System.out.println("Copyright 2005 Elliotte Rusty Harold") 語(yǔ)句改成 System.out.println("Copyright 3005 Elliotte Rusty Harold") ,然后報(bào)告沒(méi)有破壞發(fā)生。但是,假陽(yáng)性一般很容易過(guò)濾出來(lái)。另外,通常也有合適的理由懷疑像這個(gè)示例一樣的情況是否真的是假陽(yáng)性。例如,對(duì)于版權(quán)日期 3005 是否是測(cè)試套件應(yīng)當(dāng)通知的 bug,有人可能會(huì)有異議。





          回頁(yè)首


          Jester 簡(jiǎn)介

          這正是 Jester 發(fā)揮作用的地方。與 Clover 這類傳統(tǒng)的代碼覆蓋工具不同,Jester 不去查看報(bào)告了哪行代碼。相反,Jester 會(huì)修改源代碼、重新編譯源代碼,然后運(yùn)行測(cè)試套件,查看是否有什么事出錯(cuò)。例如,它會(huì)把 1 改成 2,或者把 if (x > y) 改成 if (false)。如果測(cè)試套件的關(guān)注不夠,沒(méi)有注意到修改,那么就說(shuō)明遺漏了某項(xiàng)測(cè)試。

          我將通過(guò)在開源的 Jaxen XPath 工具(參閱 參考資料)上應(yīng)用 Jester 而對(duì)它進(jìn)行演示。Jaxen 有一個(gè)基于 JUnit 的測(cè)試套件,而且這個(gè)套件的代碼覆蓋并不完善。

          入門

          在運(yùn)行 Jester 之前,所有對(duì)沒(méi)有修改的源代碼的單元測(cè)試都必須測(cè)試通過(guò)。如果不是這樣,那么 Jester 就無(wú)法知道是不是它的修改造成了破壞。(為了演示,我不得不修復(fù)一個(gè) bug,我過(guò)去為它編寫了測(cè)試用例,但是沒(méi)有跟蹤修復(fù)它。)

          Jester 與 IDE 的集成不是特別好(或者根本不好),所以要讓測(cè)試通過(guò),重要的是正確設(shè)置 CLASSPATH 和目錄。運(yùn)行測(cè)試套件所需要的命令行對(duì)于每個(gè)項(xiàng)目都是不同的。因?yàn)?Jaxen 測(cè)試使用指向特定測(cè)試文件的相對(duì) URL ,所以它的測(cè)試必須在 jaxen 目錄中運(yùn)行。下面是我最后運(yùn)行 Jaxen 測(cè)試的方式:

          												
          														$ java -classpath ../jester136/jester.jar:target/lib/junit-3.8.1.jar
          :target/lib/dom4j-core-1.4-dev-8.jar:target/lib/jdom-b10.jar
          :target/lib/xom-1.0d21.jar:target/test-classes:target/classes 
          junit.textui.TestRunner org.jaxen.JaxenTests
          												
          										

          在運(yùn)行 Jester 之前,還需要清楚針對(duì)測(cè)試套件的一項(xiàng)附加限制。除非測(cè)試失敗,否則不能打印有關(guān) System.err 的任何內(nèi)容。Jester 要通過(guò)檢查打印的內(nèi)容來(lái)判斷測(cè)試是否成功,所以對(duì) System.err 的程序輸出會(huì)把 Jester 弄混。

          測(cè)試套件運(yùn)行無(wú)誤之后,請(qǐng)做一份源代碼樹的拷貝。記住,Jester 要向代碼故意加入 bug,所以您可不要冒險(xiǎn)在出現(xiàn)問(wèn)題的情況下遺漏一個(gè) bug。(如果您在使用源代碼控制,那么這不會(huì)是個(gè)大問(wèn)題。如果沒(méi)有,請(qǐng)暫停閱讀本文,立即把代碼簽入 CVS 或 Subversion 倉(cāng)庫(kù)。)

          運(yùn)行 Jester

          要運(yùn)行 Jester,在路徑中必須同時(shí)擁有 jester.jar 和 junit.jar(JUnit 沒(méi)有和 Jester 綁在一起。需要分別下載)。Jester 在類路徑中查找它的配置文件,所以必須還要把 Jester 的主目錄放在類路徑中。當(dāng)然,還需要添加所測(cè)試的應(yīng)用程序需要的其他 JAR。主類是 jester.TestTester。傳遞給這個(gè)程序的參數(shù)是測(cè)試應(yīng)用程序的測(cè)試套件名稱。(我不得不為 Jaxen 編寫一個(gè)主類,因?yàn)樗鼪](méi)有包含一個(gè)可以運(yùn)行它的全部測(cè)試的類。)如果把全部必要的 JAR 文件和目錄都添加到 CLASSPATH 環(huán)境變量,而不是把它們添加到 jre/lib/ext 或者用 -classpath 引用它們,那么 Jester 工作起來(lái)會(huì)更加穩(wěn)定。下面是我針對(duì) Jaxen 運(yùn)行初始測(cè)試的方式:

          												
          														$ export CLASSPATH=src2/java/main:../jester136/jester.jar:../jester136
          :target/lib/junit-3.8.1.jar:target/lib/dom4j-core-1.4-dev-8.jar
          :target/lib/jdom-b10.jar:target/lib/jdom-b10.jar:target/lib/xom-1.0d21.jar
          :target/test-classes:target/classes
          $ java  jester.TestTester org.jaxen.JaxenTests src2/java/main 
          												
          										

          Jester 運(yùn)行很慢,即使檢測(cè)一個(gè)文件也是如此。它顯示一個(gè)進(jìn)度對(duì)話框,如圖 1 所示,并在 System.out 上打印輸出,讓您知道它在做的工作,并向您保證它并沒(méi)有完全掛起。


          圖 1. Jester 進(jìn)度

          如果在第一次運(yùn)行若干分鐘(或者時(shí)間足夠運(yùn)行完整的測(cè)試套件,甚至更長(zhǎng))之后,什么輸出也沒(méi)有看到,那么 Jester 可能 確實(shí) 掛起了,這很可能是因?yàn)轭惵窂降膯?wèn)題。如果每件事都進(jìn)行順利,那么應(yīng)當(dāng)看到像清單 1 所示的輸出:


          清單 1. Jester 輸出
          												
          																		Use classpath: src2/java/main:../jester136/jester.jar
          :../jester136:target/lib/junit-3.8.1.jar:target/lib/dom4j-core-1.4-dev-8.jar
          :target/lib/jdom-b10.jar:target/lib/jdom-b10.jar:target/lib/xom-1.0d21.jar
          :target/test-classes:target/classes
          ...
          src2/java/main/org/jaxen/BaseXPath.java 
           - changed source on line 192 (char index=7757) from 1 to 2
                       answer.size() == ?1 )
                  {
                      Object first = answ
          
          src2/java/main/org/jaxen/BaseXPath.java 
           - changed source on line 691 (char index=24848) from 0 to 1
          
          
                  return results.get( ?0 );
              }
          }
          
          lots more output...
          src2/java/main/org/jaxen/BaseXPath.java 
           - changed source on line 691 (char index=24848) from 0 to 1
          
          
                  return results.get( ?0 );
              }
          }
          
          
          
          10 mutations survived out of 11 changes. Score = 10
          took 1 minutes
          												
          										

          從清單 1 中可以看到,BaseXPath 沒(méi)有得到很好的測(cè)試。Jester 對(duì)類進(jìn)行了 11 項(xiàng)修改,而只有一項(xiàng)造成測(cè)試失敗。有些修改是假陽(yáng)性,但是 11 處修改肯定不應(yīng)當(dāng)只報(bào)告 1 處。

          下一步是在不破壞測(cè)試套件的情況下查看 Jester 改變的代碼,看看是否需要為它編寫測(cè)試。Jester 在 GUI 中顯示它進(jìn)行的修改,如 圖 1 所示(它不能在無(wú)人控制的情況下運(yùn)行,這有點(diǎn)煩人),在控制臺(tái)上打印輸出,如 清單 1 所示,并生成 XML 文件,文件中是沒(méi)有產(chǎn)生影響的修改列表,如清單 2 所示:


          清單 2. Jester XML 輸出
          												
          																		<JesterReport>
          <JestedFile fileName="src2/java/main/org/jaxen/BaseXPath.java" absolutePathFileName=
          "/Users/elharo/Documents/articles/jester/jaxen/src2/java/main/org/jaxen/BaseXPath.java" 
          numberOfChangesThatDidNotCauseTestsToFail="8" numberOfChanges="11" score="28">
          <ChangeThatDidNotCauseTestsToFail index="7691" from="if (" to="if (true ||"/>
          <ChangeThatDidNotCauseTestsToFail index="7691" from="if (" to="if (false &&"/>
          <ChangeThatDidNotCauseTestsToFail index="7703" from="!=" to="=="/>
          <ChangeThatDidNotCauseTestsToFail index="7754" from="==" to="!="/>
          <ChangeThatDidNotCauseTestsToFail index="7757" from="1" to="2"/>
          <ChangeThatDidNotCauseTestsToFail index="7826" from="if (" to="if (true ||"/>
          <ChangeThatDidNotCauseTestsToFail index="7826" from="if (" to="if (false &&"/>
          <ChangeThatDidNotCauseTestsToFail index="24749" from="if (" to="if (false &&"/>
          </JestedFile></JesterReport>
          												
          										

          Jester 的行號(hào)報(bào)告通常不是個(gè)好方法,所以最好是在控制臺(tái)輸出中查找修改的代碼。下面是 清單 1 的報(bào)告中的修改:

          												
          														src2/java/main/org/jaxen/BaseXPath.java 
           - changed source on line 691 (char index=24848) from 0 to 1
          
          
                  return results.get( ?0 );
              }
          }
          												
          										

          在這個(gè)方法中,這個(gè)修改是在類的結(jié)束處:

          												
          														protected Object selectSingleNodeForContext(Context context) throws JaxenException 
          {
            List results = selectNodesForContext( context );
          
            if ( results.isEmpty() )
            {
              return null;
            }
          
                  return results.get( 0 );
          }
          												
          										

          對(duì)測(cè)試套件迅速查找之后發(fā)現(xiàn),實(shí)際上沒(méi)有測(cè)試調(diào)用 selectSingleNodeForContext。所以下一步就是為這個(gè)方法編寫一個(gè)測(cè)試。這個(gè)方法是 protected 的方法,所以測(cè)試不能直接調(diào)用它。有時(shí)需要編寫一個(gè)子類(通常作為內(nèi)部類)來(lái)測(cè)試 protected 的方法。但是在這個(gè)例子中,稍做一點(diǎn)檢查就很快發(fā)現(xiàn)這個(gè)方法由同一個(gè)類中的兩個(gè) public 方法(stringValuenumberValue)直接調(diào)用。所以也可以用這兩個(gè)方法來(lái)測(cè)試它:

          												
          														    public void testSelectSingleNodeForContext() throws JaxenException {
                  
                  BaseXPath xpath = new BaseXPath("1 + 2");
                  
                  String stringValue = xpath.stringValueOf(xpath);
                  assertEquals("3", stringValue);
                  
                  Number numberValue = xpath.numberValueOf(xpath);
                  assertEquals(3, numberValue.doubleValue(), 0.00001);
                  
              }
          												
          										

          最后一步是運(yùn)行測(cè)試用例,確定它通過(guò)。下面是結(jié)果:

          												
          														java.lang.NullPointerException
          	at org.jaxen.function.StringFunction.evaluate(StringFunction.java:121)
          	at org.jaxen.BaseXPath.stringValueOf(BaseXPath.java:295)
          	at org.jaxen.BaseXPathTest.testSelectSingleNodeForContext(BaseXPathTest.java:23)
          												
          										

          Jester 捕捉到一個(gè) bug!方法沒(méi)有像預(yù)期的那樣工作。更有趣的是,對(duì) bug 的調(diào)查揭示出潛在的設(shè)計(jì)缺陷。BaseXPath 類可能更適合作為抽象類而不是具體類。我發(fā)誓,我并不是特意挑選這個(gè)示例來(lái)公開這個(gè) bug。我從 BaseXPath 開始只是因?yàn)樗琼敿?jí) org.jaxen 包的第一個(gè)類,而且我選擇 selectSingleNodeForContext 作為所測(cè)試的方法也只是因?yàn)樗?Jester 報(bào)告的最后一個(gè)錯(cuò)誤。我真的認(rèn)為這個(gè)方法沒(méi)有什么問(wèn)題,但是我錯(cuò)了。如果某些事沒(méi)有經(jīng)過(guò)測(cè)試,那么就應(yīng)當(dāng)假設(shè)它是有問(wèn)題的。Jester 會(huì)告訴您出了什么問(wèn)題。

          下一步顯而易見(jiàn):修復(fù) bug。(請(qǐng)確保同時(shí)對(duì) Jester 正在處理的源樹拷貝和實(shí)際樹中的 bug 進(jìn)行了修復(fù)。)然后,迭代 —— 針對(duì)這個(gè)類重新運(yùn)行 Jester,直到任何修改都不能通過(guò),或者可以通過(guò)的修改都是不相關(guān)的。在我為這個(gè) bug 添加測(cè)試(并修復(fù))之后,Jester 就報(bào)告 11 個(gè)修改中只有 8 個(gè)沒(méi)有檢測(cè)到,如 清單 2 所示。這在調(diào)試中是經(jīng)常出現(xiàn)的事:修復(fù)了一個(gè)問(wèn)題就修復(fù)(或者暴露了)另外幾個(gè)。





          回頁(yè)首


          Jester 的性能

          因?yàn)?Jester 重新編譯代碼基,而且要為自己做的每個(gè)修改都重新運(yùn)行測(cè)試套件,所以它的運(yùn)行要比 Clover 這樣的傳統(tǒng)工具慢得多。因此,對(duì)性能加以關(guān)注是很重要的。可以用許多技術(shù)加快 Jester 的運(yùn)行。

          首先,如果編譯在 Jester 執(zhí)行時(shí)間中占了顯著部分,那么請(qǐng)嘗試使用一個(gè)更快的編譯器。許多用戶都報(bào)告采用 Jikes 代替 Javac 后速度有顯著提高(參閱 參考資料)。可以在 Jester 主目錄中的 jester.cfg 文件中修改 Jester 使用的編譯命令。

          第二,剖析和優(yōu)化測(cè)試套件。一般情況下,人們對(duì)單元測(cè)試運(yùn)行的速度沒(méi)太注意,但是如果乘上 Jester 上千次執(zhí)行測(cè)試套件的次數(shù),那么任何節(jié)約都會(huì)非常顯著。具體來(lái)說(shuō),要在測(cè)試套件中查找在正常代碼中不會(huì)出現(xiàn)的問(wèn)題。JUnit 會(huì)重新初始化每個(gè)執(zhí)行方法的全部字段,所以如果不是測(cè)試類的每個(gè)方法都用的字段,那么把測(cè)試數(shù)據(jù)從字段中拿出來(lái)放在本地變量中,可以顯著提高速度。如果形成的代碼副本不合您的風(fēng)格,請(qǐng)嘗試把測(cè)試套件分成更小、更模塊化的類,以便所有的初始數(shù)據(jù)可以在全部測(cè)試方法之間共享。

          第三,重新組織測(cè)試套件的 suite 方法,以便最脆弱的測(cè)試(修改之后最有可能出錯(cuò)的)在不太脆弱的測(cè)試之前運(yùn)行。只要 Jester 發(fā)現(xiàn)一個(gè)測(cè)試失敗,就會(huì)終止運(yùn)行,所以盡早失敗可以短路大量耗時(shí)的額外測(cè)試。

          第四,出于相似的原因,當(dāng)測(cè)試失敗的機(jī)會(huì)差不多時(shí),把最快的測(cè)試放在第一位。按照大概的執(zhí)行時(shí)間給測(cè)試排序。只在內(nèi)存中執(zhí)行的測(cè)試在訪問(wèn)磁盤的測(cè)試之前,訪問(wèn)磁盤的測(cè)試在訪問(wèn) LAN 的測(cè)試之前,訪問(wèn) LAN 的測(cè)試在訪問(wèn) Internet 的測(cè)試之前。如果有些測(cè)試特別慢,試試去掉它們,即便這會(huì)增加假陽(yáng)性的數(shù)量。在 XOM (一個(gè)用 Java 語(yǔ)言處理 XML 的 API)的測(cè)試套件中,在 50 個(gè)測(cè)試類中,只有很少的幾個(gè)就占據(jù)了 90% 以上的執(zhí)行時(shí)間。在測(cè)試的時(shí)候清除這些類可以帶來(lái) 10 倍的性能提升。

          最后,也是最重要的,就是不要一次測(cè)試整個(gè)代碼基。每次把測(cè)試限制在一個(gè)類上,而且只運(yùn)行能夠暴露這個(gè)類的覆蓋不足的測(cè)試。可能需要更長(zhǎng)時(shí)間來(lái)測(cè)試每個(gè)類,但是用這種方法,幾乎可以立即填補(bǔ)不足、修復(fù) bug,而不必為 Jester 的一次運(yùn)行完成等上好幾天。





          回頁(yè)首


          結(jié)束語(yǔ)

          Jester 是聰明的程序員的工具包中一個(gè)重要的附加。它可以發(fā)現(xiàn)其他工具不能發(fā)現(xiàn)的代碼覆蓋不足,這會(huì)直接變成發(fā)現(xiàn)和修復(fù) bug。使用 Jester 對(duì)代碼基進(jìn)行測(cè)試,可以制造出更強(qiáng)壯的軟件。





          回頁(yè)首


          參考資料





          回頁(yè)首


          關(guān)于作者

          Elliotte Rusty Harold 來(lái)自新奧爾良,現(xiàn)在他還定期返回新奧爾良研究一盆秋葵。但是,他和妻子 Beth 及他們的貓咪 Charm(以夸克命名)和 Marjorie(以他的岳母為名),定居在布魯克林附近的 Prospect Heights。他是 Polytechnic 大學(xué)計(jì)算機(jī)科學(xué)的副教授,他在該校講授 Java 和面向?qū)ο缶幊獭K?Web 站點(diǎn) Cafe au Lait 已經(jīng)成為 Internet 上最流行的獨(dú)立 Java 站點(diǎn)之一,他的分站點(diǎn) Cafe con Leche 已經(jīng)成為最流行的 XML 站點(diǎn)之一。他的書包括 Effective XMLProcessing XML with JavaJava Network ProgrammingThe XML 1.1 Bible。他目前在開發(fā)處理 XML 的 XOM API 和 XQuisitor GUI 查詢工具。

          主站蜘蛛池模板: 宕昌县| 陕西省| 昌吉市| 壶关县| 花莲市| 德安县| 武义县| 锦屏县| 孝义市| 浦东新区| 张家界市| 钟山县| 崇州市| 梁平县| 雷波县| 黔江区| 德昌县| 宜城市| 阿坝县| 红原县| 三门峡市| 湘乡市| 林州市| 馆陶县| 浦江县| 疏附县| 平远县| 民和| 旺苍县| 竹北市| 建瓯市| 伊通| 洞口县| 吉木萨尔县| 阿拉善右旗| 聂拉木县| 鄂尔多斯市| 内江市| 望城县| 萨嘎县| 广南县|