程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> Java理論與實踐: 平衡測試,第3部分:用方面檢驗設計約束

Java理論與實踐: 平衡測試,第3部分:用方面檢驗設計約束

編輯:關於JAVA

面向方面編程(AOP)是項大有前途的新技術,但是采用新技術可能有風險( 當然,不 采用新技術也會有風險)。與所有的新技術一樣,通常來說,最好是 沿著一條可以管理風險的路徑來采用它們。如果用 AOP 來執行策略和測試,就 可以從 AOP 得到降低風險的好處。因為方面不會進入生產,所以不會出現技術 破壞代碼穩定性或開發過程的風險,但卻會有助於開發質量更好的軟件。用方面 進行測試也是學習方面的工作方式,並體驗這項激動人心的新技術的好方法。

組合測試方法

正如我在 第 1 部分 中討論過的,QA 的目的不是找到所有可能有的 bug — — 因為這是不可能的 —— 而是提升我們對代碼按預期工作的確信程度。對於 管理有效的 QA 組織,它的挑戰就是最大化所花費資源的回報,即確信度。因為 所有的測試方法最終都會表現出回報消退(對於等量的付出增加,得到的確信度 增加越來越少),而且不同的方法適合尋找不同類型的錯誤,所以把 QA 付出分 散在測試、代碼審查和靜態分析上,要比把整個 QA 預算只花在其中一項措施上 ,回報要更好。

FindBugs 這樣的靜態分析工具是不精確的,但是不精確的分析對於提高軟件 質量仍然是非常有用和有效的。它們可能發出假警告,例如在無害的構造上觸發 警告,也可能忽略了 bug,例如沒有找出與特定 bug 模式匹配的全部 bug。但 是它們仍然能發現真正的 bug,而且只要誤報率沒有高到讓用戶厭煩的程度,那 麼它們仍然對測試付出提供了有價值的回報。

從測試的角度來說,使用 AOP 來驗證設計規則與使用靜態分析有許多共同之 處。靜態分析和面向方面編程都不用為了特定的方法或類設計測試用例,而是都 鼓勵找出違犯規則的全部分類,並創建能夠發現代碼體中任何違規的工件。另一 個相似性就是它們不必非常完善也能夠發揮作用;盡管 bug 探測器或測試方面 都不能找出所有可能的 bug,甚至有些會發出假警告,它們仍然是非常有用的工 具,可以驗證代碼是否按期望的那樣工作。有些 bug 模式用靜態工具更容易找 出,而另一些用方面會更容易找出 —— 這使得方面成為參與 QA 過程的一個有 用的方法。

簡單的測試方面

FindBugs 這樣的靜態分析工具審計代碼但不執行代碼;面向方面的工具既提 供靜態類工具,也提供動態類工具。靜態方面可以生成編譯時警告或錯誤;動態 方面可以把錯誤檢測代碼插入類。

在 第 1 部分 中,我提供了一個簡單的 FindBugs 探測器,查找可能潛伏在 庫中的對 System.gc() 的調用。靜態分析能探測的許多 bug 模式(包括這個模 式)也能被方面探測到;根據具體的 bug 模式,用靜態分析或用方面來做可能 會更容易,所以把它們都放在工具庫中,可以提高效果。

清單 1 顯示了一個簡單的動態方面,在要調用 System.gc() 時,拋出 AssertionError。(因為這類 bug 探測器的一個重要作用是不僅要找到您自己 代碼中的錯誤,還要找到代碼依賴的庫中的錯誤,所以可能需要告訴工具還要分 析或處理這些庫。)

清單 1. 執行 “不調用 System.gc()” 規則的動態方面

public  aspect GcAspect {
   pointcut gcCalls() : call(void java.lang.System.gc ());

   before() : gcCalls() {
     throw new AssertionError("Don't call System.gc!");
   }
}

清單 1 演示的動態方式不如使用靜態分析進行測試有效,因為它要求程序在 方面發現問題之前,實際地執行對 System.gc() 的調用,而不是程序只需包含 一個對 System.gc() 的調用,就會被探測到。但是,很快就會看到,動態方面 更靈活,因為它們能在方面觸發的點上執行任意測試代碼,從而對聲明的問題提 供更精細的控制。

也可以容易地創建一個靜態方面,在編譯時識別對 System.gc() 的調用,如 清單 2 所示。同樣,如果想發現在庫代碼中出現的這個 bug 模式,不僅要處理 項目中的代碼,還要處理它使用的庫。

清單 2. 執行 “不調用 System.gc()” 規則的靜態方面

public  aspect StaticGcAspect {
   pointcut gcCalls() : call(void java.lang.System.gc ());

   declare error : gcCalls() : "Don't call System.gc!";
}

檢查對 Swing 單線程規則的違犯

有一個幾乎無法靜態地實施的規則是線程限制 —— 指定的對象只能從一個 線程訪問(有時是特定線程,例如 Swing 事件線程)。Swing 程序的正確性依 賴於線程限制,但是對於實施這個規則,從編譯器、運行時或類庫都得不到任何 幫助。如果違犯了這個規則,程序就會被破壞,但是因為在測試時程序可能看起 來工作正常,所以問題可能一直暴露不了。

