JXUnit

XMLでテスト・データ・セットを作成する

Release 2.0.0
18 April 2001

日本語訳
masaki@metabolics.co.jp
2001/4/26

データ中心テスト: テスト・コードからテスト・データを分離することの利点

テスト・データはテストにおいて中心的な役割を果たすものだ。最も低いレベルのテスト(メソッド・テスト)から最も高いレベルのテスト(検収テスト)まで、入力データ、出力されるはずのデータ、出力されるはずのデータと実際の出力データの比較がテストにはついて回る。

テスト・コードからテスト・データを分離することにはいくつかの利点がある:

JXUnit

JXUnitはディレクトリ・ベースのテスト記述システムで、JUnitの上に構築されている。JXUnitの操作は以下の三つの段階からなる:

  1. カレント・ディレクトリおよびその下のすべてのディレクトリからtest.jxuファイルを検索する。それぞれのtest.jxuファイルごとに、一種類のテストを行うのに必要なステップが書いてある。
  2. test.jxuファイルが見つかったディレクトリごとに、test.jxucファイルがあるかどうかを調べる。もしあれば、そのtest.jxucファイルにはテストに使うテスト・データのディレクトリ・ツリーとファイル・フィルタが書いてある。
  3. 最後にテストを走らせる。

変更点

これはJXUnitの最初の製品リリースである。インストールを簡単にするためにパッケージを変更した(必要なjarファイルはすべて入れるようにした)。

新しいフィーチャ:

jxu.qimlファイルとjxuc.qimlファイルを、JXUnitのjarファイルに追加した。なので、いちいちテスト・ディレクトリにこれらのファイルをコピーする必要はなくなった。

さらにいくつかのドキュメントをQuick Wiki Wikiに追加した(Wiki Wikiとは見た人が変更することのできるwebページの集まりのこと) 。

テストのステップとテストの属性

テストは以下の小さなステップに分けて考えることができる:

テストの属性とはテストのステップ間で受け渡されるデータのことで、属性ごとに名前と値がある。

さて、最初の例を見れば我々がやろうとしていることがだいたい分かってもらえるのではないかと思う:

	<jxu>
		<set name="input" 
			value="123"/>
		<eval stepClass="DummyTestStep"/>
		<isEqual name="output" 
			value="123" 
			message="Output was not 123"/>
	</jxu>

ここでは、Java XML Unit(JXU)というXMLを使ってテストのステップの並びを記述している:

  1. inputという名前の属性を生成して、その値を"123"にする
  2. DummyTestStepを走らせる
  3. outputという名前の属性が生成されていて、その値が"123"であることをチェックする。もしそうなっていないならば、テストは失敗なのでメッセージ"Output was not 123"を出す。

テスト・フレームワーク

クラスnet.sourceforge.jxunit.JXTestCaseはJXUnitのフレームワーク全体を提供する。このクラスはjunit.framework.TestCaseをextendしたもので、JUnitから呼ばれるsuitクラスを持っている。

次のクラスnet.sourceforge.jxunit.JXPropertiesは、テストの属性のコンテナ・クラスである。このクラスはjava.util.HashMapをextendしている。

上の例のようなJXUテスト・スクリプトはオブジェクトの集合に変換される。上記のset, eval, isEqualに使われているのは以下のクラスである:

set net.sourceforge.jxunit.JXTestSet
eval net.sourceforge.jxunit.JXEval
isEqual net.sourceforge.jxunit.JXIsEqual
上記のDummyTestStepと同様、これらのクラスはすべてインタフェースnet.sourceforge.jxunit.JXTestStepを実装している:
	package net.sourceforge.jxunit;

	public interface JXTestStep
	{
		public void eval(JXTestCase testCase)
			throws Throwable;
	}

ではDummyTestStepのコードを見てみよう:

	import net.sourceforge.jxunit.*;

	public class DummyTestStep implements JXTestStep
	{
		public void eval(JXTestCase testCase)
			throws Throwable
		{
			JXProperties properties=testCase.getProperties();
			Object data=properties.get("input");
			properties.put("output",data);
		}
	}

