用 JUnit進行單元測試是一個功能強大的方法,它可以確保您的代碼基礎的完整性,但是一些不變量比其他(方法調用序列是其中一種)更難測試。在診斷Java 代碼這一部分,Eric Allen描述了怎樣在您的單元測試中使用記錄器(一種特殊的偵聽器),來確保一個方法調用序列按恰當的順序發生。請點擊文章頂部和底部的 討論,與作者和其他讀者在論壇上分享您關於本文的看法。
隨著時間的推移,當系統開發人員,維護人員甚至是系統詳細說明改變時,JUnit 框架提供一個很好的方法來改善系統的堅固性。通過測試,您可以檢查到代碼的某些不變量是受支持的。
測試通常分為兩類: 單元和 接受測試:
單元測試確保組成組件完成其應完成的功能。
接受測試確保系統的最高級功能出現在用戶面前時,與它設計時的功能一致。
JUnit 可幫助進行單元測試。
理想情況下,為系統開發的單元測試會完全覆蓋組成部分的預期不變量的設置,並能確保新的開發人員所作的任何更改都不會破壞現有代碼。
實際上,一些不變量將會被測試忽略。部分原因是一些不變量在沒達到全面的系統測試水平時,陷入到系統的許多孤立組件的交互作用中。
在本文中,我將討論一個那種類型的不變量以及如何使用一個復雜的單元測試來檢查此不變量。我要討論的不變量類型是一組相關方法序列調用的恰當順序。
與 JUnit 握手
在繼續之前,熟悉 JUnit 和學會怎樣輕松使用它來為您的代碼寫單元測試非常重要。在 參考資料一節,我已經包括了一個鏈接,它能鏈接到下載和開始使用 JUnit 所需要的所有信息。(如果您熟悉 JUnit,請直接跳到 第 1 個示例。)
單元測試為開發人員提供下列功能:
從接口透視圖設計類
除去發行包中的類混亂
自動確認捕捉變化的錯誤
單元測試過程通常按照以下步驟進行:
決定您的組件該做什麼。
正式地(或非正式地,取決於復雜性)設計您的組件。
寫出單元測試來檢查組件的活動。(在這一步,測試將不編譯;代碼還沒寫。寫測試的目的是用來幫助確定組件的功能目的。)
按設計寫出組件代碼;如果有必要,則進行單元重組。
當測試(從第 3 步開始)通過後,停止編碼過程。
集體討論其它的代碼中斷的可能性;寫出測試進行確認,然後修改代碼。
每次探測到一個缺陷就要寫一個新的測試。
每次改動代碼後都要重新開始 全部測試。
JUnit 是由 Erich Gamma 和 Kent Beck 創建的一個簡單構架,可用來編寫可重復的測試,它使得構造一個可增加改動的測試套件變得相對簡單,該測試套件可幫助開發人員評估開發的進展以及探測非故意的影響。JUnit 是 xUnit 架構的一個實例。
有了 JUnit,每個測試實例繼承了 TestCase 類。其中名字以 "test" 開始的每個無參數的公共方法每次執行一次。測試方法調用測試下的組件,並對該組件的行為做出斷言。在不能做出斷言的時候,JUnit 還會報告失敗的位置。
由於以下的原因,JUnit 尤其有用:
它是一個完整的、開放源代碼的產品;您不必自己寫或購買一個框架。
因為它是公開源代碼的,所以它的許多用戶都是很有經驗的。
它允許您從產品代碼中分離出測試代碼。
在構建過程中很容易整合。
現在,您已經了解了 JUnit,讓我們看一個示例。
Greeters 和 Senders
考慮下面的示例,它將給外部客戶端發送不同的消息:
清單 1. 同客戶端通信的三個類
public class Greeter {
public void sayHello() {...}
public void sayGoodbye() {...}
}
public class Sender {
public void sendFirstMessage() {...}
public void sendSecondMessage() {...}
}
public class Coordinator extends Thread {
Sender s;
Greeter g;
public Coordinator(Sender _s, Greeter _g) {
this.s = _s;
this.g = _g;
}
public void run() {
g.sayHello();
s.sendFirstMessage();
s.sendSecondMessage();
g.sayGoodbye();
}
}
第一個類, Greeter ,負責建立和中斷與外部客戶端的連接。第二個類, Sender ,負責給客戶端發送不同的消息。第三個類, Coordinator ,管理另外兩個類的實例,確保它們共同合作同客戶端進行通信。
勿庸置疑,按適當的順序調用這些方法是至關重要的。但是將來的擴展和代碼的單元重組可能會不經意間改變方法調用的順序。例如,另一個開發人員可能將 Greeter 和 Sender 方法的調用移到單獨的線程中,使用信號來控制調用它們的順序。
不管發生什麼改變,我們要怎樣將測試放在我們的套件,才能保證在任何情況下都能按正確的順序調用方法呢?與許多單元測試不同,我們不能僅僅調用這些方法並檢查結果,因為我們想要檢查的,並不是使用其中任何一個方法得出的結果。
記錄您的下一偉大的步驟…
解決方法是使用一個特殊類型的偵聽器,我們稱之為 Recorder (記錄器)。記錄器保存它們所注冊的每個對象上的每個方法調用。
記錄器按方法被調用的順序線性地保存這些記錄,這非常象一個盒式磁帶。通過將相同的 Recorder 安裝到每一個對象中,可以檢查這些對象中方法的調用順序。請考慮下面的代碼:
清單 2. 在每個對象中安裝相同的 Recorder
public class Recorder {
private StringBuffer tape;
public Recorder() {
this.tape = new StringBuffer();
}
public String playBack() {
return tape.toString();
}
public void record(String s) {
tape.append(s);
}
}
public class Greeter {
private Recorder r;
public Greeter(Recorder _r) {
this.r = _r;
}
public void sayHello() {
r.record("sayHello();");
...
}
public void sayGoodbye() {
r.record("sayGoodbye();");
...
}
}
public class Sender {
private Recorder r;
public Sender(Recorder _r) {
this.r = _r;
}
public void sendFirstMessage() {
r.record("sendFirstMessage();");
...
}
public void sendSecondMessage() {
r.record("sendSecondMessage();");
...
}
}
用 String 還是 ToString
注意 Sender 和 Greeter 中的每一個方法必須向 Recorder 通報新方法的調用。在這種方法下,記錄器就像其他偵聽器一樣:任何改變發生時都必須通知它們。
另外,注意被傳送到 Recorder 的消息是一個簡單的 String 。使用 String 消息有利也有弊。一方面,一個較復雜的對象在每一種方法調用時都能被保存下來,提供更詳細的信息。另一方面,這麼復雜的對象將使得測試工作更加困難。
例如,使用清單 2 中的記錄器,只要把下面簡單的測試加到我們的套件中,就能決定調用的順序:
清單 3. 一個 JUnit 測試
public void testOrderOfInvocation() throws InterruptedException {
Recorder r = new Recorder();
Greeter g = new Greeter(r);
Sender s = new Sender(r);
Coordinator c = new Coordinator(s, g);
c.start();
c.join();
assertEquals("sayHello();sendFirstMessage();sendSecondMessage();
sayGoodbye();",r.playBack());
}
由於我們已經將消息保存為簡單的 String 型變量 s,檢查 playBack 的內容的測試就很簡單:只要寫出正確的 String ,然後與之對照檢查就可以了。
另一方面,如果我們已經使用了一個較復雜類型的對象,我們不得不為這些對象中的每一個都構造一個同樣的實例,並重述所有記錄過的實例,檢查它們之中每一個的等同性。另外,這還需要我們為記錄過的對象的每個類寫一個 equals 方法。
像這樣的話,一次測試要做很多工作。我不知道您怎麼樣,但是我寧願把時間花在寫更多較簡單的測試上(設計代碼使得它們更容易),而不願為我的測試寫基礎結構的代碼。
這兩種方法間的一個折衷是制作另一個 Recorder ,它可以存儲非常復雜的數據,但有一個簡單的 toString 方法可用來測試,就如上面提到的那個。於是,較復雜的數據可用於其他的測試,檢查調用序列的詳細屬性。
准備好測試
用 Recorder 進行測試的思想可應用到許多類型的測試中:
除檢查調用的簡單順序之外,記錄器可在分布環境中使用,確保通信中不同的不變量在相互通信過程中保持不變。
記錄器也可用來和 GUI 一起確保響應各種預期的用戶操作。
簡而言之,記錄器提供了一種測試組件集合體的方法,它比大多數單元測試覆蓋的范圍大,但還是比整個系統小。我希望您能和我一樣,覺得它有用。