Swing 單線線程規則指定:

Swing 組件和模塊只應當從事件分派線程中創建、修改和實現。

單線程規則的早期描述允許 Swing 組件和模塊在屏幕上出現之前,由其他線 程訪問,但是這種方式帶來了線程安全問題,所以規則被強化了。使用 SwingUtilities.isEventDispatchThread() 方法,Swing 提供了一個機制,詢 問 “當前線程是不是事件分派線程?”。所以需要在每個 Swing 的方法調用之 前插入代碼,檢查調用是否是由合適的線程發出的,如果不是由合適線程發出的 ,就拋出 AssertionError,這樣就能在測試中捕捉到對單線程規則的違犯,而 不會讓它們在生產中造成莫名其妙的故障。

清單 3 演示了一個可以檢測許多單線程規則違犯的方面:它有兩部分:不應 當從事件線程之外調用的方法的列表,以及要插到對這些方法的調用之前的代碼 。建議(要插入的代碼)非常簡單:檢查當前線程是否是事件線程,如果不是, 就拋出 AssertionError。這個方面處理了對 Swing 包中的所有方法以及擴展了 最重要的 Swing 類的那些類中的方法的全部調用(以便捕捉用戶提供的組件和 模塊),但是它排除了這些類中已知為可以安全地(或者需要)從多線程調用的 方法。安全方法列表並不全面;構建一個全面的列表可能要花費一些額外時間研 究 Javadoc,找到所有標記為線程安全的方法。

清單 3. 實施 Swing 的單線程規則的方面

public aspect  SwingThreadAspect {

  pointcut swingMethods() : call(* javax.swing..*.*(..))
     || call(javax.swing..*.new(..));

  pointcut extendsSwing() : call(* javax.swing.JComponent+.* (..))
     || call(* javax.swing..*Model+.*(..))
     || call(* javax.swing.text.Document+.*(..));

  pointcut safeMethods() : call(void JComponent.revalidate())
     || call(void JComponent.invalidate(..))
     || call(void JComponent.repaint(..))
     || call(void add*Listener(EventListener+))
     || call(void remove*Listener(EventListener+))
     || call(boolean SwingUtilities.isEventDispatchThread())
     || call(void SwingUtilities.invokeLater(Runnable))
     || call(void SwingUtilities.invokeAndWait(Runnable))
     || call(void JTextPane.replaceSelection(..))
     || call(void JTextPane.insertComponent(..))
     || call(void JTextPane.insertIcon(..))
     || call(void JTextPane.setLogicalStyle(..))
     || call(void JTextPane.setCharacterAttributes(..))
     || call(void JTextPane.setParagraphAttributes(..));

  pointcut edtMethods() : (swingMethods() || extendsSwing())  && !safeMethods();

  before() : edtMethods() {
  if (!SwingUtilities.isEventDispatchThread())
   throw new AssertionError(thisJoinPointStaticPart.getSignature ()
    + " called from " + Thread.currentThread().getName ());
  }
}

swingMethods() 切入點包含對 javax.swing 包中的所有方法(包括構 造函數)的調用。extendsSwing() 切入點代表對所有擴展自 JComponent 或任 何 Swing 模型類的類中方法的全部調用。safeMethods() 切入點代表一些已知 可以從任何線程安全調用的 Swing 方法。

SwingThreadAspect 並 不完美,但是足夠了。safeMethods() 切入點沒有完全枚舉線程安全方法,而且 extendsSwing() 切入點可能也沒有包含所有經常被擴展的 Swing 類。但是我們 不會把它們用於生產 —— 只是用它們進行測試。它能夠不必為每個 程序創建新的測試用例就發現 bug,而這就是它的價值所在。而且,像大多數 bug 探測器一樣,它可能會在以前以為是正確的程序中找到 bug。

在調試對象中切換

方面的另一個好應用就是在類的正式版本和 “調試” 版本之間進行切換。創建一個類的調試版本是相當普遍的 情況,例如創建一個帶有更多日志或錯誤檢測的版本,這個版本因為副作用或性 能問題而不適合在生產中使用。但是在需要的時候在調試版本中切換,會很煩瑣 或者容易出錯。如果對象是通過構造函數實例化的,就不得不在代碼中找到所有 調用構造函數的地方。緩解修改所有構造函數調用的不方便性的一種常用技術是 ,改用工廠來實例化對象,但是只為了在生產版本和調試版本之間進行選擇而使 用工廠,會增加復雜性或帶來安全漏洞。

如果目的是為了 “ 在所有實例化 Foo 的地方,都換成實例化 DebuggingFoo”,那麼方面為 做這件事提供了非常可靠且不需要修改程序的簡單機制。作為示例,清單 4 顯 示了一個方面,它有助於發現死鎖,把 ReentrantLock 的所有實例化都替換成 DebuggingLock。(請注意,AspectJ 只修改要求 AspectJ 編譯器處理的代碼中 的調用;Java™ 類庫本身中對 ReentrantLock 的實例化不會被替換,除 非特意把方面編織到平台庫中。)