上記コードのJXTestCaseで使われているgetPropertiesメソッドに注意してほしい。このメソッドで、テストのステップ間で引き渡されるデータへのアクセスを行っている。

JXU

JXUマークアップ言語は、テスト・ステップの並びからテストを生成するのに用いられる。JXUマークアップ言語とJXUnitクラスの関係は、QJMLドキュメントに記述している。例えば以下のQJMLの一部には、eval要素およびJXEvalクラスとの関係を記述している:

	<bean tag="eval">
		<rem>Create and run a test step.</rem>
		<implements>testStep</implements>
		<targetClass>net.sourceforge.jxunit.JXEval</targetClass>
		<attributes>
			<item coin="stepClass">
				<field name="stepClass"/>
			</item>
		</attributes>
	</bean>

	<text tag="stepClass">
		<rem>The fully qualified class name of the test step</rem>
	</text>

これを表形式で書くとすると次のようになる:

   element name
Java Class Name
Class Description
Attribute Name Description Java Variable Usage
   eval
net.sourceforge.jxunit.JXEval
Create and run a test step.
stepClass The fully qualified class name of the test step String stepClass Required


(ここにJXUのすべてを表した表と、そのJavaでの実装方法が書いてある)

JXUを拡張して、自分のクラスを入れるのは簡単である(ただしJXTestStepをimplementしている必要がある)。そのやり方は:

  1. jxu.qjmlファイルに自分のクラスとXMLからどうやってアクセスされるかの記述を追加する。(引数のないパブリックなコンストラクタが存在すること、同様に必要な変数はすべてパブリックにすること。)
  2. Quick4ツールqjml2qimlを使ってアップデートしたjxu.qjmlファイルをコンパイルする。その出力ファイルの名前はjxu.qimlにする。
  3. jarファイルJXUnitを展開する(jarファイルは単なるzipファイルである)。 jarファイルの中身はclassesディレクトリに展開される。
  4. classes\net\sourceforge\jxunitディレクトリのjxu.qimlファイルを置き換える。
  5. classpathからjarファイルJXUnitを削除して、さっきのクラス・ファイルの入ったディレクトリを追加する。(あるいはjarファイルを作り直してもよい。)

データ・ファイルを使う

JXUスクリプトの中で相対パス名が使われている場合には、その基点はいつも、アクティブな(=今使っている)test.jxuファイルのあるディレクトリになる。以下は例:

	<jxu>
		<set name="input" 
			file="myData.txt"/>
		<eval stepClass="DummyTestStep"/>
		<isEqual name="output" file="myData.txt"
			message="Dummy Test Failure: output does not match myData.txt"/>
	</jxu>

テスト・オブジェクト

単体テストを生成するときには、たいていテスト・オブジェクトを生成してメソッドやテスト中のサブ・システムに渡すものだ。いよいよ、テキストの文字列を適当なオブジェクトに変換して、テスト・ステップのクラス群を生成する番になった。しかしこのままではオブジェクトの複雑さが増すにつれて、無様なやり方となってしまう。

そこで複雑なデータ構造を表すためにXMLを使うことにする。JXUnitではこの構造をテスト・オブジェクトに変換することができる。例を見てみよう:

	<dataList>
		<dataItem>abc<dataItem>
		<dataItem>def<dataItem>
		<dataItem>g<dataItem>
		<dataItem>hi<dataItem>
	</dataList>

ここでやりたいことは、この構造をStringオブジェクトのListに変換することだ。XMLからJavaへのマッピングは以下のようになる:

   dataList
java.util.ArrayList
A list of String objects
Holds the subordinate dataItems
   dataItem
java.lang.String
A simple text value

下がこのマッピングを定義しているQJMLファイルだ:

	<qjml root="dataList">
		<bean tag="dataList">
			<rem>A list of String objects</rem>
			<targetClass>java.util.ArrayList</targetClass>
			<elements>
				<item coin="dataItem" repeating="true">
					<identity kind="list"/>
				</item>
			</elements>
		</bean>
		<text tag="dataItem">
			<rem>A simple text value</rem>
		</text>
	</qjml>

