程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> 診斷Java代碼: Liar View錯誤模式

診斷Java代碼: Liar View錯誤模式

編輯:關於JAVA

Liar, liar!

設想一下:您已經為一個分布式系統精心設計了一個極好的 GUI 程序,它包含了客戶機請求的所有東西及其它一些東西。您已經讓它運行通過了一個自動化測試套件的測試 ― 由於不變量的數量是個天文數字,因此,自動化測試是必須的。測試的結果是程序獲得了一張“無錯誤的健康證明書”。

發布這個 GUI 的期限到了,但是,作為一個象您這樣嚴格的程序員,只是為了發現錯誤的行為 ― 本該在自動化測試中就被捕捉到的行為,您啟動了程序,對它做最後一次手工測試。但願您能夠避免這種情形。真的,您能夠。

Liar View 錯誤模式

好的調試從好的測試開始。由於 GUI 程序中有大量的不變量需要檢查,因此,自動化測試是必需的。但有時盡管已經通過了一套測試,在手工檢查時,程序仍會出現本該由這些測試之一發現的錯誤行為。

在分布式和多線程系統中,這種行為是常見的。在這些情況下,程序的“不確定性”本質經常就是原因所在。但在 GUI 中,卻有另一種常見的原因 ― Liar View 錯誤模式。

症狀

多數 GUI 程序測試,跟通常的程序測試一樣,遵循下列步驟:

啟動程序

檢查程序狀態的一些特征

嘗試修改狀態

檢查狀態是否已經按意願被修改

但就象我已提到過的,有時對運行時程序行為的手工檢查的結果會與測試得到的成功結果相矛盾:屏幕上可能顯示一個隊列,包含被測試(按設想進行)確認刪除了的元素;而對象中可能包含報告顯示已被更新了的陳舊數據。

類似這樣的錯誤會使我們對自己的心智是否健全產生懷疑,或者更糟地陷入到康德的懷疑論哲學,懷疑起原因本身的有效性。

不要讓這些發生在您身上。當正確對待原因時,它確實是有用的。盡管有反面的報告,但很少有程序員會在寫代碼時永久地喪失心智( 永久地是一個關鍵詞)。

起因

找到這些錯誤的一個關鍵是要認識到,至少有一部分錯誤可以在測試套件中找到。

在測試 GUI 程序的過程中,錯誤最常發生的地方是在最後一步:檢查狀態是否已經按意願被修改。原因是 GUI 一般是基於模型-視圖-控制器(MVC)體系結構設計的。Swing 類庫甚至把這種體系結構建到了 GUI 類自身的結構中。

在 MVC 體系結構中,程序的內部狀態保存在模型中。視圖響應改變模型狀態的事件並相應更新屏幕圖像。控制器把這兩個組件連接在一起。

這種體系結構的優點是把視圖從模型中分離出來,使得各自的實現可以獨立修改。但是它對自動化測試方法卻是一個挑戰:我們很難檢驗模型中的狀態改變是否在視圖中得到了適當的反映。當這兩者之間存在矛盾時,我們就會遇到一個 Liar View 錯誤模式的實例。

例如,考慮下面的簡單 GUI。它在一列元素的內容被更新時顯示其內容。 Controller 類的 main 方法被用作一個簡單的測試。在實際的應用程序中,我把這個方法移到單獨的測試類中,並將其掛到 JUnit 中(請參閱 參考資料)。

為了讓我們能夠使測試以慢動作方式進行,並在每個事件發生時對它進行手工檢查,我添加了 pause() 方法和 PAUSE 字段。

清單 1. JTable 及表模型

import java.awt.*;
import java.awt.event.*;
import java.util.Vector;
import javax.swing.*;
public class Controller {
  private static final int PAUSE = 1;
  private static void assert(boolean assertion) {
   if (! assertion) {
    throw new RuntimeException("Assertion Failed");
   }
  }
  private void pause() {
   try {
    synchronized (this) {
   wait(PAUSE);
    }
   }
   catch (InterruptedException e) {
   }
  }
  public static void main(String[] args) {
   Controller controller = new Controller();
   JFrame frame = new JFrame("Test");
   Model model = new Model();
   JList view = new JList(model);
   view.setPreferredSize(new Dimension(200,100));
   frame.getContentPane().add(view);
   frame.pack();
   frame.setVisible(true);
   assert(model.getSize() == 0);
   controller.pause();
   model.add("test0");
   controller.pause();
   model.add("test1");
   controller.pause();
   assert(model.getSize() == 2);
   controller.pause();
   model.remove(0);
   controller.pause();
   assert(model.getSize() == 1);
   controller.pause();
   System.exit(0);
  }
}
class Model extends AbstractListModel {
  private Vector elements = new Vector();
  public synchronized Object getElementAt(int index) {
   return elements.get(index);
  }