清單 4. 把所有 ReentrantLock 的實例化替換成 DebuggingLock 的方面

public aspect ReentrantLockAspect {

  pointcut newLock() : call(ReentrantLock.new());

  pointcut newLockFair(boolean fair) : 
   call(ReentrantLock.new(boolean)) && args(fair);

  ReentrantLock around() : newLock() {
   return new DebuggingLock();
  }

  ReentrantLock around(boolean fair) : newLockFair(fair) {
   return new DebuggingLock (fair);
  }
}

在 Java SE 6 中,運行時對請求執行死鎖檢測,通過 java.lang.management 中的 ThreadMXBean 接口,或者在請求線程轉儲時執行 。清單 5 顯示了 DebuggingLock 的一個可能實現,每次請求鎖時,都執行死鎖 檢測,所以可以更迅速地得到死鎖的警告。鎖定性能要比 ReentrantLock 差, 因為每次試圖鎖定時要做更多的工作,所以這種方式可能不適合在生產中使用。(而且,維護 waitingFor 數據結構時自帶的同步,可能會干擾應用程序的計時 ,從而改變死鎖的可能性。)

清單 5. ReentrantLock 的調試版本會在檢測到死鎖時拋出 AssertionError

public class DebuggingLock extends  ReentrantLock {
   private static ConcurrentMap<Thread, DebuggingLock>  waitingFor
     = new ConcurrentHashMap<Thread, DebuggingLock> ();

   public DebuggingLock() { super(); }
   public DebuggingLock(boolean fair) { super(fair); }

   private void checkDeadlock() {
     Thread currentThread = Thread.currentThread();
     Thread t = currentThread;
     while (true) {
       DebuggingLock lock = waitingFor.get(t);
       if (lock == null || !lock.isLocked())
         return;
       else {
         t = lock.getOwner();
         if (t == currentThread)
           throw new AssertionError("Deadlock  detected");
       }
     }

   }

   public void lock() {
     if (tryLock())
       return;
     else {
       waitingFor.put(Thread.currentThread(), this);
       try {
         checkDeadlock();
         super.lock();
       }
       finally {
         waitingFor.remove(Thread.currentThread());
       }
     }
   }
}

要讓清單 5 中的 DebuggingLock 版本有幫助,程序必須在測試時實際地發 生死鎖。因為死鎖通常依賴於計時和環境,所以清單 5 中的方法可能還不夠。清單 6 顯示了另一個版本的 DebuggingLock,它不僅判斷是否發生死鎖,還會 判斷給定的一對鎖是否由多個線程在不一致的順序下得到。每次得到鎖時,它都 查看已經持有的鎖的集合,對於每個鎖,都記住在這個鎖之前某個線程已經請求 了這些鎖。在試圖獲得鎖之前,lock() 方法都查看已經持有的鎖,如果在這個 鎖之後已經得到了其中一個鎖,就拋出 AssertionError。這個實現的空間開銷 要比前一個版本大得多(因為需要跟蹤在給定鎖之前所有已經得到的鎖),但是 它能檢測到更大泛圍的 bug。它不會檢測出所有可能的死鎖 —— 只有由兩個特 定鎖之間的不一致順序造成的死鎖,而這是最常見的情況。

清單 6. DebuggingLock 的替代版本,即使死鎖沒有後果,也能檢查出不一 致的鎖定順序

public class OrderHistoryLock extends  ReentrantLock {
   private static ThreadLocal<Set<OrderHistoryLock>>  heldLocks =
    new ThreadLocal<Set<OrderHistoryLock>>() {
     public Set<OrderHistoryLock> initialValue() {
       return new HashSet<OrderHistoryLock>();
     }
   };

   private final Map<Lock, Boolean> predecessors 
     = new ConcurrentHashMap<Lock, Boolean>();

   public OrderHistoryLock() { super(); }

   public OrderHistoryLock(boolean fair) { super(fair); }

   public void lock() {
     boolean alreadyHeld = isHeldByCurrentThread();
     for (OrderHistoryLock lock : heldLocks.get()) {
       if (lock.predecessors.containsKey(this))
         throw new AssertionError("Possible deadlock  between "
          + this + " and " + lock);
       else if (!alreadyHeld)
         predecessors.put(lock, Boolean.TRUE);
     }
     super.lock();
     heldLocks.get().add(this);
   }

   public void unlock() {
     super.unlock();
     if (!isHeldByCurrentThread())
       heldLocks.get().remove(this);
   }
}

結束語

這裡描述的方面屬於策略實施方面。有些策略是應用程序設計的一部分,例 如 “這些方法應當只從類 X 中調用” 或 “什麼東西都不要使用 System.out 或 System.err”。其他策略是 API 的接口合約的一部分,例如 Swing 的單線 程規則或 EJB 不應當創建線程或調用 AWT 之類的需求。在所有情況下,都可以 在開發和測試中使用方面找出是否違犯了這些策略。不論是否在生產中使用方面 ,它都是測試工具包中的一個優秀工具。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved