程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> 追求代碼質量 - 可重復的系統測試

追求代碼質量 - 可重復的系統測試

編輯:關於JAVA

在本質上,像 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 應用程序通信,並試圖創建一個基於該交互的小部件。 該測試隨後校驗此部件是否被成功創建。讀過本系列之前部分的讀者們也許會注 意到該測試的一個微妙的可重復性問題。您注意到了嗎?如果這個測試用例連續 運行兩次會怎樣呢?

由這個小部件實例(即,widget-id)的驗證方面可以判斷出,可以安全地做 出這樣的假設,即此應用程序中的數據庫約束很可能會阻止創建一個已經存在的 額外的小部件。由於缺少了一個在運行另一個測試前刪除此測試用例的目標小部 件的過程,如果再連續運行兩次,這個測試用例非常有可能會失敗。

幸運的是,如前面文章中所探討的那樣,有一個有助於數據庫-依賴性 (database-dependent)測試用例可重復性的機制 —— 即 DbUnit。

使用 DbUnit

改進 清單 1 中的測試用例來使用 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 和 清單 2 中定義的測試用例稱 為系統測試。因為系統測試運行安裝完整的應用程序,如 Web 應用程序,它們 通常包含一個 servlet 容器和一個相關聯的數據庫。這些測試的目的在於校驗 那些設計為端對端操作的外部接口(如 Web 應用程序中的 Web 頁面)。

彈性優先級
作為總體規則,應在任何可能的時候避免測試用例繼 承。許多 JUnit 擴展框架都提供特定的可繼承測試用例,以便利於測試一個特 定的架構。然而由於 Java™ 平台的單一繼承范例,使得從框架中繼承類 的測試用例飽受缺乏彈性之苦。通常,這些相同的 JUnit 擴展框架提供了代理 API,這使得聯合各種不具有嚴格繼承結構的框架變得十分簡單。

由於設 計它們的目的是為了測試功能完整的應用程序,因而系統測試趨向於增加運行次 數而不是減少設置測試的總時間。例如,清單 1 和 清單 2 中展示的邏輯測試 在運行前 需要下列步驟:

創建一個 war 文件,該文件包含所有相關 Web 內容,如 JSP 文件、servlet、第三方的 jar 文件、圖像等。

將此 war 文件部署到目標 Web 容器中。(如果該容器尚未啟動,啟動該容 器。)

啟動任何相關的數據庫。(如果需要更新數據庫模式,在啟動前進行更新。 )

現在,對於一個微不足道的小測試要做大量的輔助性工作!如果證明這個過 程是耗時的,那麼您認為這個測試會間隔多長時間運行一次呢?面對要使系統測 試在邏輯上可重復(在一個連續的集成環境中)這一需求,這個步驟列表的確令 人望而生畏。

介紹 Cargo

好消息是可以在之前的列表中使所有主要設置步驟自動化。事實上,如果恰 好從事過 Java Web 開發,可能已經用 Ant、Maven 或其他構建工具使步驟 1 自動化了。

步驟 2 卻是一個有趣的障礙。自動化一個 Web 容器還是需要一定技巧的。 例如,一些容器具有定制的 Ant 任務,這些任務方便了其自動部署及運行,但 這些任務是特定於容器的。而且,這些任務還有一些假設,如容器的安裝位置, 還有更重要的是,容器已被安裝。

Cargo 是一個致力於以通用方式自動化容器管理的創新型開源項目,因而用 於將 WAR 文件部署到 JBoss 的相同的 API 也能夠啟動及停止 Jetty。Cargo 也能自動下載並安裝一個容器。可以以不同的方式利用 Cargo 的 API,從 Java 代碼到 Ant 任務,再到 Maven 目標。

運用一個如 Cargo 這樣的工具,應對了在編寫合乎邏輯可重復的測試用例中 遇到的主要問題之一。另外,還可以構造一個構建用於駕馭 Cargo 的功能以 自 動地完成下列任務:

下載一個所期望的容器。

安裝該容器。

啟動該容器。

將一個選定的 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 和清單 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 中定義的測試用例就位於這 個目錄下。

清單 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 中,完整地配置了 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 應用程序的可 重復測試的負擔。

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