レベル: 初級 Andrew Glover (aglover@stelligent.com), President, Stelligent Incorporated
2006年 9月 26日 論理的再現性を持つテストの作成は、特にサーブレット・コンテナーを組み込む Web アプリケーションのテストとなると、とりわけ困難です。この記事では、コード品質の改善を追求し続ける Andrew Glover がコンテナー管理を汎用手順で自動化する Cargo を紹介します。このオープン・ソース・フレームワークによって、毎回論理的に反復可能なシステム・テストを作成できるようになります。
JUnit や TestNG などのテスト・フレームワークではその性質上、反復可能テストの作成が容易にできます。これらのフレームワークは、単純なブール論理の信頼性を (アサート・メソッドという形で) 利用するため、人の手を借りずにテストを実行できます。実際、自動化はテスト・フレームワークの第一の利点として挙げられます。特定の動作を表明するかなり複雑なテストを作成することも可能で、これらの動作が変更された場合には、フレームワークが誰でも解釈できるエラーをレポートします。
十分に完成されたテスト・フレームワークを使用すると、フレームワークの再現性がそのまますぐに活用されます。ただし論理的再現性となると、それsはあなたの腕次第です。例えば、Web アプリケーションを検証する反復可能テストを作成する場合を考えてみてください。いくつかの JUnit 拡張フレームワーク (JWebUnit、HttpUnit など) は、自動 Web テストの簡易化に卓越していますが、テストを反復可能な構成にするのは開発者の仕事です。さらに、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 アプリケーションとやりとりし、その対話に基づいてウィジェットを作成しようと試みます。次に、ウィジェットが正常に作成されたかどうかを検証します。この連載のこれまでの記事を読んだ方なら、このテストに関する微妙な再現性の問題に気付くことでしょう。何だかおわかりですか。ヒントは、このテスト・ケースが 2 回続けて実行された場合にどうなるかです。
ウィジェト・インスタンスの ID (widget-id) という点から判断すると、アプリケーションのデータベース制約によって、既存のウィジェットをもう一つ作成することはできないと考えて間違いはありません。別のテストを実行する前にテスト・ケースの対象ウィジェットを除去するプロセスを抜かすと、テスト・ケースを 2 回続けて実行した場合にそのテスト・ケースが失敗する可能性が高くなります。
幸い、以前の記事で説明したように、データベース依存テストの再現性を手助けするメカニズムがあります。それは、DbUnit です。
DbUnit の出番
リスト 1 のテスト・ケースを拡張して DbUnit を取り込むのは極めて簡単なことです。 リスト 2 に示すように、DbUnit に必要なのはデータベースに挿入するデータと、該当するデータベース接続だけです。
リスト 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 つ以上のデータベースに依存するテスト・ケースを容易に再現できるようにします。
|
|
ただし、これでテスト・ケースの再現性についての話題を締めくくったわけではありません。実際、ここからが本題です。
システム・テストを繰り返す
リスト 1 とリスト 2 に定義したテスト・ケースは、私がシステム・テストと呼んでいるものです。システム・テストは、Web アプリケーションなどの完全にインストールされたアプリケーションを使うため、通常はサーブレット・コンテナー、そして関連データベースを組み込みます。このようなテストはどちらかと言えば、外部インターフェース (Web アプリケーションの場合は、Web ページなど) が始めから終わりまで設計通りに動作することを検証します。
 |
