trunk/DbUnitNG/sampleとsampletestディレクトリ以下にあります。
sampleはiBatisを使ったデータベースアクセスアプリです。
なので、DbUnitNGの依存ライブラリに加えてiBatisのライブラリが必要です。
DBはMySQLを使っていますが、他のDBでも動くはずです。
必要なテーブルのcreate文や初期データはsampleのorg.dbunitng.sample.sqlパッケージに入っています。
例外発生テストメソッドに見るテスティングフレームワークの進化
テストにおいては、正しく例外が発生するかもテストする必要がある。
JUnit3の時代は、僕の周りではそのテストの書き方を理解している人が少なかった印象がある。
JUnit3では例外発生テストはこのように書くものだった。
import junit.framework.TestCase; public class TestCaseSample extends TestCase { public void testExpectedException() { Sample s = new Sample(); try { s.throwException(); fail("Exception is not thrown."); } catch (IllegalStateException expected) { assertTrue(true); } } private static class Sample { public void throwException() { throw new IllegalStateException(); } } }
ポイントは2つあった。
- 例外が発生するメソッドを呼び出した次の行でTestCase#fail()を呼び出す。
- 例外発生をテストしていると読みやすくするため、catch句では例外の変数をexpectedとし、assertTrue(true)とする。
要は、例外が正しく発生しなければcatch句に行かず、fail()が呼ばれてテストが失敗する。
変数expectedやassertTrue()は読みやすさのためだが、このfailを書かずにテストしている人が多かったように感じる(そもそも例外発生のテストを書く人自体が少なかった)。
fail()がないと例外が発生しなくてもテストが成功してしまう。
こうしたややトリッキーなコードであるため、広まっていなかったと思う。
JUnit4では、@Testに期待する例外クラスを記述することができる。
同じテストをJUnit4で書くとこうなる。
import org.junit.Test; public class JUnit4Sample { @Test(expected = IllegalStateException.class) public void testExpectedException() { Sample s = new Sample(); s.throwException(); } private static class Sample { public void throwException() { throw new IllegalStateException(); } } }
@Testの属性としてexpectedがあり、そこに発生する例外クラスを記述する。
もし発生しなければテストは失敗する。
もちろんTestNGでも同様(というよりこっちが先駆者?)
TestNGではこうなる。
import org.testng.annotations.Test; public class TestNGSample { @Test(expectedExceptions = IllegalStateException.class) public void testExpectedException() { Sample s = new Sample(); s.throwException(); } private static class Sample { public void throwException() { throw new IllegalStateException(); } } }
アノテーションの属性名が異なるだけだ。expectedExceptionsで指定する。
ただ、違いはある。
TestNGとJUnit4のもっとも大きな違い - Fight the Future じゅくのblogでも述べたように。JUnitはテストの独立性を考えているため、例外クラスは1つしか記述できない。
テストは独立しているのだから、2つの例外が同時に発生することはないし、当然だ。
一方、TestNGはexpectedException「s」となっているように、例外クラスを複数指定できる。
どれが1つの例外が発生しさえすればテストは成功になる。
テストメソッドにパラメータを渡せるため、パラメータによって発生する例外が異なることもあるからだ。
TestNGのログを出力するには
クラスパスのルートに「log4testng.properties」を置く。
log4testng.propertiesはこんな風に設定する。
# log4testng will log its own behavior (generally used for debugging this package only). log4testng.debug=true # Specifies the root Loggers logging level. Will log DEBUG level and above log4testng.rootLogger=DEBUG
TestNGは特に他のログライブラリには依存していないので、これだけでOK。
TestNGの発音は「てすとえぬじー」ではなかった!
TestNG (pronounced "testing", but derived, one understands, from "Testing, the Next Generation")
Next Generation Java Testing - JavaLobby Book Review
TestNGと書いて「てすてぃんぐ」と読むようだ。
ちなみに、僕がよく使う発音の調べ方は、「XXX pronounce」でググること。
pronounceは発音って意味ね。
これで長年の疑問だったiBatisも解決できたよ(あいばてぃす)。
ライブラリをソーシャルブックマークしてくれてる人が!
Livedoorクリップでライブラリをブクマしてくれてる人が!
1人だけど、超感激。
TestNG+DbUnitライブラリのパフォーマンス
5000件をBeanのListで取得して、XMLの期待値と比較するパフォーマンステストをやってみました。
こういうのって一概に参考にならないことも多いと思うんですけど、感触だけでも伝えられたらって感じです。
こんなテーブルに5000件入れます。
mysql> desc emp; +----------+-------------+------+-----+---------+-------+ | Field | Type | Null | Key | Default | Extra | +----------+-------------+------+-----+---------+-------+ | EMPNO | int(11) | NO | PRI | NULL | | | ENAME | varchar(10) | YES | | NULL | | | JOB | varchar(9) | YES | | NULL | | | MGR | int(11) | YES | | NULL | | | HIREDATE | date | YES | | NULL | | | SAL | float(7,2) | YES | | NULL | | | COMM | float(7,2) | YES | | NULL | | | DEPTNO | int(11) | YES | MUL | NULL | | +----------+-------------+------+-----+---------+-------+
データはこんなのです。期待値のXMLです。
<?xml version='1.0' encoding='UTF-8'?> <dataset> <EMP EMPNO="1" ENAME="SMITH" JOB="CLERK" MGR="7902" HIREDATE="1980-12-17" SAL="800.0" DEPTNO="20"/> ...follow similar elements </dataset>
こういうテストです。
DAOは内部でiBatisを使ってデータを取得してます。
実行結果はこうでした!
Test Suite starts. DAO execute time : (381 ms) PASSED: testPerformance Test Suite ends. execute time : (14359 ms)
テスト全体での実行時間は14秒ちょい(3回実行してもほぼこの時間でした)。
DAOのメソッドを呼び出す前後で時間を計測しましたが、それが0.4秒なので、
CLEAN_INSERTして、データセットへの変換とアサーションで14秒かかってます。
5000件でこの実行時間なら十分許容範囲じゃないです?
まさかユニットテストで5000件入れることはないし、
結合テストとかならビルドマシンやデイリービルドで実行するので、
十分実用にたえると思います。
以下実行環境や詳しいテスト内容を記述します。
僕のマシンスペックはこうです。
- プロセッサ名: Intel Core 2 Duo
- プロセッサ速度: 2.4 GHz
- プロセッサ数: 1
- 合計コア数: 2
- 二次キャッシュ: 3 MB
- メモリ: 2 GB
MySQLのバージョンです。
mysql> select version(); +-------------+ | version() | +-------------+ | 5.0.51b-log | +-------------+
テストクラスです。
package org.dbunitng.sample.test; import java.util.List; import org.dbunit.DatabaseUnitException; import org.dbunitng.annotations.DatabaseOperationType; import org.dbunitng.annotations.SetUpOperation; import org.dbunitng.assertion.AssertionHelper; import org.dbunitng.dataset.BeanListConverter; import org.dbunitng.sample.dao.EmpDao; import org.dbunitng.sample.entity.Emp; import org.testng.annotations.Test; @Test(groups = "performance") public class PerformanceTest { @SetUpOperation(value = DatabaseOperationType.CLEAN_INSERT, pathname = "org/dbunitng/sample/test/result.xml") public void testPerformance() throws DatabaseUnitException { EmpDao dao = new EmpDao(); long start = System.currentTimeMillis(); List<Emp> list = dao.listAllEmployee(); System.out.printf("DAO execute time : (%d ms)\n", System.currentTimeMillis() - start); AssertionHelper.assertEqualsOnlyColumnsInFile(new BeanListConverter( list).convert(), getClass(), "result.xml"); } }
実行時間を計測するリスナークラスです。
package org.dbunitng.sample.test; import org.testng.ISuite; import org.testng.ISuiteListener; public class PerformanceListener implements ISuiteListener { private long start; public void onStart(ISuite suite) { System.out.println("Test Suite starts."); start = System.currentTimeMillis(); } public void onFinish(ISuite suite) { System.out.println("Test Suite ends."); System.out.printf("execute time : (%d ms)", System.currentTimeMillis() - start); } }
DbUnitNG 0.1 リリース!
DbUnitNGのバージョン0.1をリリースしました。
- DbUnitのSetUpOperationとTearDownOperatonをアノテーション対応しました。
- BeanのListをDbUnitのIDataSetに変換するBeanListConverterを実装しました。
- DbUnitのAssertionに対するヘルパークラスAssertionHelperクラスを実装しました。
以上すべてテストコードを記述済です。
基本的な使い方はSourceForgeのWikiに記述しています。
少しアノテーションの属性でファイルを指定するのがまどろっこしいので、0.2では簡便性のために属性を増やそうと思っています。
TestNGで使えるアノテーションの作り方
DbUnit + TestNGのライブラリでは、@SetUpOperationなどのアノテーションを使えるわけですが、これはTestNGでリスナーインタフェースが用意されているおかげです。
TestNGでは、リスナーインタフェースが4つ用意されています。
リスナー | 説明 |
---|---|
IMethodInterceptor | テストメソッド実行前に呼び出されるリスナー。実行するテストメソッドを選択することができる。 |
IReporter | テスト結果のレポート処理に関するリスナー。 |
ISuiteListener | テストスイートの実行前と終了後に呼び出されるリスナー。 |
ITestListener | 各テストやテストメソッド実行前と終了後に呼び出されるリスナー。 |
ちなみにこれら4つはすべてITestNGListenerのサブインタフェースです。
なので、たとえばテストメソッドごとにつける@SetUpOperationなどは、テストメソッドごとに解釈する必要があるので、ITestListenerを使います。
ITestLisnerには7つのリスナーメソッドがあります。
メソッド | 説明 |
---|---|
onStart(ITestContext context) | テストクラスをインスタンス化した後に呼び出される。 |
onFinish(ITestContext context) | すべてのテストを実行した後に呼び出される。 |
onTestStart(ITestResult result) | 各テストメソッドを実行する前に呼び出される。 |
onTestFailedButWithinSuccessPercentage(ITestResult result) | テストが成功確率以内なら呼び出される。*1 |
onTestFailure(ITestResult result) | テストメソッドが失敗したら呼び出される。 |
onTestSkipped(ITestResult result) | テストメソッドをスキップしたら呼び出される。 |
onTestSuccess(ITestResult result) | テストメソッドが成功したら呼び出される。 |
このうち、テストメソッドごとにアノテーションを解釈するので、onTestStart()を実装する必要があります。
さて、@SetUpOperationですが、こんなアノテーションです。
@Retention(RUNTIME) @Target(METHOD) public @interface SetUpOperation { DatabaseOperationType value() default DatabaseOperationType.NONE; String pathname() default ""; String extension() default ""; }
DatabaseOperationTypeはCLEAN_INSERTとかREFLESHとかのEnumです。pathnameは初期データファイルのパス、extensionは拡張子を指定します。
で、このアノテーションをonTestStart()で解釈します。
public class DbUnitNGTestListener implements ITestListener { public void onTestStart(ITestResult result) { SetUpOperation setUpOperation = this.getAnnotation(result, SetUpOperation.class); if (setUpOperation == null) { return; } DatabaseOperationType type = setUpOperation.value(); String extension = setUpOperation.extension(); String pathName = setUpOperation.pathname(); readFileForDatabase(result, type, extension, pathName); } protected <T extends Annotation> T getAnnotation(ITestResult result, Class<T> annotationClass) { ITestNGMethod testNGMethod = result.getMethod(); Method method = testNGMethod.getMethod(); return method.getAnnotation(annotationClass); }
onTestStart()には引数としてITestResultオブジェクトが渡されます。
この中に、今から実行するテストメソッドが入っています。
呼び出しているgetAnnotation()メソッドを見てみましょう。
ITestResult#getMethod()を呼び出します。この戻り値はjava.lang.reflect.Methodではなく、ITestNGMethodです。
ITestNGMethodからアノテーションはとれないので、ITestMethod#getMethod()を呼び出してMethodを取得します。
そして、Method#getAnnotaion(Class)でアノテーションクラスであるSetUpOperation.classを渡し、アノテーションを取得します。
もし、@SetUpOperationがついていないメソッドなら、nullが返ります。
あとはアノテーションから属性の値(DatabaseOperationType, pathname, extension)を取得し、ファイルを読み出してデータベースに登録してます(readFileForDatabase())。
このように、リスナーとアノテーションを使えば、うまくテスト実行前の初期化処理ができます。
こういう仕組みは、JUnitにはないので、TestNGの利点だと思います。
ちなみに、作ったリスナーを利用するには、testng.xmlに記述します。
<suite name="DbUnitNG"> <listeners> <listener class-name="org.dbunitng.listeners.DbUnitNGTestListener"></listener> </listeners> </suite>
DbUnitライブラリでネストしたBeanをDateSetに変換できます
ライブラリDbUnitNGで、
ネストしたBeanをDbUnitのデータセットに変換できるように対応しました。
ネストしたBeanとは次のようなことを意味してます。
あるBeanのプロパティとして、
- 別のBeanを持つ
- 別のBeanをListで持つ
- 別のBeanを配列で持つ
テストコードも記述して、コミットしています。
ネストしたBeanをどんなDataSetにしているかというと、Beanの型ごとにDbUnitのテーブルとしています。
つまりこういうクラス
public class SampleListBean implements Serializable { private String name; private List<SampleNestedBean> list; // setter and getters... }
があると、データセットはこうなります。
- SampleListのテーブル
- SampleNestedBeanのテーブル
SampleListBeanのリストをデータセットに変換すると、各要素が持つNestedBeanはすべて同じテーブルになります。
たとえば、SampleListBeanのリストがこうだとします。
List -SampleListBean1 -NestedBean1 -NestedBean2 -NestedBean3 -SampleListBean2 -NestedBean4 -NestedBean5
これをデータセットに変換すると、次のXMLで記述したデータセットと等価になります。
<dataset> <samplelistbean name="SampleListBean1" /> <samplelistbean name="SampleListBean2" /> <samplenestedbean name="NestedBean1" /> <samplenestedbean name="NestedBean2" /> <samplenestedbean name="NestedBean3" /> <samplenestedbean name="NestedBean4" /> <samplenestedbean name="NestedBean5" /> </dataset>
SampleListBeanとNestedBeanは別のテーブルになりますし、その結果アサーションも別に行うことができます。
けっこう便利なんじゃないかなー。
これでDAOの戻り値とかアサーションできるし。
DbUnitアノテーションを使えば、Clean Insertも簡単にできる。
たとえプロジェクトがJava5でなくても、個人でテストする分には使えるわけだし。
明日からまたドキュメント整備やね。
TestNG+DbUnitNGライブラリのテストコードを記述しました
BeanのListからDataSetへ変換する部分について、テストコードを記述しました。
TestNGでテストしてます。
Beanがネストしない(Beanが他のBeanをプロパティとして持たない)限り、
リストをデータセットに変換できるようになってます。
# いくつかバグを修正しました。
BeanがDateやBigDecimalをプロパティに持っても、変換してDataSetの形でアサーションできるので、けっこう実用的だと思います。
ライブラリはここにあります。DbUnitNG プロジェクト日本語トップページ - SourceForge.JP
trunkをダウンロードすればJavadocも見れます。
DbUnitでファイル(XML,Xls)に日付を書くときの書式
DbUnitで期待値をXMLやExcelに記述すると思うけど、
日付の書式は基本的にDate.valueOf()やTimestamp.valueOf()で有効な書式を記述する。
こんな感じ。
<?xml version='1.0' encoding='UTF-8'?> <dataset> <datebean javaUtilDate="2008-08-08" javaSqlDate="2008-08-08 15:20:11" timestamp="2008-08-08 15:20:11.000" /> </dataset>
試したけど、java.util.Dateでもjava.sql.Dateでも次の2つのパターンが有効。
- yyyy-MM-dd
- yyyy-MM-dd HH:mm:ss(.SSSSSSSSS)*1
*1:ミリ秒は1〜9桁まで記述可
DbUnitのFlatXmlDataSetの注意点
DbUnitのFlatXmlDataSetはXMLからDataSetを生成するときに利用する。
だけど1点注意。
FlatXmlDataSetは1つ目の要素の属性をテーブルのカラムとする。
だから、こういうのはダメだ。
<?xml version='1.0' encoding='UTF-8'?> <dataset> <simplebean i="0" /> <simplebean string="" i="1" /> <simplebean string="a" i="-1" /> </dataset>
カラムstringをnullにしたいがために、1つめの要素にstring要素を記述していない。
(記述して何も値を書かなかったら、nullではなく空文字が値になる)
結果として生成されるテーブルは、こうなる。
行番号 | i |
---|---|
0番目 | 0 |
1番目 | 1 |
2番目 | -1 |
stringはカラムに含まれない。
nullのカラムがある場合は、2つ目以降の要素にするか、DTDを書くこと。
まあテストだし、2つ目以降にするのがいいと思う。
TestNGとJUnit4における、テストメソッドへのパラメータの考え方の違い
たとえば数値の引数を2つとって、それを足し算したりかけ算したりするメソッドをテストするとして。
- public int add(int a, int b)
- public int multiple(int a, int b)
みたいな。
こういうメソッドをテストする場合、引数のバリエーションを考えてテストする必要がある。
TestNGでもJUnit4でもこうしたバリエーションをパラメータとしてテストメソッドに渡す仕組みがあるが、個人的にはTestNGの方が進んでいると思う。
じゃあ実際にコードで違いを確認しよう。
さっきの足し算、かけ算するクラスと、それをJUnitでテストするクラスを作る。
足し算を3パターン、テストする。
import java.util.Arrays; import java.util.List; import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameters; class Sample { public int add(int a, int b) { return a + b; } public int multiple(int a, int b) { return a * b; } } @RunWith(Parameterized.class) public class SampleTestByJUnit { private int a; private int b; private int expected; public SampleTestByJUnit(int a, int b, int expected) { this.a = a; this.b = b; this.expected = expected; } @Parameters public static List<Object[]> testData() { Object[][] datas = new Object[][] { { 1, 2, 3 }, { 0, 0, 0 }, { 4, 5, 9 } }; return Arrays.asList(datas); } @Test public void testAdd() { Sample sample = new Sample(); Assert.assertEquals(sample.add(a, b), expected); } }
JUnit4でテストクラスにパラメータを渡すには。
- テストクラスに「@RunWith(Parameterized.class)」をつける
- 戻り値がコレクションでstaticなメソッドを作る
- ↑のメソッドに「@Parameters」をつける
- コンストラクタの引数でコレクションの要素を受け取る
- コンストラクタで引数の値をインスタンス変数に格納する
- テストメソッドでそのインスタンス変数を利用する
となる。今回、1つのテストで複数のパラメータを渡すために、コレクションの要素はObject配列にした。
JUnit4の考え方のキモは、あくまでテストメソッド自体は引数を取らないということだ。
だからコンストラクタでパラメータを受け取っている。
対して、TestNGではこうなる。
import org.testng.Assert; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; public class SampleTestByTestNG { @DataProvider(name = "test-data") public Object[][] testData() { Object[][] datas = new Object[][] { { 1, 2, 3 }, { 0, 0, 0 }, { 4, 5, 9 } }; return datas; } @Test(dataProvider = "test-data") public void testAdd(int a, int b, int expected) { Sample sample = new Sample(); Assert.assertEquals(sample.add(a, b), expected); } }
- 戻り値がObjectの2次元配列のメソッドを作る(staticである必要はない)
- ↑のメソッドに「@DataProvider」をつけ、name属性に任意の文字列を指定する。
- テストメソッドのdataProvider属性にDataProviderのnameを指定する
- テストメソッドの引数で配列の要素を受け取る
配列の要素はまたObject配列なので、Object配列の要素を1つずつ引数として受け取ってる。
TestNGの考え方のキモは、テストメソッド自体がパラメータを受け取るということだ。
どっちでもいいじゃん、というかもしれない。
けど、次かけ算するmultiple()メソッドのテストをするときはどう??
JUnitではパラメータのメソッドとテストメソッドに直接の関連がないため、
かけ算テストのパラメータを作ろうとしても、1つのパラメータメソッドを共有したり、コンストラクタを変更したりしないといけない。
対して、TestNGであれば、別のパラメータメソッドを作り(nameは別の文字列にして)、
かけ算のテストメソッドでそれを指定すればよい。
なので、この点はTestNGが進んでるな、と思う。
BeanのリストをDbUnitのDataSetに変換する
過去のエントリでBeanのListとDbUnitのDataSetの比較をなんとかしたいと思ったわけだけど。
DbUnitの弱点はBeanのListとDataSetを比較できないこと - Fight the Future じゅくのblog
BeanのListとDbUnitのDataSetを比較する方法を考える - Fight the Future じゅくのblog
実装してみた。
まだネストしたBeanには対応してないけど、よくあるDTOみたいなフラットなBeanのListならDataSetにできる。
使い方は簡単。
DeptDao deptDao = new DeptDao(); List<Dept> actual = deptDao.listAllDept(); BeanListConverter converter = new BeanListConverter(actual); IDataSet dataSet = converter.convert();
この場合、Deptクラスのリストをデータセットに変換してる。
ちなみにDeptクラスはただのBean。
public class Dept implements Serializable { private int deptno; private String dname; private String location; // has getters and setters... }
こうやってBeanのListをDataSetに変換してしまえば、
DbUnitのAssertion#assertEquals(IDataSet, IDataSet)でアサートできます!
List<Dept> actual = deptDao.listAllDept(); BeanListConverter converter = new BeanListConverter(actual); IDataSet dataSet = converter.convert(); IDataSet initialDataSet = new FlatXmlDataSet(new File("dept.xml")); Assertion.assertEquals(initialDataSet, dataSet);
試したい方はこちらから!DbUnitNG プロジェクト日本語トップページ - SourceForge.JP
intやStringといった単純なプロパティのものしか試してないので、
DateやBigDecimalみたいなものもテストしなくては。
今からTestNGでテストケース作っていきます!
実装方法
Seasar2のBeanReaderやBeanDescクラスなどをかなり参考にさせてもらいました。
Beanのプロパティをリフレクションで収集して、
それを元にListの各要素であるBeanから値を取り出してます。
このライブラリでは、Beanが持つ1つ1つのプロパティをBeanPropertyクラスとし、
あるBeanが持つプロパティをすべて保持するクラスとしてBeanMetaDataクラスをつくりました。
つまり、BeanMetaDataがBeanPropertyを集約している感じです。
ちょっと抜粋してみると(コメントも主要なとこだけ抜粋)。
public class BeanProperty { /** * @param n プロパティ名 * @param t プロパティの型 * @param f プロパティのフィールド * @param g プロパティのGetter * @param s プロパティのSetter */ public BeanProperty(String n, Class<?> t, Field f, Method g, Method s) {...} /** * プロパティの値を返す。 * @param target プロパティを持つオブジェクト */ public Object getValue(Object target) {...} } public class BeanMetaData { /** メタデータを収集するクラスを渡す */ public BeanMetaData(Class<?> clazz) {...} /** プロパティ名を渡す */ public BeanProperty getProperty(String name) {...} }
値は、適切なBeanPropertyにBeanのインスタンスを渡せば取得できる。
で、DbUnitのデータセットに含まれるITableの実装クラスとして、BeanListTableクラスを作ってます。
これが、BeanのListをDbUnitのテーブルにしたものです。