Fight the Future

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

社内勉強会で発表したJUnit4&TestNGの資料を公開します

日曜日に社内勉強会をしました。
僕は株式会社クロノスに所属しているんですが、「クロノスエンジニアの会」略して「エン会」というタイトルで勉強会をしました。
今までの勉強会のスタイルと変えて、デブサミのようなイメージで社内の有志数人で、様々な内容の発表をしました。
幹事的な役割もして、セッション一覧とかもお遊びでデブサミっぽくしたりしました。


僕は「Beyond JUnit3」ということで、JUnit3の弱点と、JUnit4とTestNGの紹介や比較をソースコードを使って解説してみました。
せっかくなので、その資料とソースコードを公開します。
あんまり大したのじゃないんですけど、よかったら活用してください。


SlideShareにアップしました。
http://www.slideshare.net/jyukutyo/beyond-junit3-presentation/


ソースコードはMediaFireにアップしたので、ここからダウンロードしてください。

TestNGのテストをAntで実行する

<!-- TestNGのJARファイルに含まれるtestngtasksファイルを指定する -->
<taskdef resource="testngtasks" classpath="lib/testng-5.8-jdk15.jar" />

<!-- 実行時のクラスパスに含めるJARファイルを指定する -->
<path id="run.cp">
  <fileset dir="lib" includes="*.jar"/>
</path>

<target name="run-tests">
  <testng classpathref="run.cp" haltOnfailure="true">
    <!-- テストクラスのクラスファイルをクラスパスに含める -->
    <classpath>
      <pathelement location="bin"/>
    </classpath>
    <!-- testng.xmlを指定する -->
    <xmlfileset dir="test" includes="testng.xml" />
  </testng>
</target>

testng.xmlではなくテストクラスを実行する場合、xmlfilesetではなくclassfileset要素を使います。

<classfileset dir="${test.build.dir}" includes="**/*.class" />

DbUnitNG0.4リリース!

TestNG+DbUnitライブラリであるDbUnitNGのバージョン0.4をリリースしました。
ダウンロード - DbUnitNG - SourceForge.JP
DbUnitNGは、TestNGDbUnitを連携させ、SetUpやTearDownのアノテーション化、BeanのListをDbUnitのデータセットへ変換といったことをする小さなライブラリです。


今回の追加点は以下のとおり。

DBに接続して、任意のテーブルあるいは全テーブルをDbUnitの形式でファイルに出力するユーティリティクラスを作りました。
org.dbunitng.data.TestDataFileMakerクラスです。

初期値や期待値のファイルとしてCSVにも対応しました(他はXMLとExcel)。

期待値のファイルとデータベースのテーブルをアサートするアノテーションを作りました。
@TableAssertです。

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

AssertionHelperクラスを使ってアサートする場合、
[null]と記述すると、nullとして扱います。


(追記)Wikiも更新しました。ここに書いたことはすべてWikiにまとめています。

DbUnitライブラリでDBデータをXML,Excel,CSVに出力するクラスを作りました

DBに接続して、任意のテーブルあるいは全テーブルをDbUnitの形式でファイルに出力するユーティリティクラスを作りました。
org.dbunitng.data.TestDataFileMakerクラスです。
使い方は簡単です。

public TestDataFileMaker(String driver, String url, String userName,
			String password, String schema) {

コンストラクタにJDBC接続に必要な情報を渡します。スキーマは特に必要なければnullを渡してください。
メソッドは2つあります。

public void extractTables(String targetDirectory, String[] tableNames,
			FileType type)
public void extractAllTables(String targetDirectory, FileType type)

特定のテーブルだけ出力したい場合extractTables()を、全テーブルを出力する場合はextractAllTables()を呼び出してください。
引数は、targetDirectoryにファイルを出力するディレクトリを渡してください。存在していない場合はディレクトリを作成して出力します。
tableNamesはテーブル名の配列です。
FileTypeはEnumで、XML、EXCEL、CSVから選択してください。この形式でファイルを出力します。
たとえば、こんなテーブルがあるとします。

mysql> select * from dept;
+--------+------------+----------+
| DEPTNO | DNAME      | LOC      |
+--------+------------+----------+
|     10 | ACCOUNTING | NEW YORK | 
|     20 | RESEARCH   | DALLAS   | 
以下省略
mysql> select * from emp;
+-------+--------+-----------+------+------------+---------+---------+--------+
| EMPNO | ENAME  | JOB       | MGR  | HIREDATE   | SAL     | COMM    | DEPTNO |
+-------+--------+-----------+------+------------+---------+---------+--------+
|  7369 | SMITH  | CLERK     | 7902 | 1980-12-17 |  800.00 |   10.00 |     20 | 
|  7499 | ALLEN  | SALESMAN  | 7698 | 1981-02-20 | 1600.00 |  300.00 |     30 | 
以下省略

こういうコードでファイルに出力できます。

TestDataFileMaker maker =
	new TestDataFileMaker(
		"com.mysql.jdbc.Driver",
		"jdbc:mysql://127.0.0.1/testframework",
		"user",
		"password",
		null);
String targetDir = "temp/xml/specified";
String[] tableNames = new String[] { "dept", "emp" };
maker.extractTables(targetDir, tableNames, FileType.XML);

するとtemp/xml/specifiedディレクトリにファイルを出力します。

temp/xml/specified
 -DEPT.xml
 -EMP.xml

内容もきちんとDbUnit形式です。

<?xml version='1.0' encoding='UTF-8'?>
<dataset>
  <DEPT DEPTNO="10" DNAME="ACCOUNTING" LOC="NEW YORK"/>
以下省略

同様に、Excel形式でもCSV形式でも出力できます。
CSVであれば「table-ordering.txt」も出力します。

DbUnitライブラリでCSV形式のファイルにも対応しました

TestNG+DbUnitライブラリDbUnitNGで、初期値や期待値のファイルとしてCSVにも対応しました(他はXMLとExcel)。


意外に知らないDbUnitでCSVを使う方法 - Fight the Future じゅくのblogにあるように、DbUnitではCSVファイルの場合「table-ordering.txt」というファイルが必須なので、このファイルをアノテーションに指定した場合、CSVとして処理します。
「table-ordering.txt」は定数としてDbUnitNGConstrantsにあります。
こんな感じです。

@SetUpOperation(pathname = DbUnitNGConstrants.CSV_ORDER_FILE, value = DatabaseOperationType.CLEAN_INSERT)
public void testCsvFile() {
}

pathname属性にファイル名「table-ordering.txt」だけ指定しているので、テストクラスと同じパッケージに配置します。
「table-ordering.txt」はこんな感じ。

dept
emp

テーブル名を記述します。そして、この名前に対応するCSVファイルをテキストファイルと同じパッケージに配置します。dept.csvとemp.csvです。

DEPTNO,DNAME,LOC
10,ACCOUNTING,NEW YORK
EMPNO,ENAME,JOB,MGR,HIREDATE,SAL,COMM,DEPTNO
7782,CLARK,MANAGER,7839,1981-01-09,2450.0,null,10

1行目は列名、2行目以降にデータを記述します。nullの場合はそのままnullと書きます。

意外に知らない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なのにインタフェースの実装クラスを意識してダウンキャストなんて、一番ダサいプログラミングだよね。。。
他にいい方法が思いつかなかった。。。