柔軟性の優先
一般的な経験則として、テスト・ケースの継承はできるだけ避けてください。多数の JUnit 拡張フレームワークには、特定アーキテクチャーのテストを容易にするために継承可能な特殊化されたテスト・ケースが用意されています。ですが、フレームワークからのクラスを継承するテスト・ケースは、Java™ プラットフォームの単一継承というパラダイムにより柔軟性を損ねることになります。たいての場合、これらの JUnit 拡張フレームワークには委譲 API があり、これによって、固定された継承構造を引き継がずに簡単に各種フレームワークを組み合わせることができます。
|
|
システム・テストは完全に機能するアプリケーションのテストを目的に設計されているため、テストのセットアップ時間を一切入れないとしても、実行するにはかなりの時間がかかる傾向にあります。例えば、リスト 1 とリスト 2 に示した論理テストには、実行する段階に至るまでに以下の手順が必要になります。
- すべての関連 Web 成果物 (JSP ファイル、サーブレット、サードパーティーの jar ファイル、画像など) が含まれる war ファイルを作成する。
- war ファイルを対象の Web コンテナーにデプロイする (コンテナーが起動していない場合は起動する)。
- すべての関連データベースを起動する (データベースのスキーマを更新する必要がある場合は、起動する前に更新する)。
たった 1 つのテストに、これだけの下準備が必要になります。このプロセスに多大な時間がかかるものだとわかったら、はたしてこのテストを何回実行することになると思いますか。システム・テストを (例えば、継続的インテグレーション環境で) 論理的に反復可能にするために必要な作業だとしても、この手順のリストを見ると当然気後れしてしまいます。
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 との関連におけるコンテナーとコンテナー管理の概念を理解しておかなければなりません。
まず最初に、コンテナーという概念があります。コンテナーは、アプリケーションをホストするサーバーです。ホストするアプリケーションは、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 タスクが、http://www.apache.org/dist/tomcat/tomcat-5/v5.0.28/bin/ jakarta-tomcat-5.0.28.zip からローカル一時ディレクトリーに Tomcat をダウンロードしてインストールします。
リスト 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 を関連付けなければならない (関連付けたくなるでしょうが) ことに注意してください。それが、cargo タスクの id="${tomcat-refid}" です。
もう一つの注意事項は、Tomcat の構成プロパティーは cargo タスク内で扱われるということです。Tomcat の場合、username および password プロパティーを設定する必要があります。最後に、deployable 要素を使って WAR ファイルへのポインターを定義します。
Cargo のプロパティー
リスト 5 に、Cargto タスクで使用されているすべてのプロパティーを示します。例えば tomcatdir は、2 つの場所のうちのどちらから 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"/>
|
コンテナーを停止するには、リスト 6 に示すように、tomcat-refid プロパティーを参照するタスクを定義します。
リスト 6. Cargo によるコンテナーの停止
<cargo containerId="tomcat5x" action="stop"
refid="${tomcat-refid}"/>
|
Cargo でラップする
リスト 7 では、テスト・ターゲットを 2 つの Cargo タスクでラップして、リスト 4 とリスト 6 のコードを組み合わせています。タスクの一方は Tomcat を起動し、もう一方は停止します。antcall タスクは _run-system-tests という名前のターゲットを呼び出します。このターゲットは、リスト 8 で定義しています。
リスト 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>
|
テスト・ターゲット _run-system-tests は、以下のリスト 8 で定義されます。このタスクは、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 で、Cargo のデプロイメント魔術でシステム・テストをラップする Ant ビルド・ファイルを完全に構成しました。リスト 7 のコードによって、リスト 8 の test/system ディレクトリーにあるシステム・テストはすべて論理的に反復可能になります。これらのシステム・テストは任意のマシンでいつでも実行できるため、継続的インテグレーション環境にはまさに最適です。これらのテストは、コンテナーの場所どころか、コンテナーが実行中であるかどうかなどの前提を一切設けません。(もちろん、これらのテストにはまだ 1 つの前提が残されています。つまり、基礎となるデータベースが適切に構成され、実行中であるという前提です。これについては別の機会に説明します。)
再現可能な結果
これまでの作業結果をリスト 9 で見てください。Ant ビルドに対して system-test コマンドを発行すると、システム・テストが実行されます。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 によって、標準アプリケーションからテスト・ケースに至るまでのすべてでコンテナーのプログラマチックな管理が容易になります。そしてもう一つ忘れてはならないのは、この記事のコード・サンプルは JUnit で作成していますが、Cargo は JUnit 専用ではないということです。TestNG ユーザーには喜ばしいことに、Cargo は TestNG のテスト・ケースでも同じように機能します。実際、テストが何で作成されているかは問題ではありません。テストを Cargo でラップするだけで、コンテナー管理はすぐさま自動化されます。
まとめ
テストを論理的に反復可能にするのは開発者の仕事ですが、今月の記事を読んで、Cargo がこの仕事に大いに役立つことがわかったはずです。コンテナー環境の管理は Cargo に任せられます。Cargo をテスト・ルーチンに組み込めば、Web アプリケーション検証用の反復可能テストを作成する負担が軽くなること間違いありません。
参考文献 学ぶために
製品や技術を入手するために
議論するために
著者について  | 
|  | Andrew Gloverは合衆国ワシントン特別区にある、Vanward TechnologiesのCTO(最高技術責任者)です。Vanward Technologiesは自動化テスト・フレームワークの構築を専門としており、ソフトウェアのバグ発生数や統合時間やテスト時間の減少、また全体的なコード安定性改善に貢献しています。
|
記事の評価
|