posts - 11, comments - 3, trackbacks - 0, articles - 0
            BlogJava :: 首頁 :: 新隨筆 :: 聯系 :: 聚合  :: 管理

          (轉貼)用 Jester 對測試進行測試

          Posted on 2006-07-19 15:33 eddy liao 閱讀(388) 評論(0)  編輯  收藏 所屬分類: 軟件質量
          http://www-128.ibm.com/developerworks/cn/java/j-jester/

          用 Jester 對測試進行測試

          測試套件有缺陷,這不是玩笑

          developerWorks
          文檔選項
          將此頁作為電子郵件發送

          將此頁作為電子郵件發送

          未顯示需要 JavaScript 的文檔選項


          最新推薦

          Java 應用開發源動力 - 下載免費軟件,快速啟動開發


          級別: 初級

          Elliotte Rusty Harold , 副教授, Polytechnic University

          2005 年 6 月 02 日

          全面的單元測試套件對健壯的程序是必不可少的。但是如何才能保證測試套件測試了應當測試的每件事呢?Ivan Moore 的 JUnit 測試的測試器 Jester,擅長發現測試套件的問題,并提供對代碼基本結構的深入觀察。Elliotte Rusty Harold 介紹了 Jester 并展示如何使用它才能得到最佳結果。

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

          代碼覆蓋

          對代碼進行測試的下一步是用 代碼覆蓋 工具對測試進行度量。代碼覆蓋是一種查看一套測試覆蓋了多少代碼的方法。信心的獲得,不僅需要知道測試了程序整體,還要知道每個方法在全部可能情況下都得到測試。傳統情況下,這類度量的執行方法是在測試執行時對測試進行監視,可以通過 Java 虛擬機調試接口(JVMDI)或 Java 虛擬機工具接口 (JVMTI)進行,或者直接處理字節碼。一次都沒有執行過的語句是測試不到的。

          Clover 和 EMMA(參閱 參考資料) 這類工具采用的這種方法對于發現測試不到的語句很有價值 —— 但是還不夠。知道測試套件沒有執行某個語句,可以證明該語句沒測試到。但是,反過來不成立。如果執行了某一行代碼,并不一定代表它得到測試。完全有可能存在這樣的情況:測試并沒有檢查代碼行是否生成正確結果。

          當然,沒有人會編寫測試套件對每個語句的結果都進行驗證。在眾多的問題當中,這個問題可能會破壞封裝。您可能認為,針對特定輸入,只有方法中的每一行都操作正確,方法才會生成預期結果。但是這個假設并不合理。例如,如果沒有測試到所有可能輸入,也就沒有測試到為處理邊際情況而設計的代碼,這時會如何呢?有可能還會測試到每行代碼,但有可能遺漏真正的 bug。

          并不簡單

          Jester 的方法并不簡單。這個工具有可能會報告大量假陽性。例如,它可能把 System.out.println("Copyright 2005 Elliotte Rusty Harold") 語句改成 System.out.println("Copyright 3005 Elliotte Rusty Harold") ,然后報告沒有破壞發生。但是,假陽性一般很容易過濾出來。另外,通常也有合適的理由懷疑像這個示例一樣的情況是否真的是假陽性。例如,對于版權日期 3005 是否是測試套件應當通知的 bug,有人可能會有異議。





          回頁首


          Jester 簡介

          這正是 Jester 發揮作用的地方。與 Clover 這類傳統的代碼覆蓋工具不同,Jester 不去查看報告了哪行代碼。相反,Jester 會修改源代碼、重新編譯源代碼,然后運行測試套件,查看是否有什么事出錯。例如,它會把 1 改成 2,或者把 if (x > y) 改成 if (false)。如果測試套件的關注不夠,沒有注意到修改,那么就說明遺漏了某項測試。

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

          入門

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

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

          												
          														$ 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
          												
          										

          在運行 Jester 之前,還需要清楚針對測試套件的一項附加限制。除非測試失敗,否則不能打印有關 System.err 的任何內容。Jester 要通過檢查打印的內容來判斷測試是否成功,所以對 System.err 的程序輸出會把 Jester 弄混。

          測試套件運行無誤之后,請做一份源代碼樹的拷貝。記住,Jester 要向代碼故意加入 bug,所以您可不要冒險在出現問題的情況下遺漏一個 bug。(如果您在使用源代碼控制,那么這不會是個大問題。如果沒有,請暫停閱讀本文,立即把代碼簽入 CVS 或 Subversion 倉庫。)

          運行 Jester

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

          												
          														$ 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 運行很慢,即使檢測一個文件也是如此。它顯示一個進度對話框,如圖 1 所示,并在 System.out 上打印輸出,讓您知道它在做的工作,并向您保證它并沒有完全掛起。


          圖 1. Jester 進度

          如果在第一次運行若干分鐘(或者時間足夠運行完整的測試套件,甚至更長)之后,什么輸出也沒有看到,那么 Jester 可能 確實 掛起了,這很可能是因為類路徑的問題。如果每件事都進行順利,那么應當看到像清單 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 沒有得到很好的測試。Jester 對類進行了 11 項修改,而只有一項造成測試失敗。有些修改是假陽性,但是 11 處修改肯定不應當只報告 1 處。

          下一步是在不破壞測試套件的情況下查看 Jester 改變的代碼,看看是否需要為它編寫測試。Jester 在 GUI 中顯示它進行的修改,如 圖 1 所示(它不能在無人控制的情況下運行,這有點煩人),在控制臺上打印輸出,如 清單 1 所示,并生成 XML 文件,文件中是沒有產生影響的修改列表,如清單 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 的行號報告通常不是個好方法,所以最好是在控制臺輸出中查找修改的代碼。下面是 清單 1 的報告中的修改:

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

          在這個方法中,這個修改是在類的結束處:

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

          對測試套件迅速查找之后發現,實際上沒有測試調用 selectSingleNodeForContext。所以下一步就是為這個方法編寫一個測試。這個方法是 protected 的方法,所以測試不能直接調用它。有時需要編寫一個子類(通常作為內部類)來測試 protected 的方法。但是在這個例子中,稍做一點檢查就很快發現這個方法由同一個類中的兩個 public 方法(stringValuenumberValue)直接調用。所以也可以用這兩個方法來測試它:

          												
          														    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);
                  
              }
          												
          										

          最后一步是運行測試用例,確定它通過。下面是結果:

          												
          														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 捕捉到一個 bug!方法沒有像預期的那樣工作。更有趣的是,對 bug 的調查揭示出潛在的設計缺陷。BaseXPath 類可能更適合作為抽象類而不是具體類。我發誓,我并不是特意挑選這個示例來公開這個 bug。我從 BaseXPath 開始只是因為它是頂級 org.jaxen 包的第一個類,而且我選擇 selectSingleNodeForContext 作為所測試的方法也只是因為它是 Jester 報告的最后一個錯誤。我真的認為這個方法沒有什么問題,但是我錯了。如果某些事沒有經過測試,那么就應當假設它是有問題的。Jester 會告訴您出了什么問題。

          下一步顯而易見:修復 bug。(請確保同時對 Jester 正在處理的源樹拷貝和實際樹中的 bug 進行了修復。)然后,迭代 —— 針對這個類重新運行 Jester,直到任何修改都不能通過,或者可以通過的修改都是不相關的。在我為這個 bug 添加測試(并修復)之后,Jester 就報告 11 個修改中只有 8 個沒有檢測到,如 清單 2 所示。這在調試中是經常出現的事:修復了一個問題就修復(或者暴露了)另外幾個。





          回頁首


          Jester 的性能

          因為 Jester 重新編譯代碼基,而且要為自己做的每個修改都重新運行測試套件,所以它的運行要比 Clover 這樣的傳統工具慢得多。因此,對性能加以關注是很重要的。可以用許多技術加快 Jester 的運行。

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

          第二,剖析和優化測試套件。一般情況下,人們對單元測試運行的速度沒太注意,但是如果乘上 Jester 上千次執行測試套件的次數,那么任何節約都會非常顯著。具體來說,要在測試套件中查找在正常代碼中不會出現的問題。JUnit 會重新初始化每個執行方法的全部字段,所以如果不是測試類的每個方法都用的字段,那么把測試數據從字段中拿出來放在本地變量中,可以顯著提高速度。如果形成的代碼副本不合您的風格,請嘗試把測試套件分成更小、更模塊化的類,以便所有的初始數據可以在全部測試方法之間共享。

          第三,重新組織測試套件的 suite 方法,以便最脆弱的測試(修改之后最有可能出錯的)在不太脆弱的測試之前運行。只要 Jester 發現一個測試失敗,就會終止運行,所以盡早失敗可以短路大量耗時的額外測試。

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

          最后,也是最重要的,就是不要一次測試整個代碼基。每次把測試限制在一個類上,而且只運行能夠暴露這個類的覆蓋不足的測試。可能需要更長時間來測試每個類,但是用這種方法,幾乎可以立即填補不足、修復 bug,而不必為 Jester 的一次運行完成等上好幾天。





          回頁首


          結束語

          Jester 是聰明的程序員的工具包中一個重要的附加。它可以發現其他工具不能發現的代碼覆蓋不足,這會直接變成發現和修復 bug。使用 Jester 對代碼基進行測試,可以制造出更強壯的軟件。





          回頁首


          參考資料





          回頁首


          關于作者

          Elliotte Rusty Harold 來自新奧爾良,現在他還定期返回新奧爾良研究一盆秋葵。但是,他和妻子 Beth 及他們的貓咪 Charm(以夸克命名)和 Marjorie(以他的岳母為名),定居在布魯克林附近的 Prospect Heights。他是 Polytechnic 大學計算機科學的副教授,他在該校講授 Java 和面向對象編程。他的 Web 站點 Cafe au Lait 已經成為 Internet 上最流行的獨立 Java 站點之一,他的分站點 Cafe con Leche 已經成為最流行的 XML 站點之一。他的書包括 Effective XMLProcessing XML with JavaJava Network ProgrammingThe XML 1.1 Bible。他目前在開發處理 XML 的 XOM API 和 XQuisitor GUI 查詢工具。

          主站蜘蛛池模板: 安多县| 乐山市| 老河口市| 合山市| 青岛市| 同心县| 泸西县| 保亭| 朔州市| 永康市| 晋宁县| 修文县| 平昌县| 黄梅县| 临海市| 大宁县| 甘德县| 屏边| 兴海县| 商水县| 永康市| 蒙山县| 济阳县| 江华| 涟源市| 阿拉善盟| 大英县| 岱山县| 郧西县| 四子王旗| 湛江市| 鄂温| 武清区| 姚安县| 循化| 依安县| 宾阳县| 普安县| 惠东县| 宁南县| 江北区|