Fight the Future

Java言語とJVM、そしてJavaエコシステム全般にまつわること

意外に知らないDbUnitでCSVを使う方法

dbunit csv」で検索してもヒットしなかったので書いておく。
DbUnit2.1から、初期値や期待値をCSVファイルに書くことができる。XMLやExcelの代わりに。


使い方はとても簡単。

  1. テーブル名.csvというファイルを作成する(EMPテーブルならemp.csv)
  2. CSVファイルの1行目に列名、2行目以降にデータを記述する
  3. 複数のテーブルにデータを入れたい場合、CSVファイルも複数作成する
  4. 「table-ordering.txt」というファイルを作成する
  5. txtファイルに登録するテーブルの順序を記述する(参照整合性制約に反しないように)
  6. CSVファイル、txtファイルを同じディレクトリに配置する
  7. CsvDataSetのコンストラクタにファイルを配置したディレクトリのjava.io.Fileオブジェクトを渡す


たとえば、EMPテーブルがあるとして、emp.csvはこんな感じ。

EMPNO,ENAME,JOB,MGR,HIREDATE,SAL,DEPTNO
7782,CLARK,MANAGER,7839,1981-01-09,2450.0,10

DEPTテーブルのdept.csv。

DEPTNO,DNAME,LOC
10,ACCOUNTING,NEW YORK

EMPテーブルのDEPTNOは、DEPTテーブルを参照しているので、データを登録するときは「DEPT→EMP」の順序にしたい。
なので「table-ordering.txt」はこうなる。

dept
emp

拡張子.csvは必要なし。
CsvDataSetオブジェクトを生成する。

String pathName = "ディレクトリのクラスパスルートからの位置";
URL url =
	Thread
	.currentThread()
	.getContextClassLoader()
	.getResource(pathName);
CsvDataSet dataSet = new CsvDataSet(new File(url.toURI()));


この話、公式サイトにもドキュメントないような。。。
ソースを読むとわかります。

たとえば。

public CsvDataSet(File dir) throws DataSetException {
    super(new CsvProducer(dir));
    this.dir = dir;
}

CsvDataSetのコンストラクタに引数としてdirつまりディレクトリを渡して。