前と同様、Quick4ユーティリティqjml2qimlを使って、QJMLファイルをちゃんと使える形式に変換する。出力ファイルの名前はmySchema.qimlとする。このファイルを使うことによって、XMLファイルからテスト・オブジェクトを生成することができる。

	<jxu>
		<set name="input" 
			file="myData.xml" schema="mySchema.qiml"/>
		<eval stepClass="DummyTestStep"/>
		<isEqual name="output" schema="mySchema.qiml"
			file="myData.xml"
			message="Dummy Test Failure: myData.xml"/>
	</jxu>
  1. setステップでは、mySchema.qimlを使ってmyData.xmlの内容をinputという名前のオブジェクトに変換している。
  2. evalステップでは、前と同様にDummyTestStepを起動して、実際のテストを実行する。
  3. isEqualステップでは、まずmyScheme.qimlの束縛スキーマを使って出力オブジェクトをXML文字列に変換している。次に、その文字列とmyData.xmlファイルの内容を比較している。

実際に上のテストを走らせてみたら、おやまぁ何と言うことか、失敗してしまったかも知れない。その理由は、myData.xmlの中身とXMLに変換されたdataListが完全に一致しなければならないからだ。これを簡単に修正して動くようにするには:

	<jxu>
		<set name="input" 
			file="myData.xml" schema="mySchema.qiml"/>
		<save name="input" 
			file="genData.xml" schema="mySchema.qiml"/>
		<eval stepClass="DummyTestStep"/>
		<isEqual name="output" schema="mySchema.qiml"
			file="genData.xml"
			message="Dummy Test Failure: genData.xml"/>
	</jxu>

ここで、新たにsaveステップを追加した。saveは、入力オブジェクトからXMLファイルを生成する。こうすればisEqualはちゃんと動くはず!

テスト・データを生成する

テストが失敗したときには、データのコピーを保存しておくとよい。データを人間に読めるような形式で保存しておくともっとよいはずだ。以下のように:

	<jxu>
	    <set name="input" 
		 file="myData.xml" schema="mySchema.qiml"/>
	    <eval stepClass="DummyTestStep"/>
	    <ifEqual converse="true"
		     name="output" schema="mySchema.qiml"
		     file="myData.xml">
		<save name="output" schema="mySchema.qiml"
		      file="myData_.xml"/>
		<fail>Dummy Test Failure: myData.xml</fail>
	    </ifEqual>
	</jxu>
  1. setステップでは、inputという名前のオブジェクトを生成している。
  2. evalステップで、実際のテストを行う。
  3. ifEqualステップでは、出力オブジェクトをXMLに変換し、それをmyData.xmlの内容と比較している。もし内容が一致していないならば(convert="ture")以下の入れ子を実行する:
    1. saveステップでは、出力オブジェクトをXMLに変換して、myData_.xmlファイルに書き出す。
    2. failステップでは、テストを失敗として終了させる。
さあ、出力データを保存しておくだけでもデバッグの助けとなると思うが、これから入力データを作るのはもっといいと思う。保存したデータ・ファイルの名前を変えるだけで新しいテストにすることができる。

テスト・コンテキスト: 複数のデータ・ファイルで同じテストを走らせる

一つのテストを走らせるだけでは不十分ということはよくある。テストを何回も、複数のスレッドで、あるいは複数のテスト・ファイルで走らせたいと思うことがあるだろう。これをテストのコンテキストと呼ぶことにしよう。テストのコンテキストはtest.jxucファイルで定義する(.jxucのcはcontextのc)。このファイルは、アクティブなtest.jxuファイルがあるのと同じディレクトリに置いておかなければならない。

上で述べたように、ここでやりたいことはいっぱいある。まずは、複数のテスト・ファイルを使えるようにしよう。何と言ってもこれがデータ中心テストの核心だからだ。(ここに現在サポートしているJXUC要素を記述した表がある。)では例題のJXUCドキュメントを見てみると:

	<jxuc>
		<directoryScan dir="testDirectory">
			<includeFiles regexp=".txt$"/>
			<excludeFiles regexp="_.txt$"/>
		</directoryScan>
	</jxuc>

DirectoryScanにはディレクトリからファイルを選択するオプションがある。任意個のincludeFiles要素とexcludeFiles要素を置くことができる(includeFilesがなければ、デフォルトで選択されたディレクトリのすべてのファイルを選択する)。

