業務でアプリケーションプログラミングを行っている人で、データベースを使わない案件はまれでしょう。ほとんどがデータベースに顧客データを格納したり、マスターデータを引っ張ってきたりするはずです。

そんなデータベースとのやり取りで、SQL文を書いたが、それがほんとうに合っているのか?、単体テスト時にテストデータを毎回手動でデータベースに入れては、プログラムで引っ張ってきた値と等しいかをチェックするのはばかばかしい、データを更新してはみたが、ほんとうに更新されているのかを確認しないといけない、なんて作業はとてもめんどくさくて憂鬱な作業ですよね。

そんな時に威力を発揮するのが、DBUnitという、データベースに関係する単体テストを自動で行ってくれるツールです。 もちろん、どんなテストケースがあるのかは自分で書かなければいけませんが、意図しているデータや、更新されたはずのデータはすべて外部のXMLファイルに書くため、プログラマとテスト実施者が別々に作業することも可能です。

DBUnitは開発環境に依存しませんので、EclipseなどのIDEを使っているかテキストエディタを使っているかは問題にしません。

特徴

DBUnitの特徴は以下のようなものです。

  • テスト用のテーブルの自動生成と自動削除機能
  • テストデータの自動insertと自動delete機能
  • データベース内を直接見なくても、データの比較を行ってくれる
  • テストデータとテストコードの分離(テストデータはXMLファイルとして記述できる)
  • DTDをエクスポートできる(多少のプログラミングが必要[5処理くらい])

DBUnitはテストデータを外部のXMLファイルに記述することができます。もちろん、プログラム中に埋め込むこともできますが、テストコードとテストデータを分離して開発すれば、プログラマーとテスト担当者に作業を分割することもできます。

XMLBuddy

テストデータを記述するこの作業はルーチンワークになりますので、テストエディタで処理するのは次第に苦痛になってくるでしょう。そんな時におすすめなのが、EclipseのプラグインであるXMLBuddyです。XMLBuddy はXMLエディタとしてEclipseプラグインで提供されています。記述の補佐や要素をツリー形式で表示してくれたりします。

DBEdit

これらをEclipseで使用する場合はついでにDBEditもインストールすると良いでしょう。DBEditはEclipseプラグインでデータベースの中身をEclipseから見ることができるツールです。テーブルの関連などもボタン一発で参照できますし、データの追加・変更・削除もEclipse上からできるようになります。

まとめ

DBUnitを使ったテストは最初はデータベースの設定やDTDの読み込みなどの手間が発生します。これはこれからくる簡単な単体テストの甘い蜜の下準備としてがんばって設定してください。この設定が終われば後は、XMLファイルにテストデータを記述してDBUnitを実行するだけです。テーブルを自動で作成してくれて、XMLファイルに書かれたテストデータを自動でinsertしてくれ、自分の書いたselect文と比較して間違っていいないか確認してくれます。

このツールの恩恵は、単体テストではもちろん、納品後の保守段階でバグが出た場合にも受けることができます。納品後というのは、実際にシステムが稼動していることがしばしばあります。そんな時に、実データに手を入れてテストを行うのは至難の業です。DBUnit を使うと、多少テストが簡単になるかもしれません。

Tips
DBUnitを使うことでテストデータを自動でinsertしてくれ、insertしたデータは単体テスト終了後に自動的に削除するという設定もできます。

今まで複雑で、めんどくさかったデータベースの単体テストがこのDBUnitを使って簡単になるといいなと思います。

DBUnit で使う表定義の DTD ファイルを自動生成する

DBUnitには表定義を DTD ファイルとして作成する機能があります。少しソースコードを書かないといけないので、サンプルコードを載せておきます。間違ってたらもうしわけないです。

DTDExporter.java

import java.io.*;
import java.sql.*;
import org.dbunit.database.DatabaseConnection;
import org.dbunit.database.IDatabaseConnection;
import org.dbunit.dataset.xml.FlatDtdDataSet;
 

