# 追求代碼質量: 可重復的系統測試
_用 Cargo 進行自動容器管理_
在測試加入到 servlet 容器的 Web 應用程序時,編寫符合邏輯的可重復的測試尤其需要技巧。在 Andrew Glover 的提高代碼質量的這個續篇中,他介紹了 Cargo,這是一個以通用方式自動化容器管理的開源框架,有了這個框架,您可以隨時編寫符合邏輯的可重復的系統測試。
在本質上,像 JUnit 和 TestNG 一樣的測試框架方便了可重復性測試的創建。由于這些框架利用了簡單 Boolean 邏輯(以 _assert_ 方法的形式)的可靠性,這使得無人為干預而運行測試成為可能。事實上,自動化是測試框架的主要優點之一 —— 我能夠編寫一個用于斷言具體行為的相當復雜的測試,且一旦這些行為有所改變,框架就會報告一個人人都能明白的錯誤。
利用成熟的測試框架會帶來_框架_ 可重復性的優點,這是顯而易見的。但_邏輯的_ 可重復性卻取決于您。例如,考慮創建用于驗證 Web 應用程序的可重復測試的情況,一些 JUnit 擴展框架(如 JWebUnit 和 HttpUnit)在協助自動化的 Web 測試方面非常好用。但是,使測試的 _plumbing_ 可重復則是開發人員的任務,而這在部署 Web 應用程序資源時很難進行。
實際的 JWebUnit 測試的構造過程相當簡單,如清單 1 所示:
##### 清單 1\. 一個簡單的 JWebUnit 測試
```
package test.come.acme.widget.Web;
import net.sourceforge.jwebunit.WebTester;
import junit.framework.TestCase;
public class WidgetCreationTest extends TestCase {
private WebTester tester;
protected void setUp() throws Exception {
this.tester = new WebTester();
this.tester.getTestContext().
setBaseUrl("http://localhost:8080/widget/");
}
public void testWidgetCreation() {
this.tester.beginAt("/CreateWidget.html");
this.tester.setFormElement("widget-id", "893-44");
this.tester.setFormElement("part-num", "rt45-3");
this.tester.submit();
this.tester.assertTextPresent("893-44");
this.tester.assertTextPresent("successfully created.");
}
}
```
這個測試與一個 Web 應用程序通信,并試圖創建一個基于該交互的小部件。該測試隨后校驗此部件是否被成功創建。讀過本系列之前部分的讀者們也許會注意到該測試的一個微妙的可重復性問題。您注意到了嗎?如果這個測試用例_連續_ 運行兩次會怎樣呢?
## 改進代碼質量
不要錯過了 Andrew 的附隨的 [討論論壇](http://www.ibm.com/developerworks/forums/dw_forum.jsp?S_TACT=105AGX52&cat=10&S_CMP=cn-a-j&forum=812),獲取有關代碼編寫方法、測試框架及編寫高質量代碼的幫助。
由這個小部件實例(即,`widget-id`)的驗證方面可以判斷出,可以安全地做出這樣的假設,即此應用程序中的數據庫約束很可能會阻止創建一個已經存在的額外的小部件。由于缺少了一個在運行另一個測試前刪除此測試用例的目標小部件的過程,如果再連續運行兩次,這個測試用例非常有可能會失敗。
幸運的是,如前面文章中所探討的那樣,有一個有助于數據庫-依賴性(database-dependent)測試用例可重復性的機制 —— 即 DbUnit。
## 使用 DbUnit
改進 [清單 1](#code1) 中的測試用例來使用 DbUnit 是非常簡單的。DbUnit 只需要一些插入數據庫的數據和一個相應的數據庫連接,如清單 2 所示:
##### 清單 2\. 用 DbUnit 進行的數據庫-依賴性測試
```
package test.come.acme.widget.Web;
import java.io.File;
import java.io.IOException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import org.dbunit.database.DatabaseConnection;
import org.dbunit.database.IDatabaseConnection;
import org.dbunit.dataset.DataSetException;
import org.dbunit.dataset.IDataSet;
import org.dbunit.dataset.xml.FlatXmlDataSet;
import org.dbunit.operation.DatabaseOperation;
import net.sourceforge.jwebunit.WebTester;
import junit.framework.TestCase;
public class RepeatableWidgetCreationTest extends TestCase {
private WebTester tester;
protected void setUp() throws Exception {
this.handleSetUpOperation();
this.tester = new WebTester();
this.tester.getTestContext().
setBaseUrl("http://localhost:8080/widget/");
}
public void testWidgetCreation() {
this.tester.beginAt("/CreateWord.html");
this.tester.setFormElement("widget-id", "893-44");
this.tester.setFormElement("part-num", "rt45-3");
this.tester.submit();
this.tester.assertTextPresent("893-44");
this.tester.assertTextPresent("successfully created.");
}
private void handleSetUpOperation() throws Exception{
final IDatabaseConnection conn = this.getConnection();
final IDataSet data = this.getDataSet();
try{
DatabaseOperation.CLEAN_INSERT.execute(conn, data);
}finally{
conn.close();
}
}
private IDataSet getDataSet() throws IOException, DataSetException {
return new FlatXmlDataSet(new File("test/conf/seed.xml"));
}
private IDatabaseConnection getConnection() throws
ClassNotFoundException, SQLException {
Class.forName("org.hsqldb.jdbcDriver");
final Connection jdbcConnection =
DriverManager.getConnection("jdbc:hsqldb:hsql://127.0.0.1",
"sa", "");
return new DatabaseConnection(jdbcConnection);
}
}
```
加入了 DbUnit,測試用例真的是可重復的了。在 `handleSetUpOperation()` 方法中,每當運行一個測試用例時,DbUnit 對數據執行一個 `CLEAN_INSERT`。此操作本質上將一個數據庫的數據清空并插入一個新的數據集,從而刪除任何之前創建的小部件。
## 再一次探討什么是 DbUnit?
DbUnit 是一個 JUnit 擴展,用于在運行測試時將數據庫放入一個已知狀態中。開發人員使用 XML 種子文件將特定數據插入到測試用例所依賴的數據庫中。因而,DbUnit 便利了依賴于一個或多個數據庫的測試用例的可重復性。
但那并不意味著已經結束了對測試用例可重復性這一話題的探討。事實上,一切才剛剛開始。
## 重復系統測試
我喜歡將 [清單 1](#code1) 和 [清單 2](#code2) 中定義的測試用例稱為_系統測試_。因為系統測試運行安裝完整的應用程序,如 Web 應用程序,它們通常包含一個 servlet 容器和一個相關聯的數據庫。這些測試的目的在于校驗那些設計為端對端操作的外部接口(如 Web 應用程序中的 Web 頁面)。
## 彈性優先級
作為總體規則,應在任何可能的時候避免測試用例繼承。許多 JUnit 擴展框架都提供特定的可繼承測試用例,以便利于測試一個特定的架構。然而由于 Java? 平臺的單一繼承范例,使得從框架中繼承類的測試用例飽受缺乏彈性之苦。通常,這些相同的 JUnit 擴展框架提供了代理 API,這使得聯合各種不具有嚴格繼承結構的框架變得十分簡單。
由于設計它們的目的是為了測試功能完整的應用程序,因而系統測試趨向于增加運行次數而不是減少設置測試的總時間。例如,[清單 1](#code1) 和 [清單 2](#code2) 中展示的邏輯測試在運行_前_ 需要下列步驟:
1. 創建一個 war 文件,該文件包含所有相關 Web 內容,如 JSP 文件、servlet、第三方的 jar 文件、圖像等。
2. 將此 war 文件部署到目標 Web 容器中。(如果該容器尚未啟動,啟動該容器。)
3. 啟動任何相關的數據庫。(如果需要更新數據庫模式,在啟動前進行更新。)
現在,對于一個微不足道的小測試要做大量的輔助性工作!如果證明這個過程是耗時的,那么您認為這個測試會間隔多長時間運行一次呢?面對要使系統測試在邏輯上可重復(在一個連續的集成環境中)這一需求,這個步驟列表的確令人望而生畏。
* * *
## 介紹 Cargo
好消息是可以在之前的列表中使所有主要設置步驟自動化。事實上,如果恰好從事過 Java Web 開發,可能已經用 Ant、Maven 或其他構建工具使步驟 1 自動化了。
步驟 2 卻是一個有趣的障礙。自動化一個 Web 容器還是需要一定技巧的。例如,一些容器具有定制的 Ant 任務,這些任務方便了其自動部署及運行,但這些任務是特定于容器的。而且,這些任務還有一些假設,如容器的安裝位置,還有更重要的是,_容器_已被安裝。
Cargo 是一個致力于以通用方式自動化容器管理的創新型開源項目,因而用于將 WAR 文件部署到 JBoss 的相同的 API 也能夠啟動及停止 Jetty。Cargo 也能自動下載并安裝一個容器。可以以不同的方式利用 Cargo 的 API,從 Java 代碼到 Ant 任務,再到 Maven 目標。
運用一個如 Cargo 這樣的工具,應對了在編寫合乎邏輯可重復的測試用例中遇到的主要問題之一。另外,還可以構造一個構建用于駕馭 Cargo 的功能以 _自動地_完成下列任務:
1. 下載一個所期望的容器。
2. 安裝該容器。
3. 啟動該容器。
4. 將一個選定的 WAR 或 EAR 文件部署到該容器中。
很簡單,是吧?接下來,您還能夠用 Cargo 停止一個選定的容器。
### “談談” Cargo
在深入 Cargo 前,最好先了解一下 Cargo 的基礎知識。也就是說,由于 Cargo 與容器及容器管理相關,所以要理解了容器及容器管理的有關概念。
對于新手,顯然要先了解_容器_ 的概念。容器是用以寄存應用程序的服務器。應用程序可以是基于 Web 的,基于 EJB 的,或基于這兩者的,這就是為什么有 Web 容器和 EJB 容器的原因。Tomcat 是 Web 容器,而 JBoss 則會被認為是 EJB 容器。因此,Cargo 支持相當多的容器,但在我的例子中,我將使用 Tomcat 5.0.28 版。(Cargo 將稱其為“tomcat5x”容器。)
接下來,如果尚未安裝容器,可以使用 Cargo 來下載并安裝一個特定的容器。為此,需要提供給 Cargo 一個下載 URL。一旦安裝了容器,Cargo 也會允許使用_配置選項_ 來對其進行配置。這些選項以名稱-值對的形式存在。
最后,要介紹_可部署資源_ 的概念,在我的例子中即 WAR 文件。請注意 EAR 文件也是一樣的簡單。
將這些概念記住,讓我們來看一下可以用 Cargo 來完成什么任務。
* * *
## Cargo 實踐
本文中的例子涉及到在 Ant 中使用 Cargo,這就必需將之前定義的系統測試和 Cargo Ant 任務包裝在一起。這些任務隨后安裝、啟動、部署并停止容器。我們將首先進行安裝設置,運行測試然后停止容器。
在 Ant 構建中使用 Cargo 所需的第一步是提供一個針對所有的 Cargo 任務的任務定義。這一步允許隨后在構建文件中引用 Cargo 任務。應付這一步有很多的方法。清單 3 簡單地裝載了來自 Cargo JAR 文件中的屬性文件的任務:
##### 清單 3\. 在 Ant 中裝載所有的 Cargo 任務
```
<taskdef resource="cargo.tasks">
<classpath>
<pathelement location="${libdir}/${cargo-jar}"/>
<pathelement location="${libdir}/${cargo-ant-jar}"/>
</classpath>
</taskdef>
```
一但定義了 Cargo 的任務,真正的行動就開始了。清單 4 定義了下載、安裝及啟動 Tomcat 容器的 Cargo 任務。`zipurlinstaller` 任務將 Tomcat 從 `http://www.apache.org/dist/tomcat/tomcat-5/v5.0.28/bin/ jakarta-tomcat-5.0.28.zip` 中下載并安裝到一個本地臨時目錄中。
##### 清單 4\. 下載并啟動 Tomcat 5.0.28
```
<cargo containerId="tomcat5x" action="start"
wait="false" id="${tomcat-refid}">
<zipurlinstaller installurl="${tomcat-installer-url}"/>
<configuration type="standalone" home="${tomcatdir}">
<property name="cargo.remote.username" value="admin"/>
<property name="cargo.remote.password" value=""/>
<deployable type="war" file="${wardir}/${warfile}"/>
</configuration>
</cargo>
```
請注意要想如您所愿,從不同的任務中啟動和停止一個容器,必需將容器同一個惟一的 id 聯系起來,此 id 是 `cargo` 任務的 `id="${tomcat-refid}"`。
還要注意的是,Tomcat 的配置是在 `cargo` 任務內處理的。在 Tomcat 中,必需設置 `username` 和 `password` 屬性。最后,使用 `deployable` 元素定義一個指向 WAR 文件的指針。
### Cargo 屬性
Cargo 任務中用到的所有屬性都顯示在清單 5 中。例如,`tomcatdir` 定義 Tomcat 將安裝的兩個位置中的一個。這個特別的位置是一個鏡像結構,該位置將被實際下載并安裝的 Tomcat 實例(在臨時目錄中找到的)所引用。`tomcat-refid` 屬性則幫助將容器中惟一的實例與其鏡像關聯起來。
##### 清單 5\. Cargo 屬性
```
<property name="tomcat-installer-url"
value="http://www.apache.org/dist/tomcat/tomcat-5/v5.0.28/bin/
jakarta-tomcat-5.0.28.zip"/>
<property name="tomcatdir" value="target/tomcat"/>
<property name="tomcat.username" value="admin"/>
<property name="tomcat.passwrd" value=""/>
<property name="wardir" value="target/war"/>
<property name="warfile" value="words.war"/>
<property name="tomcat-refid" value="tmptmct01"/>
```
為停止一個容器,可以定義一個引用 `tomcat-refid` 屬性的任務,如清單 6 所示。
##### 清單 6\. 按 Cargo 方式停止容器
```
<cargo containerId="tomcat5x" action="stop"
refid="${tomcat-refid}"/>
```
### 用 Cargo 封裝
清單 7 將 [清單 4](#code4) 和清單 6 中的代碼聯合起來,用兩個 Cargo 任務封裝了一個測試目標:一個用于啟動 Tomcat,另一個用于停止 Tomcat。`antcall` 任務調用在清單 8 中定義的名為 `_run-system-tests` 的目標。
##### 清單 7\. 用 Cargo 封裝測試目標
```
<target name="system-test" if="Junit.present"
depends="init,junit-present,compile-tests,war">
<cargo containerId="tomcat5x" action="start"
wait="false" id="${tomcat-refid}">
<zipurlinstaller installurl="${tomcat-installer-url}"/>
<configuration type="standalone" home="${tomcatdir}">
<property name="cargo.remote.username" value="admin"/>
<property name="cargo.remote.password" value=""/>
<deployable type="war" file="${wardir}/${warfile}"/>
</configuration>
</cargo>
<antcall target="_run-system-tests"/>
<cargo containerId="tomcat5x" action="stop"
refid="${tomcat-refid}"/>
</target>
```
清單 8 定義測試目標,稱作 `_run-system-tests`。請注意此任務_只_ 運行置于 `test/system` 目錄下的系統測試。例如,[清單 2](#code2) 中定義的測試用例就位于這個目錄下。
##### 清單 8\. 通過 Ant 運行 JUnit
```
<target name="_run-system-tests">
<mkdir dir="${testreportdir}"/>
<junit dir="./" failureproperty="test.failure"
printSummary="yes" fork="true"
haltonerror="true">
<sysproperty key="basedir" value="."/>
<formatter type="xml"/>
<formatter usefile="false" type="plain"/>
<classpath>
<path refid="build.classpath"/>
<pathelement path="${testclassesdir}"/>
<pathelement path="${classesdir}"/>
</classpath>
<batchtest todir="${testreportdir}">
<fileset dir="test/system">
<include name="**/**Test.java"/>
</fileset>
</batchtest>
</junit>
</target>
```
在 [清單 7](#code7) 中,完整地配置了 Ant 構建文件,從而將系統測試與 Cargo 部署封裝在一起。清單 7 中的代碼確保了清單 8 中 `test/system` 目錄下的所有系統測試都是邏輯上可重復的。可以在任何時間里在任何機器上運行這些系統測試,對于連續集成環境尤佳。該測試對容器未做任何假設 —— 未對位置做假設,甚至未對其是否運行做假設!(當然,這些測試仍做了一個假設,我沒有強調,即潛在的數據庫是配置良好且在運行中的。但那又是另一個要討論的主題了。)
* * *
## 可重復的結果
在清單 9 中,可以看到工作的成果。當將 `system-test` 命令發布到 Ant 構建后,就會執行系統測試。Cargo 處理管理所選容器的所有細節,不需要對測試環境作出絕對重復性假設。
##### 清單 9\. 增強的構建
```
war:
[war] Building war: C:\dev\projects\acme\target\widget.war
system-test:
_run-system-tests:
[mkdir] Created dir: C:\dev\projects\acme\target\test-reports
[junit] Running test.come.acme.widget.Web.RepeatableWordCreationTest
[junit] Tests run: 1, Failures: 0, Errors: 0, Time elapsed: 4.53 sec
[junit] Testcase: testWordCreation took 4.436 sec
BUILD SUCCESSFUL
Total time: 1 minute 2 seconds
```
請記住,Cargo 也在 Maven 構建中起作用。另外,從正常的應用程序到測試用例,Cargo Java API 都有助于容器的程序化管理。且 Cargo 不僅適用于 JUnit(盡管樣例代碼是用 JUnit 寫的),TestNG 用戶將會很高興地了解到 Cargo 對其測試套件也起作用。事實上,測試用什么編寫并不重要,重要的是將它們同 Cargo 封裝起來,容器管理問題就會迎刃而解!
* * *
## 結束語
您的測試是否在邏輯上可重復由您來決定,但是通過本文您確實看到 Cargo 的確很有用處。Cargo 管理容器環境,所以您就可以不用管理。將 Cargo 包含到您的測試例程中 —— 這毫無疑問會減輕您構造用于驗證 Web 應用程序的可重復測試的負擔。
- 追求代碼質量
- 追求代碼質量: 對 Ajax 應用程序進行單元測試
- 追求代碼質量: 使用 TestNG-Abbot 實現自動化 GUI 測試
- 追求代碼質量: 用 AOP 進行防御性編程
- 追求代碼質量: 探究 XMLUnit
- 追求代碼質量: 用 JUnitPerf 進行性能測試
- 追求代碼質量: 通過測試分類實現敏捷構建
- 追求代碼質量: 可重復的系統測試
- 追求代碼質量: JUnit 4 與 TestNG 的對比
- 追求代碼質量: 馴服復雜的冗長代碼
- 追求代碼質量: 用代碼度量進行重構
- 追求代碼質量: 軟件架構的代碼質量
- 讓開發自動化: 除掉構建腳本中的氣味
- 追逐代碼質量: 決心采用 FIT
- 追求代碼質量: 不要被覆蓋報告所迷惑