Regular Expressionsを使い、ファイル名からファイルをフィルタリングする。上の例では、.txtで終わるすべてのファイル、ただし_.txtで終わるもの以外を選択している。

test.jxuで定義したテストは、選択されたファイルごとに一回ずつ走る。テストの名前としては選択されたファイルの絶対パス名が使われ、absDataFileNameという名前の属性としてテスト・ステップに渡される。単純なファイル名(パスを含まないもの)もdataFileNameという名前の属性に渡される。複数のテスト・ファイルを使う場合のtest.jxuファイルの例:

	<jxu>
		<set name="data" file="absDataFileName" indirect="true"/>
		<ifEqual converse="true" name="data" value="dataFileName" indirect="true">
			<save name="data" file="badFileName_.txt"/>
			<fail>Each test file must contain only its own name!</fail>
		</ifEqual>
	</jxu>
  1. data属性を、absDataFileName属性で指定されたファイルの内容に設定する。(indirectフラグによって、file属性がファイル名を指定している属性を示すものとなる)。
  2. data属性の値とdataFileName属性に名前が保持されているファイルとを比較する。(ここでもindirectフラグによって、value属性によって指定された名前の属性の値を比較に使うことになる)。もし二つの値が異なっていたら(converse="true")
    1. データ属性の値をbadFileName_.txtという名前のファイルに保存し、
    2. テストは失敗

テスト・コンテキスト: フレームワーク

JXUCの要素は以下のようにJavaのクラスにマッピングされる:

directoryScan net.sourceforge.jxunit.JXDirectoryScan
includeFiles net.sourceforge.jxunit.JXIncludeFiles
excludeFiles net.sourceforge.jxunit.JXExcludeFiles

テスト・コンテキストの定義に使用されるクラスはすべて、JXTestSetupインタフェースをimplementしなければならない:

	package net.sourceforge.jxunit;

	public interface JXTestSetup
	{
		public void setup(String cwd, JXProperties properties)
			throws Throwable;
	}

テスト・コンテキストのセットアップがいったん完了したら、candidateFilesという名前の属性にファイル名のリストが(nullでなければ)入っているはず。テストは、このリストに入っている名前のファイルに対してそれぞれ行われることになる。各テストは、セットアップ中に初期化されたそれぞれのJXPropertiesオブジェクトのコピーを使って行われる。

JXUと同じく、JXUCにも自分のクラスを入れて拡張することができる。そのためにはJXTestSetupをimplementする:

  1. jxuc.qjmlファイルに自分のクラスとXMLからどうやってアクセスされるかの記述を追加する。(引数のないパブリックなコンストラクタが存在すること、同様に必要な変数はすべてパブリックにすること。)
  2. Quick4ツールqjml2qimlを使ってアップデートしたjxuc.qjmlファイルをコンパイルする。その出力ファイルの名前はjxuc.qimlにする。
  3. jarファイルJXUnitを展開する(jarファイルは単なるzipファイルである)。 jarファイルの中身はclassesディレクトリに展開される。
  4. classes\net\sourceforge\jxunitディレクトリのjxuc.qimlファイルを置き換える。
  5. classpathからjarファイルJXUnitを削除して、さっきのクラス・ファイルの入ったディレクトリを追加する。(あるいはjarファイルを作り直してもよい。)

さあ始めよう

必要なものはすべてダウンロードしたファイルに入っている。このファイルをどこでunzipしたかによって、setup.batファイルを変更する必要があるかも知れない。

JXUnitを使う前には毎回setupを走らせよう。PATHとCLASSPATH変数を変更してくれる。

テストを走らせよう

テストの走らせ方はそのまんま:

        java junit.textui.TestRunner net.sourceforge.jxunit.JXTestCase

上記のコマンド・ラインはどのディレクトリからでも使える。JXTestCaseはカレント・ディレクトリとその下のすべてのサブ・ディレクトリからtest.jxuファイルを探す。見つかったファイルがそれぞれテスト・ケースとなり、そのディレクトリの名前がテストの名前となる。どのテストを走らせるかは、ディレクトリを変えるだけで完全に切り替えることができる。

Links