public class DTDExporter {
  /** エクスポートするファイル名 */
  private static final String EXPORT_FILENAME = "tables.dtd";
  /** スキーマ名 */
  private static final String SCHEMA = "schema";
  /** 接続するデータベースへの情報(PostgreSQL用)情報適宜変更すること */
  private static final String JDBC_DRIVER = "org.postgresql.Driver";
  private static final String JDBC_URL = "jdbc:postgresql://YOUR_HOST/DATABASE_NAME";
  private static final String JDBC_USER = "postgres";
  private static final String JDBC_PASS = "*******";
  /** 接続するデータベースへの情報(MySQL用) */
//  private static final String JDBC_DRIVER = "org.gjt.mm.mysql.Driver";
//  private static final String JDBC_URL = "jdbc:mysql://YOUR_HOST/DATABASE_NAME";
//  private static final String JDBC_USER = "mysql";
//  private static final String JDBC_PASS = "*******";
 
  public static void main(String[] args) throws Exception {
    if (args.length "使い方: java DTDExporter TABLE_NAME [TABLE_NAMES]");
      System.exit(-1);
    }
    Class.forName(JDBC_DRIVER);
    IDatabaseConnection con = 
      new DatabaseConnection(DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASS), SCHEMA); 
    FlatDtdDataSet.write(con.createDataSet(args), new FileOutputStream(EXPORT_FILENAME)); 
  }
}

これを実行してできたDTDファイルを、XMLBuddy で利用することで、テストデータを XMLBuddy で作成するときに補完機能が効くようになります。XMBuddy で作成したテストデータ記述用ファイルの先頭に次の記述を行います。

<?xml version="1.0"?> 
<!DOCTYPE dataset SYSTEM "tables.dtd">

これでこれでいくらかテストデータを作成する効率が上がると思います。

Tips
XMLBuddyで補完を使う場合、「.xml」ファイルのエディターを「XMLBuddy」にしておく必要があります。Eclipse メニューの 「ウィンドウ」→「設定」→「ワークベンチ」→「ファイルの関連付け」 で .xml のデフォルトエディターを XMLBuddy に設定しておくと便利です。

ハマりそうなポイント

例外に対応する

XML ファイルの読み込み例外が発生した場合

XMLファイルの読み込み時に以下のような例外が発生した場合の対応方法です。

org.dbunit.dataset.DataSetException: Line 2: 基本 URI を使用せずに、相対 URI "tables.dtd" を解決することはできません。

XML ファイルの DTD 読み込み部分を書き換えてみてください。

<?xml version="1.0"?>
<!DOCTYPE dataset SYSTEM "file:///C:/Documents and Settings/hamasyou/test/tables.dtd">

上のように URI の部分を絶対パスで書いてみてください。環境に依存しますが、テストコード用ですので OK としましょう。

データベースコネクションが取得できない場合

データベースコネクションを取得できない場合や、DTD を吐き出せない場合に下記の例外が出たら、コネクション取得時にスキーマ名を指定してみてください。

org.dbunit.database.AmbiguousTableNameException

Operationの種類

DBUnitの実行時に呼び出される getDataSet() メソッドの挙動を変更できる Operationの種類です。

Operation の種類
Operation 動作
INSERT 指定したデータセットをテーブルに挿入します。テーブル内で同じ主キーとなるデータがすでに存在する場合は、エラーになります。外部キーなどの参照整合性を保つようにデータセットの順序を指定する必要があります。
UPDATE 指定したデータセットの同じ主キーとなるデータを上書きします。上書きするデータが存在しない場合エラーになります。テストデータ以外が存在する場合、データの上書きに注意してください。
DELETE 指定したデータセットとマッチするものだけを削除します。データセットに含まれていないデータは、削除されません。
DELETE_ALL 指定したデータセットに存在するテーブルのレコードすべてを削除します。テーブル自体は削除されません。データセットの逆順にデータが削除されていきます。
CLEAN_INSERT 指定したデータセットに存在するテーブルに対して、DELETE_ALLを行った後、INSERTを行います。データセットに含まれるデータのみでテストしたい場合に使います。既存のデータはすべて削除されてしまいます。
REFRESH 指定したデータセットの主キーにマッチするデータを更新します。更新するデータがない場合は、挿入されます。ほとんどのデータベーステスト時に使用できますが、既存データの更新にだけは注意する必要があります。
TRANCATE_TABLE 指定したデータセットに含まれるテーブルが削除されます。テーブルに格納されているデータもすべて削除されるので、注意してください。
NONE 何も行わない処理です。