  public synchronized int getSize() {
   return elements.size();
  }
  public synchronized void add(Object o) {
   int index = this.getSize();
   this.elements.add(o);
   this.fireIntervalAdded(this, index, index);
  }
  public synchronized void remove(int index) {
   this.elements.remove(index);
  }
}

您可能已經注意到這段代碼有一個嚴重錯誤。如果我們運行這段測試代碼,所有的斷言都會是成功的,它表明已在列表中正確地添加或除去了項目。但如果我們通過某種方法,例如把 PAUSE 設成 1000,使運行速度降下來,那麼我們就能以手工檢查的方式運行測試。猜猜結果是什麼?我們注意到,視圖中不曾有項目被除去。

視圖沒被更新的原因是, Model 類中的 remove() 方法從未調用 fireIntervalRemoved() 來通知偵聽器:模型的狀態已經改變了。

但我們的測試方法中的所有斷言都取得了成功。為什麼呢?因為這些斷言只是在模型中,而不是在視圖中檢查發生的更改。因為模型被適當地更新了,斷言沒能檢測到遺漏的事件觸發。

治療及預防措施

防止這種錯誤模式的一種方法是: 只在修改模型的狀態之後才檢查視圖的顯式屬性。雖然這種技術把我們能檢查的屬性限制在視圖提供的范圍內,但至少能使斷言反映屏幕上實際正在發生的事情。

例如,我們可重寫 Controller.main ,如下所示:

清單 2. 檢查視圖、模型及行的內容

import java.awt.*;
import java.awt.event*;
import java.util.Vector;
import java.swing.*;
public class Controller {
   . . .

  public static void main(String[] args) {
   Controller controller = new Controller();
   JFrame frame = new JFrame("Test");
   Model model = new Model();
   JList view = new JList(model);
   view.setPreferredSize(new Dimension(200,100));
   frame.getContentPane().add(view);
   frame.pack();
   frame.setVisible(true);
   assert (model.getSize() == 0);
   controller.pause();
   boolean toggle = model.toggle;
   model.add("test0");
   assert ( toggle == ! model.toggle);
   controller.pause();
   toggle = model.toggle;
   model.add("test1");
   assert ( toggle == ! model.toggle);
   controller.pause();
   assert(model.getSize() == 2);
   view.setSelectedIndex(0);
   assert(view.getSelectedValue().equals("test0"));
   controller.pause();
   toggle = model.toggle;
   model.remove(0);
   assert(toggle == ! model.toggle);
   controller.pause();
   assert(model.getSize() == 1);
   view.setSelectedIndex(0);
   assert(view.getSelectedValue().equals("test1"));
   controller.pause();
   System.exit(0);
  }
}
  class Model extends AbstractListModel {
   boolean switch = false;
   private Vector elements = new Vector();
   ...
   public void fireIntervalAdded(AbstractListModel m, int start, int end) {
    super.fireIntervalAdded(m,start,end);
    this.switch = ! this.switch;
   }
   public void fireIntervalRemoved(AbstractListModel m, int start, int end) {
    super.fireIntervalAdded(m,start,end);
    this.switch = ! this.switch;
   }

  }

通過使用 setSelectedIndex() 和 getSelectedIndex() ,我們對程序進行測試,測試雖只是稍有不同,但卻有極大改善。修改後的測試不單檢查模型,還檢查視圖,而且不只簡單地檢查行的數量,還檢查所選行的內容。

直接核查視圖的另一種方法是使用 Java Robot 類(在 Java 1.3 API 中有介紹 ― 請參閱 參考資料),使 GUI 的鼠標和鍵盤的物理操作真正實現自動化。

Robot 類還允許您對屏幕的局部進行快照,允許建立基於 GUI 視圖實際物理布局的測試。當然,如果視圖的物理布局不如其邏輯結構穩定,這種能力會成為一種缺點。每次物理布局改變時,都必須重寫幾個測試是很痛苦的。因此,對於視圖不會頻繁改變的成熟的 GUI,我推薦您使用 Robot 類作為測試工具。要測試邏輯方面的問題,您可以象我們上面所做的那樣,調用視圖的方法。

最後一條忠告:對視圖對象中簡單地把調用彈回到模型的方法要小心。這樣做很快就會引入 Liar View。特別是 JTables,其中包含有很多這樣的方法。

總結

以下是本周討論的錯誤模式的小結:

模式:Liar View

症狀:GUI 程序通過了測試套件的測試,但後來卻顯示出本該在那些測試中被消除了的行為。

起因:測試只在模型方面做檢查,而沒有在視圖方面做檢查。

治療及預防措施:在視圖方面做檢查。

一點一點地,我們終於努力學完了關於最常見(也最讓人沮喪)的錯誤模式的解決方案,但我們仍然有很多事情要做。下次,我們將對付一種更具破壞性的錯誤:破壞者數據,數據一開始是完美的……直到存取它們時。且待下回分解!

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