フューチャー技術ブログ

DBUnitでいろいろはまった

本ブログは、ソフトウェアテスト - Qiita Advent Calendar 2024 - Qiita の4日目です。

3日目は、ぱいん (pinecandy)さんのテストについて相談を受けたときにいつもしていること - 飴ブロ(仮です。

Javaでデータベースを使うプロジェクトだったのでDBUnit使うぜ、と導入したのですが、細かいところで引っかかったりしたので備忘メモです。

org.dbunit.database.AmbiguousTableNameException

このプロジェクトでは複数のスキーマがあり、一部重複したテーブル名があったため、このような例外が出ていました。共通スキーマsystemというのがあったとしたら、それをJdbcDatabaseTesterに渡して設定します。

databaseTester = new JdbcDatabaseTester(this.dataSourceDriver,
this.dataSourceUrl,
this.dataSourceUser,
this.dataSourcePassword,
"system"); // これ

IDataSet dataSet = new FlatXmlDataSetBuilder().build(
new File("src/test/resources/dbunit/fixture.xml"));
databaseTester.setDataSet(replacementDataSet);
databaseTester.setSetUpOperation(DatabaseOperation.CLEAN_INSERT);
databaseTester.onSetup();

複数のスキーマにデータを入れたい!

1つのデータを定義したけど、一部のデータは別のスキーマに入れたい、という場合です。取込は2回書くのですが、データセットをデータテスターに渡すときに、フィルターでラップしてから渡します。片方はIncludeTableFilter、もう片方はExcludeTableFilterを使います。

雑にベタに書いたので、もしかしたらFlatXmlDataSetBuilderとかは

      var appOnlyTables = new String[] {
"t_work",
"t_log"};
databaseTesterApp = new JdbcDatabaseTester(this.dataSourceDriver,
this.dataSourceUrl,
this.dataSourceUser,
this.dataSourcePassword,
"app");
IDataSet dataSetApp = new FlatXmlDataSetBuilder().build(
new File("src/test/resources/dbunit/fixture.xml"));

// このフィルターでテーブルを選別
var dataFilterApp = new IncludeTableFilter(appOnlyTables);
var filterdDatasetApp = new FilteredDataSet(dataFilterApp, dataSetApp);

databaseTesterApp.setDataSet(filterdDatasetApp);
databaseTesterApp.setSetUpOperation(DatabaseOperation.CLEAN_INSERT);
databaseTesterApp.onSetup();

databaseTesterSys = new JdbcDatabaseTester(this.dataSourceDriver,
this.dataSourceUrl,
this.dataSourceUser,
this.dataSourcePassword,
"system");
IDataSet dataSetSys = new FlatXmlDataSetBuilder().build(
new File("src/test/resources/dbunit/fixture.xml"));

// このフィルターでテーブルを選別
var dataFilterSys = new ExcludeTableFilter(appOnlyTables);
var filterdDatasetSys = new FilteredDataSet(dataFilterSys, dataSetSys);

databaseTesterSys.setDataSet(filterdDatasetSys);
databaseTesterSys.setSetUpOperation(DatabaseOperation.CLEAN_INSERT);
databaseTesterSys.onSetup();

NULLを表現したい!

DBUnit標準だと、読み込んだとおりに文字列で解釈しますので、空文字列だと空文字列。NULLとか書くと”NULL”という文字列になります。データセットを置換するラッパーを使い、特定のテキストをオブジェクトに変換するラッパーを作成してデータセットにかぶせることで、nullが表現できます。

IDataSet dataSet = new FlatXmlDataSetBuilder().build(
new File("src/test/resources/dbunit/fixture.xml"));

// このフィルターで変換
var replacementDataSet = new ReplacementDataSet(dataSet);
replacementDataSet.addReplacementObject("[NULL]", null);

databaseTesterSys.setDataSet(replacementDataSet);
databaseTesterSys.setSetUpOperation(DatabaseOperation.CLEAN_INSERT);
databaseTesterSys.onSetup();

WindowsとLinuxのフォルダを吸収したい

データベースのログテーブルには処理したファイル名が入るが、WindowsだとC:\になってしまうし、Linuxだと/tmpになってしまうケースに対応する場合も、おなじデータセットを置換するラッパーを使います。

先ほどはaddReplacementObject()でしたが、こちらではaddReplacementSubstring()メソッドです。部分一致で変換できるので[tmp]/deleted-file.txtみたいにデータセットに書いておくと、OSの作業フォルダに書き換えてくれます。

      this.tmpFolderPath = Paths.get(System.getProperty("java.io.tmpdir")).resolve("機能ID");

IDataSet dataSet = new FlatXmlDataSetBuilder().build(
new File("src/test/resources/dbunit/fixture.xml"));

var replacementDataSet = new ReplacementDataSet(dataSet);
replacementDataSet.addReplacementSubstring("[tmp]",
this.tmpFolderPath.toString());

databaseTesterSys.setDataSet(replacementDataSet);
databaseTesterSys.setSetUpOperation(DatabaseOperation.CLEAN_INSERT);
databaseTesterSys.onSetup();

XMLで、最初のタグにない要素は無視される

データセットはExcelとかよりもXMLの方が便利ですね。テーブルごとにまとめれば良いかというと、従属テーブルなんかは親テーブルのそばにあった方が分かりやすいですしね。

ということでXML型式を好んで使っていたのですが、1つ罠がありました。次のようなデータがあったとします。
ユーザーを退会した場合にデータが入るNULLABLEな unregisterd_at カラムがあり、2つ目のユーザーにだけあります。ですが、このデータは無視されます。というのも、なぜかそのテーブルの要素が持っている属性以外は無視されるとのことでした。↑↑に書いたフィルタを追加して unregisterd_at="[NULL]" という属性を最初のユーザーにも設定しなければなりません。

<?xml version="1.0" encoding="UTF-8"?>
<dataset>
<!-- ユーザーマスタ -->
<m_user id="10" name="ユーザー1" />
<m_user id="20" name="ユーザー2" unregisterd_at="2024/12/01"/>
</dataset>

新たに作られたパーティションが、careateDataSet()の結果に表れない

PL/pgSQLで書かれたパーティション改廃関数を実行した後に、careateDataSet()を呼んで正しくテーブルが作られたり削除されたか確認するテストを作成したのですが、古いパーティションの削除処理は正しく処理され、結果のテーブルリストから消えていたのだが、新たに分割して作った新しいパーティションが見えなかったということがありました。

コネクションプールが悪さしていたみたいです。HikariCPをラップしたミドルウェア経由でPL/pgSQLの関数を呼んでいたのを、Javaの機能を直接呼んで実行してからDBUnitで実行結果を取得したら削除したパーティションも追加したパーティションの情報も反映された結果が取得できました。

try (Connection connection = DriverManager.getConnection(this.dataSourceUrl + "?currentSchema=cmn",
this.dataSourceUser, this.dataSourcePassword)) {
try (Statement stmt = connection.createStatement()) {
stmt.execute("SELECT 関数呼び出し('param1', 'param2')");
}
}

まとめ

単にトラブルシュートの備忘録なので、かっこいい締めの言葉とかオチとかはありません。

いろいろハマりはしましたが、なかなか便利なやつなので、Goとか他の言語にも欲しいですね。