動作に関しての注意点

データの削除に注意!
保守段階で、DBUnitを使う場合、既存のデータの更新や削除に注意してください。間違ってもテーブルの削除(TRANCATE_TABLE)などしないように!
わかってないこと
  • データセットで、DEFAULT 値の設定してある列に DEFAULT 値を入れる方法がみつかりません・・・
  • シーケンス値が主キーになるようなデータの挿入の方法がわかりません・・・
  • テーブルの列が Number などの数値型の場合、比較時に文字として認識されてしまう
  • Timestamp 型は、正確に同じデータを入れておかないと、比較に失敗する

特殊な値をテストデータとして入力するには?

現在時刻をテストデータとして入力する方法

テストデータをXMLに書く場合、Timestamp型の列には現在日時を入れたいときがある。そんな場合は、ReplacementDataSet を使用する。データセットに例えば次のように書いておく。

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE dataset SYSTEM "tables.dtd">
<dataset>
  <TABLENAME TODAY="[SYSDATE]" ID="1"/>
  <TABLENAME TODAY="2004-06-10" ID="2"/>
</dataset>

[SYSDATE] の部分を ReplacementDataSet#addReplacementObject() を使って置き換えることで、任意の値でテストすることができます。

テストクラス

protected IDataSet getDataSet() throws Exception { 
  ReplacementDataSet dataSet = 
    new ReplacementDataSet(new FlatXmlDataSet(new FileInputStream("dataset.xml"))); 
  dataSet.addReplacementObject("[SYSDATE]", 
    new Timestamp(System.currentTimeMillis())); 
  return dataSet; 
} 

テストデータに null を入れる方法

上記「現在日時をテストデータに使用する」と同様の方法が使える。

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE dataset SYSTEM "tables.dtd">
<dataset>
  <TABLENAME TODAY="[NULL]" ID="1"/>
  <TABLENAME TODAY="2004-06-10" ID="2"/>
</dataset>

[NULL]の部分をReplacementDataSet#addReplacementObject() を使って置き換えることで、任意の値でテストすることができます。

テストクラス

protected IDataSet getDataSet() throws Exception { 
  ReplacementDataSet dataSet = new ReplacementDataSet(new FlatXmlDataSet(new FileInputStream("dataset.xml")));
  dataSet.addReplacementObject("[NULL]", null); 
  return dataSet; 
} 

テストデータに ランダムな値を入れる方法

上記「現在日時をテストデータに使用する」の応用でいけると思います。

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE dataset SYSTEM "tables.dtd">
<dataset>
  <TABLENAME TODAY="[RANDOM]" ID="1"/>
  <TABLENAME TODAY="2004-06-10" ID="2"/>
</dataset>

[RANDOM] の部分を ReplacementDataSet#addReplacementObject() を使って置き換えることで、任意の値でテストすることができます。

テストクラス

protected IDataSet getDataSet() throws Exception { 
  ReplacementDataSet dataSet = new ReplacementDataSet(new FlatXmlDataSet(new FileInputStream("dataset.xml")));
  dataSet.addReplacementObject("[RANDOM]", "" + new Random().nextInt()); 
  return dataSet; 
} 

テストコードサンプル

DBUnitのサンプルコードです。感じだけでも大体つかめてもらえたらと思います。

