Fight the Future

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

BeanのListとDbUnitのDataSetを比較する方法を考える

DbUnitの弱点はBeanのListとDataSetを比較できないことから考えて、実際に比較する方法を考えようと思う。


方法は2つある。

  1. BeanのListをIDataSet実装オブジェクトに変換する
  2. DataSetから比較するBeanのListに変換する

2の方法だと、プリミティブなプロパティだけならいいけど、集約してるときとか変換するのが大変そう(あるBeanが違うBeanのListをプロパティとして持つなど)。


なので1の方法で考える。基本的にクラスのメタ情報を使えば変換できそうだ。
ただ、リフレクションだとパフォーマンスに悪影響を及ぼしそうなので、バイトコード操作ライブラリとかも考えた方がよさそう。

DbUnitの弱点はBeanのListとDataSetを比較できないこと

DbUnitを使うにあたって一番問題になることは、Daoから取得したJava BeansのListとExcelの期待値を比較できないことだ。


Excel(XML)の期待値はDbUnitではDataSetとして表す。
だけど、DbUnitのAssertクラスであるorg.dbunit.AssertionクラスではDataSet同士しか比較できない。

  • assertEquals(IDataSet expectedDataSet, IDataSet actualDataSet)
  • assertEquals(ITable expectedTable, ITable actualTable)

なので、BeanのListをDataSetにするか、逆にDataSetからBeanのListを作ってList同士を比較するかのどちらかを実装する必要がある。
# もしすでにそういうライブラリがあれば教えてください。。。


僕が個人的に好きなのはS2Unitで、S2TigerのJARに含まれてるS2Assertクラスにはそういうメソッドがある。

  • assertBeanEquals(org.seasar.extension.dataset.DataSet*1 expected, List list)


この辺りのソースコードSeasarに依存してるわけでもなさそうだし、これを参考に実装させてもらおうかなあ。

*1:DbUnitのDataSetと関わりはない

TestNGとJUnit4のもっとも大きな違い

TestNGとJUnit4のもっとも大きな違いはテストクラスのインスタンス生成だ。


あるテストクラスに複数のテストメソッドがあるとき、2つのフレームワークはこう振る舞う。


なので、JUnit4の方が大量のテストケースを実行すると、(インスタンス生成の分)コストがかかる。
ただ、これは一概に悪とは言えなくて、各テストメソッドの独立性に深くかかわってる。

テストメソッドの独立性

複数のテストメソッドを実行するときに同じインスタンスを使い回すと、インスタンス変数の値によってテストの成否が変わる可能性がある(テストクラスにインスタンス変数がある場合など)。
つまりテストが独立していないということ。
JUnit4は(3の時代からだが)この考え方を忠実に守って、テストメソッドごとにインスタンスを生成してる。


一方TestNGはこの点も考慮した上で、テストクラスのインスタンスは1つしか作らない、という設計にしている。それは速さの点もあるし、状態を共有するのは自然なことで、それによってよいデザインのテストになるという点もある。

せっかくなので実証してみる

ソースで実証します。

テスト対象クラスはこれ。

package diff.junitortestng;

public class Target {

	public int add(int a, int b) {
		return a + b;
	}

}

足し算するメソッドadd()だけがある単純なクラスです。


ではこれをテストするJUnit4のテストケースを作成します。

package diff.junitortestng;

import static org.junit.Assert.assertEquals;

import org.junit.Test;

public class JUnitTest {

	public JUnitTest() {
		System.out.println("JUnitTest class is instanciated.");
	}

	@Test
	public void verifyAdd1() {
		Target t = new Target();
		assertEquals(t.add(3, 4), 7);
	}

	@Test
	public void verifyAdd2() {
		Target t = new Target();
		assertEquals(t.add(2, 1), 3);
	}

	@Test
	public void verifyAdd3() {
		Target t = new Target();
		assertEquals(t.add(5, 4), 9);
	}
}

コンストラクタでコンソールに出力します。テストメソッドは3つあります。
これを実行すると当然テストは成功で、以下のように出力します。

JUnitTest class is instanciated.
JUnitTest class is instanciated.
JUnitTest class is instanciated.

コンストラクタを3回呼び出してます。


同じことをするTestNGのテストケースを作成します。

package diff.junitortestng;

