隨筆-1  評論-0  文章-1  trackbacks-0

          [原文]http://www.ibm.com/developerworks/cn/java/j-cq01307/index.html

          開發(fā)人員測試的主要缺點是:絕大部分測試都是在理想的場景中進行的。在這些情況下并不會出現(xiàn)缺陷 —— 能導致出現(xiàn)問題的往往是那些邊界情況。

          什么是邊界情況呢?比方說,把 null 值傳入一個并未編寫如何處理 null 值的方法中,這就是一種邊界情況。大多數(shù)開發(fā)人員通常都不能成功測試這樣的場景,因為這沒多大意義。但不管有沒有意義,發(fā)生了這樣的情況,就會拋出一個 NullPointerException,然后整個程序就會崩潰。

          本月,我將為您推薦一種多層面的方法,來處理代碼中那些不易預料的缺陷。嘗試為應用程序整合進防御性編程、契約式設計和一種叫做 OVal 的易用的通用驗證框架。

          將敵人暴露出來

          清單 1 中的代碼為給定的 Class 對象(省去了 java.lang.Object,因為所有對象都最終由它擴展)構建一個類層次。但如果仔細看的話,您會注意到一個有待發(fā)現(xiàn)的潛在缺陷,即該方法對對象值所做的假設。


          清單 1. 不檢驗 null 的方法
                      public static Hierarchy buildHierarchy(Class clzz){
                      Hierarchy hier = new Hierarchy();
                      hier.setBaseClass(clzz);
                      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;
                      }
                      }
                      

          剛編好這個方法,我還沒注意到這個缺陷,但由于我狂熱地崇拜開發(fā)人員測試,于是我編寫了一個使用 TestNG 的常規(guī)測試。而且,我還利用了 TestNG 方便的 DataProvider 特性,借助該特性,我創(chuàng)建了一個通用的測試用例并通過另一個方法來改變它的參數(shù)。運行清單 2 中定義的測試用例會產(chǎn)生兩個通過結果!一切都運轉(zhuǎn)良好,不是嗎?


          清單 2. 驗證兩個值的 TestNG 測試
                      import java.util.Vector;
                      import static org.testng.Assert.assertEquals;
                      import org.testng.annotations.DataProvider;
                      import org.testng.annotations.Test;
                      public class BuildHierarchyTest {
                      @DataProvider(name = "class-hierarchies")
                      public Object[][] dataValues(){
                      return new Object[][]{
                      {Vector.class, new String[] {"java.util.AbstractList",
                      "java.util.AbstractCollection"}},
                      {String.class, new String[] {}}
                      };
                      }
                      @Test(dataProvider = "class-hierarchies"})
                      public void verifyHierarchies(Class clzz, String[] names) throws Exception{
                      Hierarchy hier = HierarchyBuilder.buildHierarchy(clzz);
                      assertEquals(hier.getHierarchyClassNames(), names, "values were not equal");
                      }
                      }
                      

          至此,我還是沒有發(fā)現(xiàn)缺陷,但一些代碼問題卻困擾著我。如果有人不經(jīng)意地為 Class 參數(shù)傳入一個 null 值會怎么樣呢?清單 1 中第 4 行的 clzz.getSuperclass() 調(diào)用會拋出一個 NullPointerException,是這樣嗎?

          測試我的理論很容易;甚至都不用從頭開始。僅僅把 {null, null} 添加到初始 BuildHierarchyTest 的 dataValues 方法中的多維 Object 數(shù)組中,然后再次運行它。我定會得到如圖 1 所示的 NullPointerException:


          圖 1. 可怕的 NullPointerException
          可怕的 NullPointerException

          參見這里的 全圖

          關于靜態(tài)分析

          諸如 FindBugs 等靜態(tài)分析工具通過將字節(jié)碼和一系列 bug 模式相匹配來檢驗類或 JAR 文件從而尋找潛在問題。針對樣例的代碼運行 FindBugs 不會 揭示出清單 1 中找到的 NullPointerException。

          防御性編程

          一旦出現(xiàn)這個問題,下一步就是要拿出對抗的策略。問題是我控制不了這個方法能否接收這種輸入。對于這類問題,開發(fā)人員通常會使用防御性編程技術,該技術專門用來在發(fā)生摧毀性后果前捕捉潛在錯誤。

          對象驗證是處理不確定性的一項經(jīng)典的防御性編程策略。相應地,我會添加一項檢驗來驗證 clzz 是否為 null,如清單 3 所示。如果其值最終為 null,我就會拋出一個 RuntimeException 來警告他人注意這個潛在問題。


          清單 3. 添加驗證 null 值的檢驗
                      public static Hierarchy buildHierarchy(Class clzz){
                      if(clzz == null){
                      throw new RuntimeException("Class parameter can not be null");
                      }
                      Hierarchy hier = new Hierarchy();
                      hier.setBaseClass(clzz);
                      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;
                      }
                      }
                      

          很自然,我也會編寫一個快速測試用例來驗證我的檢驗是否真能避免 NullPointerException,如清單 4 所示:


          清單 4. 驗證 null 檢驗
                      @Test(expectedExceptions={RuntimeException.class})
                      public void verifyHierarchyNull() throws Exception{
                      Class clzz = null;
                      HierarchyBuilder.buildHierarchy(null);
                      }
                      

          在本例中,防御性編程似乎解決了問題。但僅依靠這項策略會存在一些缺陷。

          防御的缺陷

          關于斷言

          清單 3 使用一個條件來驗證 clzz 的值,實際上 assert 也同樣好用。使用斷言,無需指定條件,也不需要指定異常語句。在啟用了斷言的情況下,防御性編程的關注點全部由 JVM 處理。

          盡管防御性編程有效地保證了方法的輸入條件,但如果在一系列方法中使用它,不免過于重復。熟悉面向方面編程(或 AOP)的人們會把它認為是橫切關注點,這意味著防御性編程技術橫跨了代碼庫。許多不同的對象都采用這些語法,盡管從純面向?qū)ο蟮挠^點來看這些語法跟對象毫不相關。

          而且,橫切關注點開始滲入到契約式設計(DBC)的概念中。DBC 是這樣一項技術,它通過在組件的接口顯式地陳述每個組件應有的功能和客戶機的期望值來確保系統(tǒng)中所有的組件完成它們應盡的職責。從 DBC 的角度講,組件應有的功能被認為是后置條件,本質(zhì)上就是組件的責任,而客戶機的期望值則普遍被認為是前置條件。另外,在純 DBC 術語中,遵循 DBC 規(guī)則的類針對其將維護的內(nèi)部一致性與外部世界有一個契約,即人所共知的類不變式。





          契約式設計

          我在以前的一篇關于用 Nice 編程的文章中介紹過 DBC 的概念,Nice 是一門與 JRE 兼容的面向?qū)ο缶幊陶Z言,它的特點是側(cè)重于模塊性、可表達性和安全性。有趣的是,Nice 并入了功能性開發(fā)技術,其中包括了一些在面向方面編程中的技術。功能性開發(fā)使得為方法指定前置條件和后置條件成為可能。

          盡管 Nice 支持 DBC,但它與 Java™ 語言完全不同,因而很難將其用于開發(fā)。幸運的是,很多針對 Java 語言的庫也都為 DBC 提供了方便。每個庫都有其優(yōu)點和缺點,每個庫在 DBC 內(nèi)針對 Java 語言進行構建的方法也不同;但最近的一些新特性大都利用了 AOP 來更多地將 DBC 關注點包括進來,這些關注點基本上就相當于方法的包裝器。

          前置條件在包裝過的方法執(zhí)行前擊發(fā),后置條件在該方法完成后擊發(fā)。使用 AOP 構建 DBC 結構的一個好處(請不要同該語言本身相混淆?。┦牵嚎梢栽诓恍枰?DBC 關注點的環(huán)境中將這些結構關掉(就像斷言能被關掉一樣)。以橫切的方式對待安全性關注點的真正妙處是:可以有效地重用 這些關注點。眾所周知,重用是面向?qū)ο缶幊痰囊粋€基本原則。AOP 如此完美地補充了 OOP 難道不是一件極好的事情嗎?





          結合了 OVal 的 AOP

          OVal 是一個通用的驗證框架,它通過 AOP 支持簡單的 DBC 結構并明確地允許:

          • 為類字段和方法返回值指定約束條件
          • 為結構參數(shù)指定約束條件
          • 為方法參數(shù)指定約束條件

          此外,OVal 還帶來大量預定義的約束條件,這讓創(chuàng)建新條件變得相當容易。

          由于 OVal 使用 AspectJ 的 AOP 實現(xiàn)來為 DBC 概念定義建議,所以必須將 AspectJ 并入一個使用 OVal 的項目中。對于不熟悉 AOP 和 AspectJ 的人們來說,好消息是這不難實現(xiàn),且使用 OVal (甚至是創(chuàng)建新的約束條件)并不需要真正對方面進行編碼,只需編寫一個簡單的自引導程序即可,該程序會使 OVal 所附帶的默認方面植入您的代碼中。

          在創(chuàng)建這個自引導程序方面前,要先下載 AspectJ。具體地說,您需要將 aspectjtoolsaspectjrt JAR 文件并入您的構建中來編譯所需的自引導程序方面并將其編入您的代碼中。





          自引導 AOP

          下載了 AspectJ 后,下一步是創(chuàng)建一個可擴展 OVal GuardAspect 的方面。它本身不需要做什么,如清單 5 所示。請確保文件的擴展名以 .aj 結束,但不要試著用常規(guī)的 javac 對其進行編譯。


          清單 5. DefaultGuardAspect 自引導程序方面
                      import net.sf.oval.aspectj.GuardAspect;
                      public aspect DefaultGuardAspect extends GuardAspect{
                      public DefaultGuardAspect(){
                      super();
                      }
                      }
                      

          AspectJ 引入了一個 Ant 任務,稱為 iajc,充當著 javac 的角色;此過程對方面進行編譯并將其編入主體代碼中。在本例中,只要是我指定了 OVal 約束條件的地方,在 OVal 代碼中定義的邏輯就會編入我的代碼,進而充當起前置條件和后置條件。

          請記住 iajc 代替了 javac。例如,清單 6 是我的 Ant build.xml 文件的一個代碼片段,其中對代碼進行了編譯并把通過代碼標注發(fā)現(xiàn)的所有 OVal 方面編入進來,如下所示:


          清單 6. 用 AOP 編譯的 Ant 構建文件片段
                      <target name="aspectjc" depends="get-deps">
                      <taskdef resource="org/aspectj/tools/ant/taskdefs/aspectjTaskdefs.properties">
                      <classpath>
                      <path refid="build.classpath" />
                      </classpath>
                      </taskdef>
                      <iajc destdir="${classesdir}" debug="on" source="1.5">
                      <classpath>
                      <path refid="build.classpath" />
                      </classpath>
                      <sourceroots>
                      <pathelement location="src/java" />
                      <pathelement location="test/java" />
                      </sourceroots>
                      </iajc>
                      </target>
                      

          為 OVal 鋪好了路、為 AOP 過程做了引導之后,就可以開始使用 Java 5 標注來為代碼指定簡單的約束條件了。





          OVal 的可重用約束條件

          用 OVal 為方法指定前置條件必須對方法參數(shù)進行標注。相應地,當調(diào)用一個用 OVal 約束條件標注過的方法時,OVal 會在該方法真正執(zhí)行前 驗證該約束條件。

          在我的例子中,我想要指定當 Class 參數(shù)的值為 null 時,buildHierarchy 方法不能被調(diào)用。OVal 通過 @NotNull 標注支持此約束條件,該標注在方法所需的所有參數(shù)前指定。也要注意,任何想要使用 OVal 約束條件的類也必須在類層次上指定 @Guarded 標注,就像我在清單 7 中所做的那樣:


          清單 7. OVal 約束條件
                      import net.sf.oval.annotations.Guarded;
                      import net.sf.oval.constraints.NotNull;
                      @Guarded
                      public class HierarchyBuilder {
                      public static Hierarchy buildHierarchy(@NotNull Class clzz){
                      Hierarchy hier = new Hierarchy();
                      hier.setBaseClass(clzz);
                      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;
                      }
                      }
                      }
                      

          通過標注指定這個約束條件意味著我的代碼不再會被重復的條件弄得亂七八糟,這些條件檢查 null 值,并且一旦找到該值就會拋出異?!,F(xiàn)在這項邏輯由 OVal 處理,且處理的方法有些相似 —— 事實上,如果違反了約束條件,OVal 會拋出一個 ConstraintsViolatedException,它是 RuntimeException 的子類。

          當然,我下一步就要編譯 HierarchyBuilder 類和 清單 5 中相應的 DefaultGuardAspect 類。我用 清單 6 中的 iajc 任務來實現(xiàn)這一目的,這樣我就能把 OVal 的行為編入我的代碼中了。

          接下來,我更新 清單 4 中的測試用例來驗證是否拋出了一個 ConstraintsViolatedException,如清單 8 所示:


          清單 8. 驗證是否拋出了 ConstraintsViolatedException
                      @Test(expectedExceptions={ConstraintsViolatedException.class})
                      public void verifyHierarchyNull() throws Exception{
                      Class clzz = null;
                      HierarchyBuilder.buildHierarchy(clzz);
                      }
                      

          指定后置條件

          正如您所見,指定前置條件其實相當容易,指定后置條件的過程也是一樣。例如,如果我想對所有調(diào)用 buildHierarchy 的程序保證它不會返回 null 值(這樣,這些調(diào)用程序就不需要再檢查這個了),我可以在方法聲明之上放置一個 @NotNull 標注,如清單 9 所示:


          清單 9. OVal 中的后置條件
                      @NotNull
                      public static Hierarchy buildHierarchy(@NotNull Class clzz){
                      //method body
                      }
                      

          當然,@NotNull 絕不是 OVal 提供的惟一約束條件,但我發(fā)現(xiàn)它能非常有效地限制這些令人討厭的 NullPointerException,或至少能夠快速地暴露 它們。

          更多的 OVal 約束條件

          OVal 也支持在方法調(diào)用前或后對類成員進行預先驗證。這種機制具有限制針對特定約束條件的重復條件測試的好處,如集合大小或之前討論過的非 null 的情況。

          例如,在清單 10 中,我使用 HierarchyBuilder 定義了一個為類層次構建報告的 Ant 任務。請注意 execute() 方法是如何調(diào)用 validate 的,后者會依次驗證 fileSet 類成員是否含值;如果不含,會拋出一個異常,因為沒有了要評估的類,該報告不能運行。


          清單 10. 帶條件檢驗的 HierarchyBuilderTask
                      public class HierarchyBuilderTask extends Task {
                      private Report report;
                      private List fileSet;
                      private void validate() throws BuildException{
                      if(!(this.fileSet.size() > 0)){
                      throw new BuildException("must supply classes to evaluate");
                      }
                      if(this.report == null){
                      this.log("no report defined, printing XML to System.out");
                      }
                      }
                      public void execute() throws BuildException {
                      validate();
                      String[] classes = this.getQualifiedClassNames(this.fileSet);
                      Hierarchy[] hclz = new Hierarchy[classes.length];
                      try{
                      for(int x = 0; x < classes.length; x++){
                      hclz[x] = HierarchyBuilder.buildHierarchy(classes[x]);
                      }
                      BatchHierarchyXMLReport xmler = new BatchHierarchyXMLReport(new Date(), hclz);
                      this.handleReportCreation(xmler);
                      }catch(ClassNotFoundException e){
                      throw new BuildException("Unable to load class check classpath! " + e.getMessage());
                      }
                      }
                      //more methods below....
                      }
                      

          因為我用的是 OVal,所以我可以完成下列任務:

          • fileSet 類成員指定一個約束條件,確保使用 @Size 標注時其大小總是至少為 1 或更大。
          • 確保在使用 @PreValidateThis 標注調(diào)用 execute() 方法前 驗證這個約束條件。

          這兩步讓我能夠有效地去除 validate() 方法中的條件檢驗,讓 OVal 為我完成這些,如清單 11 所示:


          清單 11. 經(jīng)過改進、無條件檢驗的 HierarchyBuilderTask
                      @Guarded
                      public class HierarchyBuilderTask extends Task {
                      private Report report;
                      @Size(min = 1)
                      private List fileSet;
                      private void validate() throws BuildException {
                      if (this.report == null) {
                      this.log("no report defined, printing XML to System.out");
                      }
                      }
                      @PreValidateThis
                      public void execute() throws BuildException {
                      validate();
                      String[] classes = this.getQualifiedClassNames(this.fileSet);
                      Hierarchy[] hclz = new Hierarchy[classes.length];
                      try{
                      for(int x = 0; x < classes.length; x++){
                      hclz[x] = HierarchyBuilder.buildHierarchy(classes[x]);
                      }
                      BatchHierarchyXMLReport xmler = new BatchHierarchyXMLReport(new Date(), hclz);
                      this.handleReportCreation(xmler);
                      }catch(ClassNotFoundException e){
                      throw new BuildException("Unable to load class check classpath! " + e.getMessage());
                      }
                      }
                      //more methods below....
                      }
                      

          清單 11 中的 execute() 一經(jīng)調(diào)用(由 Ant 完成),OVal 就會驗證 fileSet 成員。如果其為空,就意味著沒有指定任何要評估的類,就會拋出一個 ConstraintsViolatedException。這個異常會暫停這一過程,就像初始代碼一樣,只不過初始代碼會拋出一個 BuildException。

          posted on 2007-04-05 09:40 愛芬先生 閱讀(144) 評論(0)  編輯  收藏 所屬分類: 我喜愛的文章

          只有注冊用戶登錄后才能發(fā)表評論。


          網(wǎng)站導航:
           
          主站蜘蛛池模板: 阳东县| 禹州市| 聂荣县| 扎兰屯市| 灵丘县| 孟村| 闽侯县| 荃湾区| 南通市| 兰州市| 乌鲁木齐市| 长治县| 铜陵市| 阳谷县| 理塘县| 石阡县| 阿坝县| 都江堰市| 洪江市| 工布江达县| 平安县| 曲松县| 民权县| 东港市| 大荔县| 东方市| 肥东县| 平谷区| 杂多县| 绥宁县| 辉南县| 呈贡县| 盖州市| 兴宁市| 道孚县| 阳原县| 丹寨县| 长治市| 常山县| 宜昌市| 娄底市|