List tableSpecs = CsvProducer.getTables(dir.toURL(), "table-ordering.txt");
for (Iterator tableIter = tableSpecs.iterator(); tableIter.hasNext();) {
    String table = (String) tableIter.next();
    try {
	produceFromFile(new File(dir, table + ".csv"));

table-ordering.txtを行単位で読んで、その行に.csvをつけたファイルを読み込んでる。

private void produceFromFile(File theDataFile) throws DataSetException, CsvParserException {
    logger.debug("produceFromFile(theDataFile=" + theDataFile + ") - start");

    try {
        CsvParser parser = new CsvParserImpl();
        List readData = parser.parse(theDataFile);
        List readColumns = ((List) readData.get(0));
        Column[] columns = new Column[readColumns.size()];

        for (int i = 0; i < readColumns.size(); i++) {
           columns[i] = new Column((String) readColumns.get(i), DataType.UNKNOWN);
        }

(List) readData.get(0)つまりファイルの1行目が列名として扱われてる。

@TableAssertでクエリをファイルに書けます

TestNG+DbUnitライブラリであるDbUnitNGで、DbUnitライブラリ@TableAssertでDBとアサートできます - Fight the Future じゅくのblogにある@TableAssertに機能を追加しました。

結果をDBから取得するクエリをプロパティファイルに記述できるようにしました。

@TableAssert(names = "dept", pathname = "insert_expected.xml", propertyFilePath="query.properties", keys="test")
@SetUpOperation(pathname = "dept.xml", value = DatabaseOperationType.DELETE_ALL)
public void testTableAssert3() {
	insert();
}

クエリを直接Javaコードに記述すると読みづらいので。


@TableAssertのpropertyFilePath属性がプロパティファイルへのパスです。
「/」がない場合、テストクラスと同じパッケージにあるファイルを探します。
「/」があればクラスパスのルートからの位置になります。
keysに指定したキーの値としてクエリを書きます。

test = select dname, deptno, loc from dept

namesはクエリの結果を格納するDbUnitのITableのテーブル名です。

Google Test Automation Conference

さっきのTestNGの動画で知ったばかりだけど、どうやらGoogleは毎年Google Test Automation Conferenceというのをやってる。


Google Testing Blog: Call for Attendance: GTAC 2008


今年はシアトルで10/23,24にやるみたい。


TestNGの動画は去年のもので、資料もGoogle Groupからダウンロードできる。

Let's talk about China! |
Google Groups

世界一やさしいTestNGの使い方

JARファイルをダウンロードする

TestNGのサイトにいきます。
メニューから「Download」を選びます。
「download TestNG here.」のリンクを押すと最新のJARファイルをダウンロードできます。
f:id:jyukutyo:20080827105650p:image
ダウンロードしたzipファイルを解凍します。
解凍したディレクトリの直下にある「testng-X.X-jdkXX.jar」をクラスパスに追加します。
2種類ありますが、JDKのバージョンに合わせて選択してください。

JDK1.4の場合

testng-X.X-jdk14.jar」をクラスパスに追加します。

JDK1.5以降の場合

testng-X.X-jdk15.jar」をクラスパスに追加します。


これでTestNGを利用できます。

eclipseプラグインを利用する

アップデートサイトを利用してプラグインをインストールします。
eclipse3.4以降なら、ヘルプ(Help)→ソフトウェアアップデート(Software Updates...)を選択します。
利用できるソフトウェア(Available Software)を選択します。
サイトを追加(Add Site)で「http://beust.com/eclipse」を入力し、OKを押します。
ソフトウェアアップデートの画面に新たに「http://beust.com/eclipse」が表示されます。
それにチェックを入れてインストール(Install)を押します。
あとは表示される画面にOKを押してそのまま進めます。
これでeclipseプラグインのインストールは完了です。

テストを作成する

普通のJavaクラスを作成します。継承やインタフェースの実装は必要ありません。クラス名も任意です。
メソッドを作成します。必ず修飾子はpublic、戻り値はvoidにします。メソッド名は任意です。
「org.testng.annotations.Test」クラスをインポートします。Testはアノテーションクラスです。
作成したメソッド(テストメソッド)に@Testアノテーションをつけます。

@Test
public void verifySample() {
	// describe your test program...
}

@Testをつけることで、TestNGはそのメソッドをテストメソッドと認識し、テストを実行してくれます。

実行結果を検証する

実行結果が正しいかどうか検証するには、「org.testng.Assert」クラスを利用します。これはJUnitのAssertと同じ役割です。
「org.testng.Assert」クラスのassertXXX()メソッドを利用して検証します。assertXXX()メソッドはすべてstaticメソッドなので、JDK5.0以降であればstaticインポートを利用するとよいです。

import static org.testng.Assert.assertEquals;

assertXXX()には主に4つの種類があります。

メソッド名 役割
assertEquals(実行した結果, 期待する結果) 実行した結果が期待する結果と同じか比較する
assertNull(実行した結果) 実行した結果がnullか比較する
assertSame(実行した結果, 期待する結果) 実行した結果が期待する結果と同じインスタンスか比較する
assertTrue(実行した結果) 実行した結果がtrueか比較する

assertEquals()とassertSame()の違いは1つだけです。
assertEquals()は同じかどうかを、equals()メソッドを使って比較します。equals()メソッドはJavaのすべてのクラスのスーパークラスであるObjectクラスに定義されているメソッドです。
assertSame()は同じインスタンスかどうかを比較します。「==」を使って比較するのと同じです。
assertXXXには、比較した結果が違っていたときに、任意のメッセージを表示させることができます。上記の表の引数にプラスString型の引数があるメソッドがあります。
たとえば、assertEquals(実行した結果, 期待する結果, メッセージ)のような形です。
ほかにもassertNotNull(),assertNotSame(),assertFalse()がありますが、上記の表にあるメソッドの意味を反転させただけのものです。

テストを実行する

eclipseであれば、作成したクラスを選んだ状態でメニューの実行設定(Run Configurations)を選択します。
左ペインでTestNGを選択します。
f:id:jyukutyo:20080827105649p:image
TestタブでClassにチェックを入れます。
Classの横にあるBrowse...を押し、作成したクラスを選択します。
実行(Run)を押すとテストを実行します。
JUnitと同様、成功するとグリーンバーを、失敗するとレッドバーを表示します。
f:id:jyukutyo:20080827105648p:image

JUnitと代わりありません

使い方に関しては、TestNGJUnitに違いはありません。
基本的な機能を使う分には、新たに覚え直すこともありません。
JUnitにはない、TestNGの便利な機能を使いたくなったら、そのときに調べればいいだけです。
JUnitTestNGの機能の違いはこちらによい図があります。

@TableAssertだと何がいいのか

僕の作っているTestNG + DbUnitライブラリでDBとのアサートをアノテーションで指定できるわけですけど。

@TableAssert(names = "dept", pathname = "insert_expected.xml")
@SetUpOperation(pathname = "dept.xml", value = DatabaseOperationType.DELETE_ALL)
public void testTableAssert() {
	insert();
}

テーブルとのアサートをアノテーションにしよう、っていう思いつきで作ったんだけど、
よくよく考えると、プロジェクトで作るようなスーパークラスにこういうメソッドを作って、呼び出すというのとそう変わりはない気がした。


アノテーションによってコード量がそこまで減るわけでもないし。。。


ただ、アノテーションというのは処理のインタフェース(実装ではなく仕様の入り口)として優れてると感じた。
この@TableAssertだって処理に必要なデータが属性としてまとまっているから、ここだけ見れば必要なすべてわかる。


この属性を使ってどうこう、という実装はまったく別にあるわけで、目には入らない。


@TableAssertによって、このテストメソッドはDBと比較するんだなとすぐにわかる。
そして、比較するファイルや対象テーブルも属性を見てすぐにわかる。
あとあとのテストのメンテナンスという意味でも。アノテーションにしたことでわかりやすくていいんじゃないかな。

JUnitは死なず

JUnitTestNGよりも多く採用され拡張されているものの、TestNGの方が多くの機能を提供し、JUnit用のテスト(スクリプト)を実行することも可能なので、テスト・フレームワークとしてTestNGを選択する方が魅力的な選択に思えるようである。

InfoQ: JUnitは死なず

僕は時代の生き証人ではないけど、JUnitって名前が独り歩きし過ぎてる気がする。
たしかにJUnitによってユニットテスト、自動テストといったコンセプトが広く知れ渡るようになったんだし、JUnitの功績は計り知れない。


テスティングフレームワークとしてデファクトの地位を確立しているわけだけど、
その圧倒的なネームバリューのせいか、プロジェクトにおけるテスティングフレームワークは熟考なしにJUnitになってしまう。


別に機能が多ければいいというわけではないので、TestNGを使え!っていうつもりもないんだけど、熟考なし、条件反射的に何も考えず選択をするっていうのは必ずよくないことになる。


つまり、テスティングフレームワークとして今回のプロジェクトでJUnitが適切なのか、TestNGが適切なのか、それとも他のフレームワークが適切なのかといったことをきちんと考えた上で選択しているのか、ということ。
(プロジェクトにおいて最大の罪は前もこうだったとかいつもこれだとか、今回何が適切なのかを深く考えずに決定を下すことだと思ってる。これをなくすだけでしょーもない失敗プロジェクトは減ると個人的に思い込んでる。)


そのためにはJUnitのほかにどんなテスティングフレームワークがあるのか知らないといけないし、どんな特徴があるのか、長短知らないといけない。


JUnitのネームバリューがここんとこを妨げてるんじゃないかなと思う。

DbUnitライブラリ@TableAssertでDBとアサートできます

DbUnitNG プロジェクト日本語トップページ - SourceForge.JPで期待値のファイルとデータベースのテーブルをアサートするアノテーションを作りました。
@TableAssertです。


こんな感じ。

	@TableAssert(names = "dept", pathname = "insert_expected.xml")
	@SetUpOperation(pathname = "dept.xml", value = DatabaseOperationType.DELETE_ALL)
	public void testTableAssert() {
		insert();
	}

必須属性はnamesとpathnameです。
この2つだけなら、namesにデータベースのテーブル名を記述します。
つまり、今回はdeptテーブルと期待値ファイルをアサートします。


アサートは、期待値のファイルにあるカラムだけ比較します。
pathnameには期待値のファイルを記述します。この場合、「insert_expected.xml」が期待値のファイルです。
ファイル名だけであれば、テストクラスと同パッケージに配置します。
内容はDbUnitの形式です。


SQL文を直接記述することもできます。

	@TableAssert(names = "dept", pathname = "org/dbunitng/sample/dao/insert_expected.xml", queries = "select dname, deptno, loc from dept")
	@SetUpOperation(pathname = "dept.xml", value = DatabaseOperationType.DELETE_ALL)
	public void testTableAssert2() {
		insert();
	}

queries属性にはSQL文を記述します。その結果がDbUnitのITableとなります。ITableのテーブル名はnames属性に記述した名前になります。
つまり、この例ではSQL文の結果がITableとなり、テーブル名はdeptとなります。
XMLで表現すると以下のイメージです。

<dataset>
	<DEPT DEPTNO="1" DNAME="test" LOC="Osaka" />
</dataset>

これを実装するのに散々悩んだのですが、結局一番ダサい方法で実装しました。

ダウンキャスト。。。
ITestListener#onStart(ITestContext context)の引数ITestContextを実装クラスTestRunnerクラスにキャストしてます。


で、リスナーのリストにアクセスします。
リストを取得したら、DbUnitNGライブラリのリスナーが先頭に来るようにソートしてます。

		TestRunner runner = (TestRunner) context;
		List<ITestListener> list = runner.getTestListeners();

		Collections.sort(list, new Comparator<ITestListener>() {
			public int compare(ITestListener o1, ITestListener o2) {
				if (o1 instanceof DbUnitNGTestListener) {
					return -1;
				} else if (o2 instanceof DbUnitNGTestListener) {
					return 1;
				}
				return 0;
			}
		});

あーJavaなのにインタフェースの実装クラスを意識してダウンキャストなんて、一番ダサいプログラミングだよね。。。
他にいい方法が思いつかなかった。。。

TestNG eclipseプラグインでのリスナークラス登録の続き

TestNGプラグインのリスナーより順序を前で自分のリスナーを登録したい - Fight the Future じゅくのblogで困っているわけだけど、
こりゃTestNGのソースを変えないと無理かもしれないなあ。


TestNGeclipseプラグインはTestNGクラスではなくサブクラスのRemoteTestNGクラスを使ってリモート実行してる。
そのRemoteTestNGクラスのstaticネストクラス*1がこれ。

private static class DelegatingTestRunnerFactory implements ITestRunnerFactory {
    public TestRunner newTestRunner(ISuite suite, XmlTest test) {
      TestRunner tr= m_delagateFactory.newTestRunner(suite, test);
      tr.addListener(new RemoteMessageSenderTestListener(suite, test, m_messageSender));

      return tr;
    }
  }

TestRunner#addLisnter()で真っ先にRemoteMessageSenderTestListenerクラスをとうろくしちゃってる。
つまりリモートであるeclipseプラグインのUIが一番先に変わってしまう問題は解決できない。


んー、TestNGのリスナーに優先順位をつけれたらいいのになあ。

*1:インナークラスじゃない[http://d.hatena.ne.jp/jyukutyo/20061020/1161314906:title]

DbUnitNG0.3リリース!

DbUnitNG プロジェクト日本語トップページ - SourceForge.JP

  • 期待値ファイルからデータセットを作成する際に、ReplacementDataSetを使用するようにしました。
  • @SetupOperationにおいてテストスイート全体でデフォルトのでDatabaseOperationを設定できるようにしました。
  • データベースへの接続情報をアノテーションでも設定できるようにしました。

TestNG ML(英語)に投げてみた

TestNGプラグインのリスナーより順序を前で自分のリスナーを登録したい - Fight the Future じゅくのblogがどうしてもわからないので、
testng-users |
Google Groups
に投げてみた。

Hi,

With TestNG eclipse plugin, I add my listener class.
Then listener List that are returned by ITestResultNotifier#getTestListeners() are the order as follows.

[org.testng.remote.strprotocol.RemoteMessageSenderTestListener@50cf54, 
org.testng.reporters.TextReporter@5bc8c9, 
org.dbunitng.listeners.DbUnitNGTestListener@db3aac, 
org.testng.TestNG$ExitCodeListener@e90abf, 
org.testng.TestListenerAdapter@c0b3d1]

I'd like to add my listener class(DbUnitNGTestListner) before RemoteMessageSenderTestListener class.
What can I do?

Thanks, 

もちろん、英語のMLに送るのなんて初めて。
というか、オープンソースのMLに送るのなんて日本語でもやったことない。


たぶん英語めちゃくちゃ。
文章を作るのにあんまり時間かけすぎるのもどうかと思って。


Cedric、回答を返してくれるといいなあ。

TestNGプラグインのリスナーより順序を前で自分のリスナーを登録したい

eclipseTestNGプラグインを利用してデバッグ実行すると、
リスナーは以下の順で Listに入っていた。

[org.testng.remote.strprotocol.RemoteMessageSenderTestListener@50cf54, 
org.testng.reporters.TextReporter@5bc8c9, 
org.dbunitng.listeners.DbUnitNGTestListener@db3aac, 
org.testng.TestNG$ExitCodeListener@e90abf, 
org.testng.TestListenerAdapter@c0b3d1]

RemoteMessageSenderTestListenerがTestNGプラグインのリスナーであり、
このリスナーが先にきているため先ほどのようにeclipseのUIが先に変わってしまう。
TestNGのリスナーより先の順番に自分のリスナーを登録したい。
XMLで先頭に書こうがデフォルトリスナーとしてeclipseに登録しようが、
どうしても現状TestNGプラグインより前に登録できない><


どなたかリスナーの登録順序を制御する方法を知っていたら教えてください!

@TableAssertでアサートしたかったけど、失敗

誰か助けてください><


期待値とDBのテーブルを比較するようなテストなら、アサートもアノテーションでできるようにしようと考えた。
こんな感じで。

@TableAssert(names = "dept", pathname = "insert_expected.xml", queries = "select dname, deptno, loc from emp")
@SetUpOperation(pathname = "dept.xml", value = DatabaseOperationType.DELETE_ALL)
public void testTableAssert2() {
	insert();
}

XMLにある期待値とSELECT文の結果をアサートするイメージ。

いちおう、できたんだ。
コマンドラインから実行したり、main()メソッドで実行する分には。

eclipseプラグインで袋小路

でも、eclipseプラグインで実行するとダメ。
テーブルと期待値が違っていてアサートに失敗する場合でも、UIではPASSEDになっちゃう。
f:id:jyukutyo:20080822144506p:image
コンソールはこう出力される。

[DbUnitNGDatabaseOperation] [DEBUG] Database Operation is DELETE_ALL.
[DbUnitNGDatabaseOperation] [DEBUG] Database Operation is DELETE_ALL.
PASSED: testTableAssert
PASSED: testTableAssert2

===============================================
    sample
    Tests run: 2, Failures: 0, Skips: 0
===============================================

[DbUnitNGTestListener] [DEBUG] DbUnitNGTestListener#onFinish
Test Suite ends.
execute time : (865 ms)
===============================================
DbUnitNG
Total tests run: 2, Failures: 2, Skips: 0
===============================================

スイート全体ではFailureになるけど、途中では PASSEDのまま。


これ、TestNGのeclipseプラグインがTestNGのRemoteClinetでテストを起動してるからだと思う。
RemoteClinetだと、プラグインでUIに表示するリスナーにはオブジェクトじゃなく文字列で通知が行く。

public void onTestSuccess(TestResultMessage trm) {

一方、僕がアノテーションでテーブルとアサートしているのは普通のリスナー。

	public void onTestSuccess(ITestResult result) {
		
		TableAssert tableAssert = this.getAnnotation(result, TableAssert.class);
		if (tableAssert != null) {
			assertTable(tableAssert, result);
		}
		onTestFinishWhateverHappens(result);
	}

	protected void assertTable(TableAssert tableAssert, ITestResult result) {

		IDataSet actual = createQueryDataSet(tableAssert, result);
		IDataSet expected = toDataSet(tableAssert.pathname(), result);
		try {
			Assertion.assertEquals(expected, actual);
		} catch (Throwable e) {
			result.setStatus(ITestResult.FAILURE);
			result.setThrowable(e);
		}

	}

アサートのところで、テストのステータスをFAILUREに上書きしてるけど、RemoteClinetにはすでに文字列で通知が行ってるから、変更は反映されない。


だからプラグインのUIではPASSED、コンソールにはFAILUREと表示されてしまう。。。
これはさすがに使えないから、アノテーションでのアサートはあきらめないといけないかな。。。


eclipseプラグインやTestNGに詳しい人、もしアイデアをお持ちでしたらぜひ教えてください!