import static org.testng.Assert.assertEquals;

import org.testng.annotations.Test;

public class TestNGTest {

	public TestNGTest() {
		System.out.println("TestNGTest class is instanciated.");
	}

	@Test
	public void verifyAdd1() {
		Target t = new Target();
		assertEquals(t.add(3, 4), 7);
	}

	@Test
	public void verifyAdd2() {
		Target t = new Target();
		assertEquals(t.add(2, 1), 3);
	}

	@Test
	public void verifyAdd3() {
		Target t = new Target();
		assertEquals(t.add(5, 4), 9);
	}
}

同じアノテーション、assert()メソッドですが、すべてTestNGのものです(import文を見てください)。


このテストを実行すると、こんな出力です。

TestNGTest class is instanciated.
PASSED: verifyAdd2
PASSED: verifyAdd1
PASSED: verifyAdd3

===============================================
    diff.junitortestng.TestNGTest
    Tests run: 3, Failures: 0, Skips: 0
===============================================

コンストラクタを1回しか呼び出していないことがわかります。

DbUnitでDatabaseOperation.DELETEするときのXML

<?xml version='1.0' encoding='UTF-8'?>
<dataset>
	<DEPT />
	<EMP />
</dataset>

のようにテーブルを要素名にしてを列挙するだけでよい。


逆にデータを登録するときにはカラムの値を属性にする必要がある。

<?xml version='1.0' encoding='UTF-8'?>
<dataset>
  <DEPT DEPTNO="10" DNAME="ACCOUNTING" LOC="NEW YORK"/>
  <EMP EMPNO="7369" ENAME="SMITH" JOB="CLERK" MGR="7902" HIREDATE="1980-12-17" SAL="800.0" DEPTNO="20"/>
</dataset>

みたいに。

testng.xmlの<suite-files>

バージョン5.3から追加された要素
要は他のテストスイートファイルを取り込める。

<suite-files>
  <suite-file path="ファイルのパス">
  </suite-files>
</suite-files>

ちなみにファイル間のパラメータの継承とかはないそうです。

Indeed, inheritance of parameters in testng.xml files is not supported, for
a couple of reasons:

  • I have the impression that nested testng.xml files are not very common.
  • Overall, defining parameters in testng.xml is not as useful and

flexible as @DataProvider, so I think that users who start with parameters
for quick prototyping usually migrate to @DataProvider shortly thereafter.

OpenSymphony Support Forums: Suite Issues ...

Cedricさんが言ってます。

DbUnitアノテーション対応の使い方

ソースはhttp://sourceforge.jp/projects/dbunitng/からダウンロードしてもらうとして。
TestNGのテストスイートファイルであるtestng.xmlにデータベース接続情報を記述します。

<parameter name="driver" value="com.mysql.jdbc.Driver"></parameter>
<parameter name="url" value="jdbc:mysql://127.0.0.1/データベース名"></parameter>
<parameter name="username" value="ユーザー名"></parameter>
<parameter name="password" value="パスワード"></parameter>

依存ライブラリは以下。

  • TestNG
  • DbUnit
  • Commons Lang
  • SLF4J(DbUnitが依存。slf4j-api.jarとslf4j-nop.jarが必要。)
  • Commons Collections(DbUnitが依存)
  • Apache POI(DbUnitが依存。テストデータをExcelに記述する場合必要。)
  • Commons DBCP(利用する場合必要)
  • Commons Pool(DBCPを利用する場合必要)


データベースに接続する際のConnectionは、特に指定がなければDriverManagerで生成します。
もしテストメソッド間でDataSourceを利用したい場合、testng-xmlに「DBCP」と指定します。

<parameter name="datasource" value="DBCP"></parameter>

この場合、もちろんDBCPとPoolのJARが必要です。


テストメソッド(@Testを付与したメソッド)に対して、初期データの投入を@SetUpOperationで指定します。
@SetUpOperationに属性を指定します。

  • value(データベース操作方法の指定。要はDbUnitのDatabaseOperationの定数。デフォルトはNONE。)
  • pathname(テストデータを記述したファイルパス。クラスパスでの位置。)

valueにはEnumであるDatabaseOperationTypeを指定します。


こんな感じで指定します。