テストコードサンプル

package com.hamasyou.domain.test;

import java.io.; import junit.framework.; import org.dbunit.; import org.dbunit.database.; import org.dbunit.operation.; import org.dbunit.dataset.; import org.dbunit.dataset.xml.*; import com.hamasyou.domain.Form;

/* テストクラス / public class FormTest extends DatabaseTestCase { private IDatabaseConnection connection; protected void setUp() throws Exception { super.setUp(); connection = getConnection(); }

protected void tearDown() throws Exception { connection.close(); super.tearDown(); }

protected IDatabaseConnection getConnection() throws Exception { return new DatabaseConnection(getJDBCConnection(), “SCHEMA”); }

protected IDataSet getDataSet() throws Exception { ReplacementDataSet dataSet = new ReplacementDataSet(new FlatXmlDataSet(new FileInputStream(“dataset.xml”))); dataSet.addReplacementObject(“[SYSDATE]”, new Timestamp(System.currentTimeMillis())); return dataSet; }

protected DatabaseOperation getSetUpOperation() throws Exception { return DatabaseOperation.REFRESH; }

protected DatabaseOperation getTearDownOperation() throws Exception { return DatabaseOperation.DELETE; }

/* * データベースコネクションを返します。 * @param autoCommit 自動コミット(true/自動コミットOn, false/自動コミットOff) / private Connection getJDBCConnection(boolean autoCommit) throws Exception { Class.forName(System.getProperty(“test.jdbc.driver”)); String url = System.getProperty(“test.jdbc.url”); String user = System.getProperty(“test.jdbc.user”); String pass = System.getProperty(“test.jdbc.pass”); Connection con = DriverManager.getConnection(url, user, pass); con.setAutoCommit(autoCommit); return con; }

/=====================================/ / テストケース / /=====================================/

/* * フォームを期待通りに取得できるかどうかのテスト / public void testGetData() throws Exception { Form form = Form.getData(); // 期待するデータを読み込む IDataSet expectedDataSet = new FlatXmlDataSet(new FileInputStream(“expect-data.xml”)); ITable expectedTable = expectedDataSet.getTable(“FORM”); // アプリケーションID、フォームID、フォーム名が一致した場合同じ物とみなす assertEquals(“アプリID”, expectedTable.getValue(0, “APP_ID”), form.getAppId()); assertEquals(“フォームID”, expectedTable.getValue(0, “FORM_ID”), form.getFormId()); assertEquals(“フォーム名”, expectedTable.getValue(0, “FORM_NAME”), form.getFormName()); } }

テストデータを Excel で作る

テストデータを Excel で作ることができます。Excelで作ったデータを読み込むには、XlsDataSet というクラスを使います。このクラスは、内部で Jakarta POI を使っています。

Jakarta POIプロジェクト

POI とは、Javaから Microsoft Word や Microsoft Excel を扱うためのライブラリです。Jakarata POIプロジェクトでは、Microsoft OLE 2複合ドキュメント形式に基づいた様々なファイル形式を Pure Java で取り扱うためのAPI群から成り立つプロジェクトです。

Jakarta POI のダウンロードはこちら

Excelでどのようにテストデータを作るかというと、シートごとにテーブルに挿入するデータを作成します。シート名には、挿入するテーブル名をそれぞれ書きます。シートの最初の行に、テーブルのカラム名(列名)を並べて書きます。2行目から挿入するデータを書き並べます。下はサンプル画像です。

Excelサンプル

Excel のセルの書式設定によって、XlsDataSet での値の取り扱い方が違います。数値は BigDecimal 型、日付は Date 型、文字列は String 型で扱われます。

数字を文字として扱いたい場合
数字を文字列として扱いたい場合は、セルの書式設定を「文字列」に設定します。文字列として扱われていれば、Excel2003 であればセルの左上に緑の三角が現れます。

現時点では、POI が未熟なせいか、まだまだ不安定な部分もあります。(特定の列名の後に空白が入ってしまうなど)それでも、Excelを使ってテストデータを作れるのは、非常に強いと思います。

データセットをSQLを使って取得する

QueryDataSet クラスの addTable メソッドを使うと、SQL 文を使ってデータセットを作り出すことができます。

QueryDataSet dataSet = new QueryDataSet(getConnection());
dataSet.add(“TABLE_NAME”, “select * from TABLE_NAME where ID = ‘1’”);

データベース接続からデータセットを取得する方法もあります。

getConnection().createDataSet()

Oracle の Timestamp 型が Insert できない

WARNING - CREATE_DATE data type (1111, ‘TIMESTAMP(6)’) not recognized and will be ignored. See FAQ for more information. 

こんな警告が表示されるのは、データ型にデフォルトのJDBC 型が使われているのが原因です。例えば Oracle を使っている場合は、次のようにしてデータ型を Oracle のものに指定してやることで回避できます。

IDatabaseConnection con =
  new DatabaseConnection(DriverManager.getConnection(JDBC_URL, JDBC_USER, JDBC_PASS), SCHEMA);
DatabaseConfig config = con.getConfig();
config.setProperty(DatabaseConfig.PROPERTY_DATATYPE_FACTORY, new OracleDataTypeFactory());

このように設定を指定することで、データベース独自の型を使えるようになります。

効率の良いテストにするには

DBUnitを使って効率のよいテストを行うには、下の3点に気をつけることで効率があがると思います。

  • 自分専用のデータベースを利用する
  • テスト終了後にデータを削除しない
  • 接続処理などを一元管理する

自分専用のデータベースを持つ

DBUnitのひとつの弱点に、データベースの状態にテストが依存してしまう>いう点があげられます。これは、複数の開発者が同じデータベースを使ってテストを行っていると顕著になってきます。

そこで、データベースの状態にできるだけ依存しないテストを行うために、自分専用のデータベースを利用する方法があります。最近では、Mckoi といった、ポータブルデータベースがありますので、簡単に自分専用のデータベースを作ることができると思います。

この場合の注意点は、最新の DDL を使って自分のデータベースを作らないと、本番データベースとバージョンが違うものができてしまうということです。

参考

テスト終了時にデータを削除しない

自分専用のデータベースを使えば心配はないのですが、開発者共通のデータベースを使ってテストしていた場合、間違ってすべてのデータを消してしまうということがあります。DBUnitの場合、コマンドをひとつまちがえるだけで簡単にデータがすべて消えてしまいます。

こういった間違いを起こさないために、テスト終了時のデータは残しておくのがいいと思います。デバッグ時に実際のデータを確認するときにもつかえることですし。

TRANCATE_TABLE に注意
TRANCATE_TABLE を行うと、テストに使ったテーブルが削除されてしまいます。本番で使っているデータベースでは、必ず確認して、実行しないようにしましょう。

接続処理を一元管理する

DBUnitを使ってテストケースを書く場合、DatabaseTestCase クラスを継承して作り始めます。このクラスでは、データベースコネクションを取得するメソッドとデータセットを取得するメソッドをオーバーライドしなければなりません。また、テスト開始時のオペレーションやテスト終了時のオペレーションも、適宜オーバーロードしなければ、デフォルトでは「何もしない」設定になっています。

毎回これらの処理を書くのは、非常にめんどくさいです。そこで、DatabaseTestCase を継承し、デフォルトの実装を行って独自テストケースクラスを作成し、そのクラスを個々のテストケースに継承させます。

参考文献:

  • DBUnit、DBEdit、XMLBuddyをつかったデータベースプログラミングはこちらが参考になります。 JavaPRESS34(技術評論社)

  • DBUnitの本家Webサイトです。膨大な情報があります。 DBUnit

  • テスティングモジュールのJUnitも参考になります。

  • DbUnitに関して、分かりやすく解説されています。 DbUnit (Stack*)