@SetUpOperation(value = DatabaseOperationType.CLEAN_INSERT, pathname = "org/dbunitng/dept-emp.xml")

DbUnitと同様で、XML形式とExcel形式に対応してます。
DatabaseOperationTypeは8つの値があります。

  1. NONE
  2. UPDATE
  3. INSERT
  4. REFRESH
  5. DELETE
  6. DELETE_ALL
  7. TRUNCATE_TABLE
  8. CLEAN_INSERT

意味はDbUnitそのままです。


これでテストメソッド実行前にテストデータを投入します。
まだtearDownは実装してないですけど、同じようになります。


よかったら試してみてください!

DbUnitのアノテーション対応 ( with TestNG )

こんなのしたくない??

	@SetUpOperation(value = DatabaseOperationType.CLEAN_INSERT, pathname = "org/dbunitng/dept-emp.xml")
	@Test
	public void verifySelectAllFromDept() {
		// call target method and assert...
	}


要はDbUnitアノテーション対応。
@SetUpOperationでテスト実行前にテストデータを投入する。
アノテーションの属性でDbUnitのDatabaseOperationを指定したり、テストデータのファイルを指定したりできる。


Javaにおいてデータベースと連携した自動テストはDbUnitデファクトスタンダードだけど、なんか面倒なんだよね、DbUnitって。
DatabaseTestCaseを継承したり*1、setUpやtearDownでOperationを指定したり、投入するデータ読み込んだりとか。


アノテーションに対応したらちょっと楽になるかな、とか思った。
なので、作ってみました!*2


さすがに単体ではできないので、TestNGと連携させます。
TestNGのリスナーとしてDbUnitとの連携を実現します。
testng.xmlにリスナーを記述します。

	<listeners>
		<listener class-name="org.dbunitng.listeners.DbUnitNGTestListener"></listener>
	</listeners>

あとはテストメソッドにアノテーションを指定するだけ!
テストデータはDbUnit形式のXMLまたはExcelファイルに記述します。


ここまで書いていたら、ソースを落とせた方がいいと思ったので、SourceForge.JPにプロジェクトを申請しました。
ってニーズはあるのかな??

*1:しなくてもIDatabaseTester実装クラスを使う方法もあるけど

*2:実は会社で開発させてもらってます

TestNGとDbUnitを連携したサンプル

TestNGDbUnitを連携させることで、テストクラスはTestCaseを継承する必要がなくなる。
TestCaseを継承する代わりに、XXDatabaseTesterというDbUnitにあるクラスを使う。

  • JdbcDatabaseTester
  • DataSourceDatabaseTester

DriverManagerからConnectionを作るか、DataSourceからConnectionを作るかで使い分ける。


たとえばテスト実行前のsetup処理はこんな感じ。

	@BeforeClass
	@Parameters( { "driver", "url", "username", "password" })
	public void initialize(String driver, String url, String username,
			String password) throws Exception {

		JdbcDatabaseTester connectionHolder =
			new JdbcDatabaseTester(driver, url, username, password);
		IDataSet initialDataSet =
			new FlatXmlDataSet(new File("DbUnitSampleTestCase.xml"));
		IDatabaseConnection connection = connectionHolder.getConnection();
		try {
			DatabaseOperation.CLEAN_INSERT.execute(connection, initialDataSet);
		} finally {
			connection.close();
		}
	}

まずアノテーションから。両方ともTestNGアノテーション

@BeforeClassはsetup処理をするメソッドにつける。この場合クラスで1回(メソッドごとではない)呼び出される。

@Parametersはこのメソッドに渡す引数の値を指定する。ただし、"driver"という値を渡しているわけではなくて、これはキーである。
実際の値はTestNGのテスト実行ファイルに記述している。たとえばこんな感じ。

	<parameter name="driver" value="com.mysql.jdbc.Driver"></parameter>
	<parameter name="url" value="jdbc:mysql://127.0.0.1/データベース名"></parameter>
	<parameter name="username" value="ユーザー名"></parameter>
	<parameter name="password" value="パスワード"></parameter>

このパラメータを使ってJdbcDatabaseTesterオブジェクトを生成する。
さらに、データベースに登録する初期データを読みこんでる。

		IDataSet initialDataSet =
			new FlatXmlDataSet(new File("DbUnitSampleTestCase.xml"));

これはDbUnitの機能。DbUnitSampleTestCase.xmlはデータを記述したXMLファイルだ。
このファイルからDataSetオブジェクトを生成する。DataSetはTableを(複数)保持している。


そしてJdbcDatabaseTesterから(DbUnitの)Connectionを取得し、初期データを登録する。

		try {
			DatabaseOperation.CLEAN_INSERT.execute(connection, initialDataSet);
		} finally {
			connection.close();
		}

DatabaseOperationはデータの登録方法で、今回は定数CLEAN_INSERTを利用した。
CLEAN_INSERTはDELETE文ですべてのレコードを削除してからINSERT文を発行してデータを登録する。
これでsetup処理は完了。


テストメソッドはこんな感じ。

	@Test
	public void verifySelectAllFromDept() {
		DeptDao deptDao = new DeptDao();
		List<Dept> actual = deptDao.listAllDept();
		assertEquals(actual.size(), 4);
	}

@TestはTestNGアノテーションでテストメソッドを表す。
テスト対象メソッドを呼び出して、assertEquals()を呼び出す。
継承していないので、assertEquals()はstatic importしてる。


完全なソースはここから。

package net.kronos_jp.dbtest.sample;

import static org.testng.Assert.assertEquals;

import java.io.File;
import java.util.List;

import net.kronos_jp.dbtest.target.dao.DeptDao;
import net.kronos_jp.dbtest.target.entity.Dept;

import org.dbunit.JdbcDatabaseTester;
import org.dbunit.database.IDatabaseConnection;
import org.dbunit.dataset.IDataSet;
import org.dbunit.dataset.xml.FlatXmlDataSet;
import org.dbunit.operation.DatabaseOperation;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Parameters;
import org.testng.annotations.Test;

/**
 * <p>
 * DbUnitTestNGを連携したサンプルです。
 * </p>
 * 
 * @author jyukutyo
 * 
 */
@Test
public class DbUnitSampleTestCase {

	@BeforeClass
	@Parameters( { "driver", "url", "username", "password" })
	public void initialize(String driver, String url, String username,
			String password) throws Exception {

		JdbcDatabaseTester connectionHolder =
			new JdbcDatabaseTester(driver, url, username, password);
		IDataSet initialDataSet =
			new FlatXmlDataSet(new File("DbUnitSampleTestCase.xml"));
		IDatabaseConnection connection = connectionHolder.getConnection();
		try {
			DatabaseOperation.CLEAN_INSERT.execute(connection, initialDataSet);
		} finally {
			connection.close();
		}
	}

	@Test
	public void verifySelectAllFromDept() {
		DeptDao deptDao = new DeptDao();
		List<Dept> actual = deptDao.listAllDept();
		assertEquals(actual.size(), 4);
	}

}

JavaのテスティングフレームワークはJUnitだけじゃないので

TestNGというテスティングフレームワークがある。
JavaにおけるテスティングフレームワークはもちろんJUnitデファクトスタンダードだ。
でも、TestNGにもスゴくいいところがある。

TestNG is a testing framework inspired from JUnit and NUnit but introducing some new functionalities that make it more powerful and easier to use

TestNG - Welcome

TestNGJUnitにインスパイアされたと同時にアンチテーゼとして作られてる。

TestNG is designed to cover all categories of tests: unit, functional, end-to-end, integration, etc...

TestNG - Welcome

TestNGユニットテストだけでなく、機能テストや統合テストもカバーするように設計されてる。
ユニットテストが主なターゲットであるJUnitとはこの前提の部分ですでに異なっている。
この点が重要。


JUnit4があるからTestNGは不要というわけではない。

そもそもJUnit4が出るまで、JUnitには課題があった。
たとえばTestCaseクラスを継承する必要があったり、例外をスローするテストメソッドが煩雑であったりすることだ。


その点をTestNGは(アンチテーゼとして)クリアしていたため、TestNGに有利な点があった。
アノテーションにも対応していた。
でもJUnit4でこれらの点は改善され、そういう意味でJUnitTestNGと同等になった。


だからTestNGはもう不要、というわけではない。
フレームワークとしてカバーしている範囲が違うんだ。実装上の小さな差異だけが問題ではない。


このblogで少しずつTestNGのいいところを紹介していけたらと思う。