LoginSignup
11
21

More than 1 year has passed since last update.

DbUnit使い方メモ

Posted at

DbUnitとは

  • Java でデータベースのテストを行うためのフレームワーク
  • JUnit でテストを実行する前にデータベースにテストデータを登録したり、テスト後のデータベースの状態が期待通りか検証したりできたりする
  • テストデータや期待値は XML、CSV、Excel 形式で定義できる
  • 結構昔からあるけど、2022年現在も Java でデータベースのテストをする場合は DbUnit が現役らしい1

環境

OS とか Java とかのバージョン。

>gradle --version

------------------------------------------------------------
Gradle 7.4.2
------------------------------------------------------------

Build time:   2022-03-31 15:25:29 UTC
Revision:     540473b8118064efcc264694cbcaa4b677f61041

Kotlin:       1.5.31
Groovy:       3.0.9
Ant:          Apache Ant(TM) version 1.10.11 compiled on July 10 2021
JVM:          18.0.1.1 (Oracle Corporation 18.0.1.1+2-6)
OS:           Windows 10 10.0 amd64

データベース

基本的に HSQLDB をインメモリモードで利用する。

ただし、一部だけ PostgreSQL を使用している。
PostgreSQL は、 Docker で起動。

compose.yml
services:
  postgres:
    image: postgres:14.4
    container_name: postgres-14.4
    ports:
      - "5432:5432"
    volumes:
      - postgres-14.4:/var/lib/postgresql/data
    environment:
      POSTGRES_PASSWORD: postgres
volumes:
  postgres-14.4: {}

Hello World

実装

プロジェクト構成
|-build.gradle
`-src/test/
  |-java/
  | `-sandbox/dbunit/
  |   `-HelloDbUnitTest.java
  `-resources/
    `-sandbox/dbunit/HelloDbUnitTest/test/
      |-setUp.xml
      `-expected.xml
build.gradle
plugins {
    id "java"
}

sourceCompatibility = 18
targetCompatibility = 18

[compileJava, compileTestJava]*.options*.encoding = "UTF-8"

repositories {
    mavenCentral()
}

dependencies {
    testRuntimeOnly "org.hsqldb:hsqldb:2.6.1"
    testImplementation "org.junit.jupiter:junit-jupiter:5.8.2"
    testImplementation "org.dbunit:dbunit:2.7.3"
}

test {
    useJUnitPlatform()
}
HelloDbUnitTest
package sandbox.dbunit;

import org.dbunit.IDatabaseTester;
import org.dbunit.JdbcDatabaseTester;
import org.dbunit.database.IDatabaseConnection;
import org.dbunit.dataset.IDataSet;
import org.dbunit.dataset.xml.XmlDataSet;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.io.InputStream;
import java.sql.Connection;
import java.sql.PreparedStatement;

import static org.dbunit.Assertion.assertEquals;

public class HelloDbUnitTest {
    static IDatabaseTester databaseTester;
    static IDatabaseConnection connection;

    @BeforeAll
    static void beforeAll() throws Exception {
        databaseTester =
            new JdbcDatabaseTester("org.hsqldb.jdbcDriver", "jdbc:hsqldb:mem:test");
        connection = databaseTester.getConnection();

        // DB初期化(テーブル作成)
        Connection jdbcConnection = connection.getConnection();
        try (
            PreparedStatement ps = jdbcConnection.prepareStatement("""
            create table test_table (
                id integer primary key,
                value varchar(8)
            )""");
        ) {
            ps.execute();
        }
    }

    @BeforeEach
    void beforeEach() throws Exception {
        XmlDataSet setUpDataSet =
            readXmlDataSet("/sandbox/dbunit/HelloDbUnitTest/test/setUp.xml");
        databaseTester.setDataSet(setUpDataSet);

        databaseTester.onSetup();
    }

    @Test
    void test() throws Exception {
        XmlDataSet expected =
            readXmlDataSet("/sandbox/dbunit/HelloDbUnitTest/test/expected.xml");
        IDataSet actual = connection.createDataSet();

        assertEquals(expected, actual);
    }

    @AfterAll
    static void afterAll() throws Exception {
        if (connection != null) {
            connection.close();
        }
    }

    private XmlDataSet readXmlDataSet(String path) throws Exception  {
        try (InputStream inputStream = getClass().getResourceAsStream(path)) {
            return new XmlDataSet(inputStream);
        }
    }
}
setUp.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE dataset SYSTEM "dataset.dtd">
<dataset>
    <table name="test_table">
        <column>id</column>
        <column>value</column>
        <row>
            <value>1</value>
            <value>hoge</value>
        </row>
        <row>
            <value>2</value>
            <value>fuga</value>
        </row>
        <row>
            <value>3</value>
            <value>piyo</value>
        </row>
    </table>
</dataset>
expected.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE dataset SYSTEM "dataset.dtd">
<dataset>
    <table name="test_table">
        <column>id</column>
        <column>value</column>
        <row>
            <value>1</value>
            <value>HOGE</value>
        </row>
        <row>
            <value>2</value>
            <value>fuga</value>
        </row>
        <row>
            <value>3</value>
            <value>piyo</value>
        </row>
    </table>
</dataset>

実行結果

Actual value='hoge' is not equal to expected value='HOGE': value (table=test_table, row=0, col=value) expected:<HOGE> but was:<hoge>
Expected :HOGE
Actual   :hoge

説明

build.gradle
dependencies {
    ...
    testImplementation "org.dbunit:dbunit:2.7.3"
}
HelloDbUnitTest

public class HelloDbUnitTest {
    static IDatabaseTester databaseTester;
    static IDatabaseConnection connection;

    @BeforeAll
    static void beforeAll() throws Exception {
        databaseTester =
            new JdbcDatabaseTester("org.hsqldb.jdbcDriver", "jdbc:hsqldb:mem:test");
        connection = databaseTester.getConnection();

        ...
    }

    @BeforeEach
    void beforeEach() throws Exception {
        XmlDataSet setUpDataSet =
            readXmlDataSet("/sandbox/dbunit/HelloDbUnitTest/test/setUp.xml");
        databaseTester.setDataSet(setUpDataSet);

        databaseTester.onSetup();
    }

    ...

    @AfterAll
    static void afterAll() throws Exception {
        if (connection != null) {
            connection.close();
        }
    }

    private XmlDataSet readXmlDataSet(String path) throws Exception  {
        try (InputStream inputStream = getClass().getResourceAsStream(path)) {
            return new XmlDataSet(inputStream);
        }
    }
}
  • まずは、 JdbcDatabaseTester のインスタンスを生成する
    • コンストラクタ引数で、接続先の DB の情報を設定する
  • setDataSet(IDataSet) で、テスト前に DB に投入するテストデータを設定する(まだ投入はされない)
    • DbUnit では、データのまとまりをデータセットと呼び、 IDataSet というインタフェースで表現している
      • データセットには、複数のテーブルが含まれる
      • 投入データだけでなく、期待値のデータも IDataSet で表現する
    • IDataSet には、データの取得方法によって様々な実装クラスが用意されている
      • データベース → DatabaseDataSet
      • Excel → XlsDataSet
      • CSV → CsvDataSet/CsvURLDataSet
      • XML → XmlDataSet, FlatXmlDataSet
    • ここでは、 XML からデータを読み取る XmlDataSet でテストデータを読み込んでいる
  • IDatabaseTestergetConnection() で、 IDatabaseConnection を取得しておく
    • あとで、検証のためにデータベースの状態を取得したりするのに利用する
    • 生成にはコストがかかるので、一度作成したインスタンスは再利用することが推奨されている
    • データベース接続を管理しているので、 @AfterAll で確実にクローズする
  • IDatabaseTester#onSetup() を、テスト前に実行する
    • setDataSet() で設定したテストデータの投入などが行われる
    • デフォルトでは、データは DELETE → INSERT される
expected.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE dataset SYSTEM "dataset.dtd">
<dataset>
    <table name="test_table">
        <column>id</column>
        <column>value</column>
        <row>
            <value>1</value>
            <value>HOGE</value>
        </row>
        ...
    </table>
</dataset>
  • XML でデータセットを定義している
  • 前述のとおり、 XML 以外にも Excel などで定義することも可能
HelloDbUnitTest
    IDatabaseConnection connection;
    ...
    @Test
    void test() throws Exception {
        XmlDataSet expected =
            readXmlDataSet("/sandbox/dbunit/HelloDbUnitTest/test/expected.xml");
        IDataSet actual = connection.createDataSet();

        assertEquals(expected, actual);
    }
  • IDatabaseConnectioncreateDataSet() で、実際のデータベースのデータを表す IDataSet を取得できる
  • org.dbunit.Assertion#assertEquals(IDataSet, IDataSet) で、 IDataSet 同士の検証ができる

検証用の自作 JUnit 拡張

以降は、コードを書きやすくするために、以下の自作の JUnit 拡張を利用する前提でコードを記述する。

package sandbox.dbunit;

import org.dbunit.IDatabaseTester;
import org.dbunit.JdbcDatabaseTester;
import org.dbunit.database.IDatabaseConnection;
import org.dbunit.dataset.Column;
import org.dbunit.dataset.DataSetException;
import org.dbunit.dataset.ITable;
import org.dbunit.dataset.ITableMetaData;
import org.dbunit.dataset.xml.XmlDataSet;
import org.junit.jupiter.api.extension.AfterAllCallback;
import org.junit.jupiter.api.extension.BeforeAllCallback;
import org.junit.jupiter.api.extension.ExtensionContext;

import java.io.IOException;
import java.io.InputStream;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

public class MyDbUnitExtension implements BeforeAllCallback, AfterAllCallback {
    private IDatabaseTester databaseTester;
    private IDatabaseConnection connection;

    @Override
    public void beforeAll(ExtensionContext context) throws Exception {
        String testClassName = context.getRequiredTestClass().getSimpleName();
        databaseTester = new JdbcDatabaseTester(
            "org.hsqldb.jdbcDriver", "jdbc:hsqldb:mem:" + testClassName );
        connection = databaseTester.getConnection();
    }

    @Override
    public void afterAll(ExtensionContext context) throws Exception {
        if (connection != null) {
            connection.close();
        }
    }

    public void sql(String sql) {
        try (PreparedStatement ps = connection.getConnection().prepareStatement(sql)) {
            ps.execute();
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

    public XmlDataSet readXmlDataSet(String path) {
        try (InputStream inputStream = getClass().getResourceAsStream(path)) {
            return new XmlDataSet(inputStream);
        } catch (DataSetException | IOException e) {
            throw new RuntimeException(e);
        }
    }

    public void printTable(String tableName) {
        try {
            System.out.println(tableName + " {");
            ITable table = this.getConnection().createDataSet().getTable(tableName);
            ITableMetaData metaData = table.getTableMetaData();

            for (int row=0; row<table.getRowCount(); row++) {
                List<String> values = new ArrayList<>();
                for (Column column : metaData.getColumns()) {
                    Object value = table.getValue(row, column.getColumnName());
                    values.add(column.getColumnName() + "=" + format(value));
                }
                System.out.println("  " + String.join(", ", values));
            }
            System.out.println("}");
        } catch (DataSetException | SQLException e) {
            throw new RuntimeException(e);
        }
    }

    private String format(Object value) {
        if (value == null) {
            return "null";
        }
        if (value instanceof String) {
            return "'" + value + "'";
        }
        return value.toString();
    }

    public IDatabaseTester getDatabaseTester() {
        return databaseTester;
    }

    public IDatabaseConnection getConnection() {
        return connection;
    }
}
  • テストクラスごとに毎回必要になる以下の処理をまとめている
    • @BeforeAll で、 IDatabaseTesterIDatabaseConnection の生成
    • @AfterAll で、 IDatabaseConnectionclose()
  • 検証をしやすくするために、以下のメソッドを定義している
    • sql(String)
      • 指定された任意の SQL を実行する
    • readXmlDataSet(String)
      • 指定されたパスから XmlDataSet を読み込む
    • printTable(String)
      • 指定されたテーブルの中身を標準出力に出力する
      • null の場合は、単に null と出力する
      • 文字列(String) の場合は、シングルクォート(') で括って出力する
      • それ以外の場合は、単に toString() した値を出力する

テスト前後のクリーンアップなどの挙動を変える

IDatabaseTester.onSetup() で実行する処理は、 setSetUpOperation(DatabaseOperation) で変更できる。

以下のようなクラスを準備して、それぞれの DatabaseOperation での動きを確認する。

package sandbox.dbunit;

import org.dbunit.dataset.xml.XmlDataSet;
import org.dbunit.operation.DatabaseOperation;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

public class DatabaseOperationTest {
    @RegisterExtension
    static MyDbUnitExtension myDbUnitExtension = new MyDbUnitExtension();

    @BeforeAll
    static void beforeAll() {
        // DB初期化(テーブル作成)
        myDbUnitExtension.sql("""
        create table foo_table (
            id integer primary key,
            value varchar(32)
        )""");
        myDbUnitExtension.sql("""
        create table bar_table (
            id integer primary key,
            foo_id integer,
            foreign key (foo_id) references foo_table (id)
        )""");
    }

    @BeforeEach
    void setUp() {
        myDbUnitExtension.sql("truncate table bar_table");
        myDbUnitExtension.sql("truncate table foo_table");

        myDbUnitExtension.sql("insert into foo_table values (9, 'HOGE')");
        myDbUnitExtension.sql("insert into foo_table values (99, 'FUGA')");
        myDbUnitExtension.sql("insert into bar_table values (10, 9)");
        myDbUnitExtension.sql("insert into bar_table values (100, 99)");

        System.out.println("[初期状態]");
        myDbUnitExtension.printTable("foo_table");
        myDbUnitExtension.printTable("bar_table");
    }

    private void printTables() {
        System.out.println("[onSetup()実行後]");
        myDbUnitExtension.printTable("foo_table");
        myDbUnitExtension.printTable("bar_table");
    }
}
  • foo_tablebar_table を作成し、テストごとに初期データを投入している

UPDATE

    @Test
    void testUpdate() throws Exception {
        myDbUnitExtension.getDatabaseTester().setSetUpOperation(DatabaseOperation.UPDATE);

        XmlDataSet xmlDataSet = myDbUnitExtension
            .readXmlDataSet("/sandbox/dbunit/DatabaseOperationTest/testUpdate.xml");
        myDbUnitExtension.getDatabaseTester().setDataSet(xmlDataSet);
        myDbUnitExtension.getDatabaseTester().onSetup();

        printTables();
    }
testUpdate.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE dataset SYSTEM "dataset.dtd">
<dataset>
    <table name="foo_table">
        <column>id</column>
        <column>value</column>
        <row>
            <value>1</value>
            <value>foo</value>
        </row>
        <row>
            <value>9</value>
            <value>UPDATE</value>
        </row>
    </table>
</dataset>
実行結果
[初期状態]
foo_table {
  ID=9, VALUE='HOGE'
  ID=99, VALUE='FUGA'
}
bar_table {
  ID=10, FOO_ID=9
  ID=100, FOO_ID=99
}

[onSetup()実行後]
foo_table {
  ID=9, VALUE='UPDATE'
  ID=99, VALUE='FUGA'
}
bar_table {
  ID=10, FOO_ID=9
  ID=100, FOO_ID=99
}
  • UPDATE では、データセットに存在するレコードだけが更新される
  • データセットには存在するけど実際には存在しないレコードの更新は無視される
  • 更新対象外の既存レコードは変更されない

INSERT

    @Test
    void testInsert() throws Exception {
        myDbUnitExtension.getDatabaseTester().setSetUpOperation(DatabaseOperation.INSERT);

        XmlDataSet xmlDataSet = myDbUnitExtension
            .readXmlDataSet("/sandbox/dbunit/DatabaseOperationTest/testInsert.xml");
        myDbUnitExtension.getDatabaseTester().setDataSet(xmlDataSet);
        myDbUnitExtension.getDatabaseTester().onSetup();

        printTables();
    }
testInsert.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE dataset SYSTEM "dataset.dtd">
<dataset>
    <table name="foo_table">
        <column>id</column>
        <column>value</column>
        <row>
            <value>1</value>
            <value>foo</value>
        </row>
    </table>
</dataset>
実行結果
[初期状態]
foo_table {
  ID=9, VALUE='HOGE'
  ID=99, VALUE='FUGA'
}
bar_table {
  ID=10, FOO_ID=9
  ID=100, FOO_ID=99
}

[onSetup()実行後]
foo_table {
  ID=1, VALUE='foo'
  ID=9, VALUE='HOGE'
  ID=99, VALUE='FUGA'
}
bar_table {
  ID=10, FOO_ID=9
  ID=100, FOO_ID=99
}
  • INSERT では、データセットで指定されたレコードが追加される
  • 既存レコードは更新されない

DELETE

    @Test
    void testDelete() throws Exception {
        myDbUnitExtension.getDatabaseTester().setSetUpOperation(DatabaseOperation.DELETE);

        XmlDataSet xmlDataSet = myDbUnitExtension
            .readXmlDataSet("/sandbox/dbunit/DatabaseOperationTest/testDelete.xml");
        myDbUnitExtension.getDatabaseTester().setDataSet(xmlDataSet);
        myDbUnitExtension.getDatabaseTester().onSetup();

        printTables();
    }
testDelete.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE dataset SYSTEM "dataset.dtd">
<dataset>
    <table name="foo_table">
        <column>id</column>
        <row>
            <value>1</value>
        </row>
        <row>
            <value>9</value>
        </row>
    </table>
    <table name="bar_table">
        <column>id</column>
        <row>
            <value>10</value>
        </row>
        <row>
            <value>100</value>
        </row>
    </table>
</dataset>
実行結果
[初期状態]
foo_table {
  ID=9, VALUE='HOGE'
  ID=99, VALUE='FUGA'
}
bar_table {
  ID=10, FOO_ID=9
  ID=100, FOO_ID=99
}

[onSetup()実行後]
foo_table {
  ID=99, VALUE='FUGA'
}
bar_table {
}
  • DELETE では、データセットで指定されたレコードだけが削除される
  • 削除は、データセット上の並び順の逆で行われる
    • 外部キー制約を考慮した挙動
  • データセットに存在するけど実際には存在しないレコードについては無視される

DELETE_ALL

    @Test
    void testDeleteAll() throws Exception {
        myDbUnitExtension.getDatabaseTester().setSetUpOperation(DatabaseOperation.DELETE_ALL);

        XmlDataSet xmlDataSet = myDbUnitExtension
            .readXmlDataSet("/sandbox/dbunit/DatabaseOperationTest/testDeleteAll.xml");
        myDbUnitExtension.getDatabaseTester().setDataSet(xmlDataSet);
        myDbUnitExtension.getDatabaseTester().onSetup();

        printTables();
    }
testDeleteAll.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE dataset SYSTEM "dataset.dtd">
<dataset>
    <table name="bar_table">
    </table>
</dataset>
実行結果
[初期状態]
foo_table {
  ID=9, VALUE='HOGE'
  ID=99, VALUE='FUGA'
}
bar_table {
  ID=10, FOO_ID=9
  ID=100, FOO_ID=99
}

[onSetup()実行後]
foo_table {
  ID=9, VALUE='HOGE'
  ID=99, VALUE='FUGA'
}
bar_table {
}
  • DELETE_ALL では、データセットで指定されたテーブルの全データが削除される
  • 削除は、データセット上での順序の逆順で行われる
    • 外部キー制約を考慮した動き

TRUNCATE

    @Test
    void testTruncate() throws Exception {
        myDbUnitExtension
            .getDatabaseTester().setSetUpOperation(DatabaseOperation.TRUNCATE_TABLE);

        XmlDataSet xmlDataSet = myDbUnitExtension
            .readXmlDataSet("/sandbox/dbunit/DatabaseOperationTest/testTruncate.xml");
        myDbUnitExtension.getDatabaseTester().setDataSet(xmlDataSet);
        myDbUnitExtension.getDatabaseTester().onSetup();

        printTables();
    }
testTruncate.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE dataset SYSTEM "dataset.dtd">
<dataset>
    <table name="foo_table">
    </table>
    <table name="bar_table">
    </table>
</dataset>
実行結果
[初期状態]
foo_table {
  ID=9, VALUE='HOGE'
  ID=99, VALUE='FUGA'
}
bar_table {
  ID=10, FOO_ID=9
  ID=100, FOO_ID=99
}

[onSetup()実行後]
foo_table {
}
bar_table {
}
  • TRUNCATE では、データセットで指定されたテーブルを切り捨てる
  • 切り捨ては、データセット上の順番の逆順で行われる
    • 外部キー制約を考慮した動き

REFRESH

    @Test
    void testRefresh() throws Exception {
        myDbUnitExtension.getDatabaseTester().setSetUpOperation(DatabaseOperation.REFRESH);

        XmlDataSet xmlDataSet = myDbUnitExtension
            .readXmlDataSet("/sandbox/dbunit/DatabaseOperationTest/testRefresh.xml");
        myDbUnitExtension.getDatabaseTester().setDataSet(xmlDataSet);
        myDbUnitExtension.getDatabaseTester().onSetup();

        printTables();
    }
testRefresh.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE dataset SYSTEM "dataset.dtd">
<dataset>
    <table name="foo_table">
        <column>id</column>
        <column>value</column>
        <row>
            <value>1</value>
            <value>foo</value>
        </row>
        <row>
            <value>9</value>
            <value>UPDATE</value>
        </row>
    </table>
</dataset>
実行結果
[初期状態]
foo_table {
  ID=9, VALUE='HOGE'
  ID=99, VALUE='FUGA'
}
bar_table {
  ID=10, FOO_ID=9
  ID=100, FOO_ID=99
}

[onSetup()実行後]
foo_table {
  ID=1, VALUE='foo'
  ID=9, VALUE='UPDATE'
  ID=99, VALUE='FUGA'
}
bar_table {
  ID=10, FOO_ID=9
  ID=100, FOO_ID=99
}
  • REFRESH では、 INSERTUPDATE を組み合わせたような更新が行われる
    • データセットには存在するけど実際には存在しないレコードは追加される
    • データセットにも実際にも既存するレコードは更新される

CLEAN_INSERT

    @Test
    void testCleanInsert() throws Exception {
        myDbUnitExtension
            .getDatabaseTester().setSetUpOperation(DatabaseOperation.CLEAN_INSERT);

        XmlDataSet xmlDataSet = myDbUnitExtension
            .readXmlDataSet("/sandbox/dbunit/DatabaseOperationTest/testCleanInsert.xml");
        myDbUnitExtension.getDatabaseTester().setDataSet(xmlDataSet);
        myDbUnitExtension.getDatabaseTester().onSetup();

        printTables();
    }
testCleanInsert.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE dataset SYSTEM "dataset.dtd">
<dataset>
    <table name="foo_table">
        <column>id</column>
        <column>value</column>
        <row>
            <value>1</value>
            <value>foo</value>
        </row>
    </table>
    <table name="bar_table">
        <column>id</column>
        <column>foo_id</column>
        <row>
            <value>1</value>
            <value>1</value>
        </row>
    </table>
</dataset>
実行結果
[初期状態]
foo_table {
  ID=9, VALUE='HOGE'
  ID=99, VALUE='FUGA'
}
bar_table {
  ID=10, FOO_ID=9
  ID=100, FOO_ID=99
}

[onSetup()実行後]
foo_table {
  ID=1, VALUE='foo'
}
bar_table {
  ID=1, FOO_ID=1
}
  • CLEAN_INSERT では、 DELETE_ALLINSERT を組み合わせた処理が行われる
    • データセットで指定されたテーブルのデータが全て削除されたうえで、データの登録が行われる
  • setSetUpOperation() が未指定の場合、デフォルトではこの処理が設定されている

DataseOperation を手動で実行する

    @Test
    void testManual() throws Exception {
        XmlDataSet xmlDataSet = myDbUnitExtension
            .readXmlDataSet("/sandbox/dbunit/DatabaseOperationTest/testManual.xml");

        DatabaseOperation.CLEAN_INSERT.execute(myDbUnitExtension.getConnection(), xmlDataSet);

        System.out.println("execute()実行後");
        myDbUnitExtension.printTable("foo_table");
        myDbUnitExtension.printTable("bar_table");
    }
testManual.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE dataset SYSTEM "dataset.dtd">
<dataset>
    <table name="foo_table">
        <column>id</column>
        <column>value</column>
        <row>
            <value>9</value>
            <value>UPDATE</value>
        </row>
    </table>
    <table name="bar_table">
        <column>id</column>
        <row>
            <value>100</value>
        </row>
    </table>
</dataset>
実行結果
[初期状態]
foo_table {
  ID=9, VALUE='HOGE'
  ID=99, VALUE='FUGA'
}
bar_table {
  ID=10, FOO_ID=9
  ID=100, FOO_ID=99
}

execute()実行後
foo_table {
  ID=9, VALUE='UPDATE'
}
bar_table {
  ID=100, FOO_ID=null
}
  • DatabaseOperation.execute(IDatabaseConnection, IDataSet) メソッドを使えば、 onSetup() で行われている処理を手動で実行できる

テスト後の後処理

    @Test
    void testOnTearDown() throws Exception {
        myDbUnitExtension.getDatabaseTester().setTearDownOperation(DatabaseOperation.UPDATE);

        XmlDataSet xmlDataSet = myDbUnitExtension
            .readXmlDataSet("/sandbox/dbunit/DatabaseOperationTest/testOnTearDown.xml");
        myDbUnitExtension.getDatabaseTester().setDataSet(xmlDataSet);

        myDbUnitExtension.getDatabaseTester().onTearDown();

        printTables();
    }
testOnTearDown.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE dataset SYSTEM "dataset.dtd">
<dataset>
    <table name="foo_table">
        <column>id</column>
        <column>value</column>
        <row>
            <value>9</value>
            <value>TEAR_DOWN</value>
        </row>
    </table>
</dataset>
実行結果
[初期状態]
foo_table {
  ID=9, VALUE='HOGE'
  ID=99, VALUE='FUGA'
}
bar_table {
  ID=10, FOO_ID=9
  ID=100, FOO_ID=99
}

[onSetup()実行後]
foo_table {
  ID=9, VALUE='TEAR_DOWN'
  ID=99, VALUE='FUGA'
}
bar_table {
  ID=10, FOO_ID=9
  ID=100, FOO_ID=99
}
  • IDatabaseTester.onTearDown() を実行すると、 setDataSet(IDataSet) で設定されたデータセットを使って後処理が実行される
  • ただし、デフォルトでは DatabaseOperation.NONE が設定されているため、そのままでは何も実行されない
  • 何かしら処理をさせたい場合は、 IDatabaseTester.setTearDownOperation(DatabaseOperation)NONE 以外を設定する必要がある

Equality Comparison

  • assertEquals(IDataSet, IDataSet) などの検証では、 Equality Comparison (同値比較?)が行われる
  • Equality Comparison では、2つのデータセットが完全に一致していることが検証される
  • それぞれのデータセットに含まれるテーブルやカラムの数が異なる場合、デフォルトではテストが失敗する
テーブル数が異なる場合のエラーメッセージ例
org.dbunit.assertion.DbComparisonFailure[table countexpected:<3>but was:<2>]
カラム数が異なる場合のエラーメッセージ例
org.dbunit.assertion.DbComparisonFailure[column count (table=foo_table, expectedColCount=2, actualColCount=3)expected:<[id, text]>but was:<[id, numeric, text]>]

特定のテーブルだけを比較する

setUp.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE dataset SYSTEM "dataset.dtd">
<dataset>
    <table name="foo_table">
        <column>id</column>
        <column>text</column>
        <column>numeric</column>
        <row>
            <value>1</value>
            <value>foo</value>
            <value>99</value>
        </row>
    </table>
    <table name="bar_table">
        <column>id</column>
        <column>text</column>
        <row>
            <value>1</value>
            <value>bar</value>
        </row>
    </table>
</dataset>
expected.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE dataset SYSTEM "dataset.dtd">
<dataset>
    <table name="foo_table">
        <column>id</column>
        <column>text</column>
        <column>numeric</column>
        <row>
            <value>1</value>
            <value>foo</value>
            <value>99</value>
        </row>
    </table>
</dataset>
  • 期待値の XML には bar_table が存在しないので、そのまま検証すると失敗する
package sandbox.dbunit;

import org.dbunit.dataset.IDataSet;
import org.dbunit.dataset.ITable;
import org.dbunit.dataset.xml.XmlDataSet;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import static org.dbunit.Assertion.assertEquals;

public class AssertSpecifiedTableTest {

    @RegisterExtension
    static MyDbUnitExtension myDbUnitExtension = new MyDbUnitExtension();

    @BeforeAll
    static void beforeAll() {
        // DB初期化(テーブル作成)
        myDbUnitExtension.sql("""
        create table foo_table (
            id integer primary key,
            text varchar(32),
            numeric integer
        )""");
        myDbUnitExtension.sql("""
        create table bar_table (
            id integer primary key,
            text varchar(32)
        )""");
    }

    @BeforeEach
    void beforeEach() throws Exception {
        XmlDataSet setUpDataSet = myDbUnitExtension
            .readXmlDataSet("/sandbox/dbunit/AssertSpecifiedTableTest/setUp.xml");
        myDbUnitExtension.getDatabaseTester().setDataSet(setUpDataSet);

        myDbUnitExtension.getDatabaseTester().onSetup();
    }

    @Test
    void testGetTable() throws Exception {
        IDataSet actual = myDbUnitExtension.getConnection().createDataSet();
        ITable actualFooTable = actual.getTable("foo_table");

        XmlDataSet expected = myDbUnitExtension
            .readXmlDataSet("/sandbox/dbunit/AssertSpecifiedTableTest/expected.xml");
        ITable expectedFooTable = expected.getTable("foo_table");

        assertEquals(expectedFooTable, actualFooTable);
    }
}

  • IDataSetgetTable(String) を使うと、テーブル単位でデータを抽出できる
  • テーブル単位のデータは、 ITable という型で表現される
  • ITable を比較するための assertEquals(ITable, ITable) が用意されているので、これを使うことでテーブル単位の比較ができる

IDatbaseConnection には createTable(String) というメソッドもあり、 IDataSet を介さずに直接 ITable を取得することもできる。
しかし、 Javadoc に書いてある通り実行される SQL は select * from tableName となっていてソート条件が指定されない。
ソート条件が指定されないと検索結果の並びは DB 依存で予測できなくなるので、テストで使うには不向きな気がする。

なお、 IDataSetgetTable() で取得した場合は主キーでソートされている。

クエリ結果を ITable として取得する

    @Test
    void testCreateQueryTable() throws Exception {
        ITable actualFooTable = myDbUnitExtension.getConnection()
            .createQueryTable("foo_table", "select * from foo_table order by id");

        XmlDataSet expected = myDbUnitExtension
            .readXmlDataSet("/sandbox/dbunit/AssertSpecifiedTableTest/expected.xml");
        ITable expectedFooTable = expected.getTable("foo_table");

        assertEquals(expectedFooTable, actualFooTable);
    }
  • IDatabaseConnectioncreateQueryTable(String, String) を使用することで、任意のクエリ結果を ITable として取得できる
    • 第一引数にはテーブル名を設定する
      • この名前は、返却される ITablegetTableMetaData() で取得できる ITableMetaDatagetTableName() が返す値に使用される
      • 特に実際のテーブル名と異なっていてもエラーにはならないが、基本は実際のテーブル名に合わせるのがいい気がする
      • join している場合とかは、識別しやすい名前にしとくのがいいのかもしれない
    • 第二引数には、実行するクエリを設定する
  • 任意のクエリなので、 join 結果を受け取ることもできる

期待値に存在するカラムだけで比較する

値が自動採番されるIDや更新日時のようなカラムは実行時に値が決定するため、あらかじめ決まった値の期待値を用意しておくことが難しい。
このため、そういうカラムだけテストの検証から除外したくなることがまれによくある。

ただ、できる限りそういうカラムもテスト対象に含めるべきだとは個人的に思う。
IDがシーケンスオブジェクトで採番されているのであれば、テスト前にシーケンスオブジェクトを更新して狙った値が採番されるように調整したり、日時項目はテストのときだけ指定値が利用されるような仕組みにしておくなど。

setUp.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE dataset SYSTEM "dataset.dtd">
<dataset>
    <table name="foo_table">
        <column>id</column>
        <column>text</column>
        <column>numeric</column>
        <row>
            <value>1</value>
            <value>foo</value>
            <value>99</value>
        </row>
    </table>
</dataset>
expected.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE dataset SYSTEM "dataset.dtd">
<dataset>
    <table name="foo_table">
        <column>id</column>
        <column>text</column>
        <row>
            <value>1</value>
            <value>foo</value>
        </row>
    </table>
</dataset>
  • 期待値の XML には、 numeric カラムが存在しない
  • このまま単純に assertEquals() で比較すると、テストは失敗する
package sandbox.dbunit;

import org.dbunit.dataset.ITable;
import org.dbunit.dataset.filter.DefaultColumnFilter;
import org.dbunit.dataset.xml.XmlDataSet;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import static org.dbunit.Assertion.assertEquals;

public class IgnoringSomeColumnsTest {
    @RegisterExtension
    static MyDbUnitExtension myDbUnitExtension = new MyDbUnitExtension();

    @BeforeAll
    static void beforeAll() {
        // DB初期化(テーブル作成)
        myDbUnitExtension.sql("""
        create table foo_table (
            id integer primary key,
            text varchar(32),
            numeric integer
        )""");
    }

    @BeforeEach
    void beforeEach() throws Exception {
        XmlDataSet setUpDataSet = myDbUnitExtension
            .readXmlDataSet("/sandbox/dbunit/IgnoringSomeColumnsTest/setUp.xml");
        myDbUnitExtension.getDatabaseTester().setDataSet(setUpDataSet);

        myDbUnitExtension.getDatabaseTester().onSetup();
    }

    @Test
    void test() throws Exception {
        ITable actualFooTable = myDbUnitExtension
            .getConnection().createDataSet().getTable("foo_table");

        XmlDataSet expected = myDbUnitExtension
            .readXmlDataSet("/sandbox/dbunit/IgnoringSomeColumnsTest/expected.xml");
        ITable expectedFooTable = expected.getTable("foo_table");

        ITable filteredActualFooTable = DefaultColumnFilter
            .includedColumnsTable(
                actualFooTable,
                expectedFooTable.getTableMetaData().getColumns()
            );

        assertEquals(expectedFooTable, filteredActualFooTable);
    }
}
  • DefaultColumnFilterincludedColumnsTable(ITable, Column[]) を使用すると、第二引数で指定したカラムだけに絞った ITable が返される
  • 実際の DB から生成され全てのカラムを持つ actualFooTable から、期待値の expectedFooTable だけが持つカラムだけに絞った ITable を作ることで、期待値で定義したカラムだけで検証ができるようになる

ソート条件

setUp.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE dataset SYSTEM "dataset.dtd">
<dataset>
    <table name="test_table">
        <column>id</column>
        <column>value</column>
        <row>
            <value>3</value>
            <value>aaa</value>
        </row>
        <row>
            <value>2</value>
            <value>bbb</value>
        </row>
        <row>
            <value>1</value>
            <value>ccc</value>
        </row>
    </table>
</dataset>
expected.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE dataset SYSTEM "dataset.dtd">
<dataset>
    <table name="test_table">
        <column>id</column>
        <column>value</column>
        <row>
            <value>3</value>
            <value>aaa</value>
        </row>
        <row>
            <value>2</value>
            <value>bbb</value>
        </row>
        <row>
            <value>1</value>
            <value>ccc</value>
        </row>
    </table>
</dataset>
  • setUp.xmlexpected.xml は、全く同じ内容にしている
package sandbox.dbunit;

import org.dbunit.dataset.IDataSet;
import org.dbunit.dataset.ITable;
import org.dbunit.dataset.xml.XmlDataSet;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

public class RowOrderingTest {
    @RegisterExtension
    static MyDbUnitExtension myDbUnitExtension = new MyDbUnitExtension();

    @BeforeAll
    static void beforeAll() {
        myDbUnitExtension.sql("""
        create table test_table (
            id integer primary key,
            value varchar(8)
        )""");
    }

    @BeforeEach
    void beforeEach() throws Exception {
        XmlDataSet setUp = myDbUnitExtension
            .readXmlDataSet("/sandbox/dbunit/RowOrderingTest/setUp.xml");
        myDbUnitExtension.getDatabaseTester().setDataSet(setUp);
        myDbUnitExtension.getDatabaseTester().onSetup();
    }

    @Test
    void test() throws Exception {
        IDataSet actual = myDbUnitExtension.getConnection().createDataSet();
        XmlDataSet expected = myDbUnitExtension
            .readXmlDataSet("/sandbox/dbunit/RowOrderingTest/expected.xml");

        assertEquals(expected, actual);
    }
}
  • setUp.xml を投入して expected.xml と比較する
実行結果
org.dbunit.assertion.DbComparisonFailure[Actual value='1' is not equal to expected value='3': value (table=test_table, row=0, col=id)expected:<3>but was:<1>]
  • IDatabaseConnection#createDataSet() で取得したデータセットは、デフォルトでは主キーでソートされている
  • assertEquals() による比較は、データの順序も検証対象となる
  • したがって、上のテストは1行目からidに差異が生まれエラーとなっている
    • expected は XML で定義したまま id=3 が1行目になっている
    • actual は主キーでソートされているので、 id=1 が1行目になっている
  • テーブルに主キーが存在しない場合、ソート順序は不定となる
    • その場合、テストが成功するかどうかも不定になる
    • このため、ソート条件は必ず設定された状態にすべき

ソート条件を指定する

明示的にソート条件を指定したい場合、1つは前述した IDatabaseConnection#createQueryTable(String) を使用する方法がある

もう1つの方法として、 SortedTable を使う方法がある。

public class RowOrderingTest {
    ...
    @Test
    void testSortedTable() throws Exception {
        IDataSet actual = myDbUnitExtension.getConnection().createDataSet();
        SortedTable sortedActualTestTable =
            new SortedTable(actual.getTable("test_table"), new String[]{"value"});

        XmlDataSet expected = myDbUnitExtension
            .readXmlDataSet("/sandbox/dbunit/RowOrderingTest/expected.xml");
        ITable expectedTestTable = expected.getTable("test_table");

        assertEquals(expectedTestTable, sortedActualTestTable); // このテストは成功する
    }
}
  • SortedTable を使うと、指定した ITable のデータをソートした状態にできる
    • コンストラクタの第一引数でソートしたい ITable を指定する
    • 第二引数にはソート対象のカラムを配列で指定する

数値項目を数値としてソートする

setUp.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE dataset SYSTEM "dataset.dtd">
<dataset>
    <table name="test_table">
        <column>id</column>
        <column>text</column>
        <column>numeric</column>
        <row>
            <value>1</value>
            <value>ccc</value>
            <value>10</value>
        </row>
        <row>
            <value>2</value>
            <value>bbb</value>
            <value>2</value>
        </row>
        <row>
            <value>3</value>
            <value>aaa</value>
            <value>1</value>
        </row>
        <row>
            <value>4</value>
            <value>ddd</value>
            <value>10</value>
        </row>
    </table>
</dataset>
expected.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE dataset SYSTEM "dataset.dtd">
<dataset>
    <table name="test_table">
        <column>id</column>
        <column>text</column>
        <column>numeric</column>
        <row>
            <value>3</value>
            <value>aaa</value>
            <value>1</value>
        </row>
        <row>
            <value>2</value>
            <value>bbb</value>
            <value>2</value>
        </row>
        <row>
            <value>1</value>
            <value>ccc</value>
            <value>10</value>
        </row>
        <row>
            <value>4</value>
            <value>ddd</value>
            <value>10</value>
        </row>
    </table>
</dataset>
  • nemerictext の順番にソートして取得することを期待した状態にしておく
package sandbox.dbunit;

import org.dbunit.dataset.ITable;
import org.dbunit.dataset.SortedTable;
import org.dbunit.dataset.xml.XmlDataSet;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import static org.dbunit.Assertion.assertEquals;

public class SortedTableForNumericTest {
    @RegisterExtension
    static MyDbUnitExtension myDbUnitExtension = new MyDbUnitExtension();

    @BeforeAll
    static void beforeAll() {
        myDbUnitExtension.sql("""
        create table test_table (
            id integer primary key,
            text varchar(8),
            numeric integer
        )""");
    }

    @BeforeEach
    void beforeEach() throws Exception {
        XmlDataSet setUp = myDbUnitExtension
            .readXmlDataSet("/sandbox/dbunit/SortedTableForNumericTest/setUp.xml");
        myDbUnitExtension.getDatabaseTester().setDataSet(setUp);
        myDbUnitExtension.getDatabaseTester().onSetup();
    }

    @Test
    void testDefault() throws Exception {
        ITable actualTestTable = myDbUnitExtension
            .getConnection().createDataSet().getTable("test_table");
        SortedTable sortedActualTestTable =
            new SortedTable(actualTestTable, new String[]{"numeric", "text"});

        ITable expectedTestTable = myDbUnitExtension
            .readXmlDataSet("/sandbox/dbunit/SortedTableForNumericTest/expected.xml")
            .getTable("test_table");

        assertEquals(expectedTestTable, sortedActualTestTable);
    }
}
  • SortedTable で、 nemerictext の順番でソートして比較する
実行結果
org.dbunit.assertion.DbComparisonFailure[value (table=test_table, row=1, col=id)expected:<2>but was:<1>]
  • 2行目で id=2 (numeric=2, text=bbb) を期待していたのに、実際には id=1 (numeric=10, text=ccc) が来てしまいテストが失敗した
  • SortedTable は、デフォルトでは各値を文字列としてソートするようになっている
    • このため、 sortedActualTestTable は以下の順序でソートされている
      1. id=3, text=aaa, numeric=1
      2. id=1, text=ccc, numeric=10
      3. id=4, text=ddd, numeric=10
      4. id=2, text=bbb, numeric=2
    • numeric を文字列としてソートするため、 "1" -> "10" -> "2" の順番になっている
  • 数値項目を数値としてソートしたい場合は、 setUseComparable(boolean)true を設定する必要がある
public class SortedTableForNumericTest {
    ...
    @Test
    void testUseComparableTrue() throws Exception {
        ITable actualTestTable = myDbUnitExtension
            .getConnection().createDataSet().getTable("test_table");
        SortedTable sortedActualTestTable =
            new SortedTable(actualTestTable, new String[]{"numeric", "text"});
        sortedActualTestTable.setUseComparable(true); // ★

        ITable expectedTestTable = myDbUnitExtension
                .readXmlDataSet("/sandbox/dbunit/SortedTableForNumericTest/expected.xml")
                .getTable("test_table");

        assertEquals(expectedTestTable, sortedActualTestTable); // このテストは成功する
    }
}
  • SortedTable のインスタンスを生成した直後に、 setUseComaprable(true) を実行する
    • setUseComparable() はコンストラクタでインスタンスを生成した直後に呼ばなければならない
    • だったらコンストラクタの引数に指定できるようにしておけばいい気がするが、それをするとすで4つ存在している SortedTable のコンストラクタが8つに増大するためしないらしい2
  • これにより、数値項目は数値としてソートされるようになる

値が異なる箇所をすべて収集する

setUp.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE dataset SYSTEM "dataset.dtd">
<dataset>
    <table name="test_table">
        <column>id</column>
        <column>value</column>
        <row>
            <value>1</value>
            <value>aaa</value>
        </row>
        <row>
            <value>2</value>
            <value>bbb</value>
        </row>
        <row>
            <value>3</value>
            <value>ccc</value>
        </row>
    </table>
</dataset>
expected.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE dataset SYSTEM "dataset.dtd">
<dataset>
    <table name="test_table">
        <column>id</column>
        <column>value</column>
        <row>
            <value>1</value>
            <value>AAA</value>
        </row>
        <row>
            <value>3</value>
            <value>bbb</value>
        </row>
        <row>
            <value>4</value>
            <value>CCC</value>
        </row>
    </table>
</dataset>
  • わざと複数の差分が発生するように期待値を設定している
package sandbox.dbunit;

import org.dbunit.dataset.IDataSet;
import org.dbunit.dataset.xml.XmlDataSet;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import static org.dbunit.Assertion.assertEquals;

public class CollectDifferencesTest {
    @RegisterExtension
    static MyDbUnitExtension myDbUnitExtension = new MyDbUnitExtension();

    @BeforeAll
    static void beforeAll() {
        myDbUnitExtension.sql("""
        create table test_table (
            id integer primary key,
            value varchar(8)
        )""");
    }

    @BeforeEach
    void beforeEach() throws Exception {
        XmlDataSet setUp = myDbUnitExtension
            .readXmlDataSet("/sandbox/dbunit/CollectDifferencesTest/setUp.xml");
        myDbUnitExtension.getDatabaseTester().setDataSet(setUp);
        myDbUnitExtension.getDatabaseTester().onSetup();
    }

    @Test
    void testDefault() throws Exception {
        IDataSet actual = myDbUnitExtension.getConnection().createDataSet();
        XmlDataSet expected = myDbUnitExtension
            .readXmlDataSet("/sandbox/dbunit/CollectDifferencesTest/expected.xml");

        assertEquals(expected, actual);
    }
}
  • setUp.xml を投入して、 expected.xml と比較
実行結果
org.dbunit.assertion.DbComparisonFailure[Actual value='aaa' is not equal to expected value='AAA': value (table=test_table, row=0, col=value)expected:<AAA>but was:<aaa>]
  • デフォルトでは、カラムの値を比較していって最初に差分が見つかった時点で即座にテストは失敗する
  • もし、すべての差分を集めてからテストを失敗させたい場合は、 DiffCollectingFailureHandler を使用する
DiffCollectingFailureHandlerで差分をすべて収集する例
    @Test
    void testCollectDifferences() throws Exception {
        IDataSet actual = myDbUnitExtension.getConnection().createDataSet();
        XmlDataSet expected = myDbUnitExtension
            .readXmlDataSet("/sandbox/dbunit/CollectDifferencesTest/expected.xml");

        DiffCollectingFailureHandler failureHandler = new DiffCollectingFailureHandler();

        assertEquals(expected, actual, failureHandler);

        @SuppressWarnings("unchecked")
        List<Difference> diffList = failureHandler.getDiffList();

        if (!diffList.isEmpty()) {
            String errorMessage = diffList.stream()
                    .map(diff -> String.format("row=%d, column=%s, failMessage=%s",
                            diff.getRowIndex(),
                            diff.getColumnName(),
                            diff.getFailMessage()))
                    .collect(Collectors.joining("\n"));
            fail(errorMessage);
        }
    }
実行結果
org.opentest4j.AssertionFailedError: row=0, column=value, failMessage=Actual value='aaa' is not equal to expected value='AAA'
row=1, column=id, failMessage=Actual value='2' is not equal to expected value='3'
row=2, column=id, failMessage=Actual value='3' is not equal to expected value='4'
row=2, column=value, failMessage=Actual value='ccc' is not equal to expected value='CCC'
  • すべての差分の情報が収集できている
  • assertEquals() には、第三引数に FailureHandler を受け取ることができるメソッドが用意されている
  • FailureHandler は、検証でエラーになったときのもろもろの制御を定義するインタフェースとなっている
  • DiffCollectingFailureHandler は、カラムの値に差分があっても例外をスローせずに内部の List に差分(Difference)を収集するようになっている
  • したがって、差分があっても assertEquals() は失敗せずに戻るようになっている
  • DiffCollectingFailureHandlergetDiffList() で、収集された Difference のリストを取得できる
    • まさかの raw 型!
    • また、 Difference をそのまま toString() しただけだと余分な情報が多すぎて読みにくいので、適度に必要な情報だけに絞って出力した方がいい

DiffCollectingFailureHandler を指定した場合に assertEquals() が例外をスローしないのは各カラムの値を比較したときに差分があった場合の話で、テーブル数やレコード数に差があったり、カラム数が異なっているなどそもそもカラムの値の比較ができないような状態では即座に例外がスローされるので注意。

DiffCollectingFailureHandler の実装はかなりアレなので、自分なら代わりに以下のようなクラスを作る気がする。

package sandbox.dbunit;

import org.dbunit.assertion.DefaultFailureHandler;
import org.dbunit.assertion.Difference;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;

import static org.junit.jupiter.api.Assertions.fail;

public class MyDiffCollectingFailureHandler extends DefaultFailureHandler {

    private List<Difference> diffList = new ArrayList<>();

    @Override
    public void handle(Difference diff) {
        diffList.add(diff);
    }

    public void failIfExistsDifferences() {
        if (diffList.isEmpty()) {
            return;
        }
        String errorMessage = diffList.stream()
                .map(diff -> String.format("row=%d, column=%s, failMessage=%s",
                        diff.getRowIndex(),
                        diff.getColumnName(),
                        diff.getFailMessage()))
                .collect(Collectors.joining("\n"));
        fail(errorMessage);
    }
}

データ型

数値や文字列型の項目の場合は、データセットファイルに書いた値がそのままデータベースに取り込まれる。
一方、 BLOB 型や日付型の項目の場合、書式はどう指定すればいいのか以下でまとめる。

BLOB 型

setUp.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE dataset SYSTEM "dataset.dtd">
<dataset>
    <table name="test_table">
        <column>id</column>
        <column>text</column>
        <column>base64</column>
        <column>file</column>
        <column>url</column>
        <row>
            <value>1</value>
            <value>[TEXT UTF-8]あいうえお</value>
            <value>[BASE64]44GL44GN44GP44GR44GT</value><!-- UTF-8でデコードすると「かきくけこ」になる -->
            <value>[FILE]./src/test/resources/sandbox/dbunit/BlobTest/file.txt</value>
            <value>[URL]file:./src/test/resources/sandbox/dbunit/BlobTest/url.txt</value>
        </row>
    </table>
</dataset>
./src/test/resources/sandbox/dbunit/BlobTest/file.txt
さしすせそ
./src/test/resources/sandbox/dbunit/BlobTest/url.txt
たちつてと
package sandbox.dbunit;

import org.dbunit.dataset.ITable;
import org.dbunit.dataset.xml.XmlDataSet;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import java.nio.charset.StandardCharsets;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class BlobTest {
    @RegisterExtension
    static MyDbUnitExtension myDbUnitExtension = new MyDbUnitExtension();

    @BeforeAll
    static void beforeAll() {
        // DB初期化(テーブル作成)
        myDbUnitExtension.sql("""
        create table test_table (
            id integer primary key,
            text blob,
            base64 blob,
            file blob,
            url blob
        )""");
    }

    @BeforeEach
    void beforeEach() throws Exception {
        XmlDataSet setUpDataSet = myDbUnitExtension
            .readXmlDataSet("/sandbox/dbunit/BlobTest/setUp.xml");
        myDbUnitExtension.getDatabaseTester().setDataSet(setUpDataSet);

        myDbUnitExtension.getDatabaseTester().onSetup();
    }

    @Test
    void test() throws Exception {
        ITable testTable = myDbUnitExtension
            .getConnection().createDataSet().getTable("test_table");

        // 以下のテストはすべて成功する
        assertEquals(toString(testTable.getValue(0, "text")), "あいうえお");
        assertEquals(toString(testTable.getValue(0, "base64")), "かきくけこ");
        assertEquals(toString(testTable.getValue(0, "file")), "さしすせそ");
        assertEquals(toString(testTable.getValue(0, "url")), "たちつてと");
    }

    private String toString(Object value) {
        byte[] bytes = (byte[]) value;
        return new String(bytes, StandardCharsets.UTF_8);
    }
}

BLOB 型の項目に値を設定する場合は、以下のような書式で値を記述する。

BLOB型項目用の書式
[DATA_TYPE]value
  • DATA_TYPE でバイナリデータを読み込む方法を指定して、 value で読み込むデータのソースを指定する。
  • DATA_TYPE には、以下のいずれかが指定できる
    • TEXT
      • value で指定した値を文字列として扱い、バイナリにエンコードした値を読み込む
      • [TEXT UTF-8]のようにして、エンコードするときの文字コードを指定できる
      • 未指定の場合は、デフォルトで UTF-8 でエンコードされる
    • BASE64
      • value で指定した値を、 Base64 でエンコードされた文字列として読み込む
    • FILE
      • value で指定した値をファイルのパスとして扱い、ファイルの内容を読み込む
    • URL
      • value で指定した値を URL として扱い、 URL から読み取った内容をバイナリとして読み込む
      • http://~ のような URL を指定すれば、インターネット経由でファイルを読み込むようなことも可能

日付型

setUp.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE dataset SYSTEM "dataset.dtd">
<dataset>
    <table name="test_table">
        <column>id</column>
        <column>date_value</column>
        <column>time_value</column>
        <column>timestamp_value</column>
        <row>
            <value>1</value>
            <value>2022-7-12</value>
            <value>12:13:14</value>
            <value>2022-08-01 12:30:42.123</value>
        </row>
    </table>
</dataset>
package sandbox.dbunit;

import org.dbunit.dataset.ITable;
import org.dbunit.dataset.xml.XmlDataSet;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import java.sql.Date;
import java.sql.Time;
import java.sql.Timestamp;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class DateTest {
    @RegisterExtension
    static MyDbUnitExtension myDbUnitExtension = new MyDbUnitExtension();

    @BeforeAll
    static void beforeAll() {
        // DB初期化(テーブル作成)
        myDbUnitExtension.sql("""
        create table test_table (
            id integer primary key,
            date_value date,
            time_value time,
            timestamp_value timestamp
        )""");
    }

    @BeforeEach
    void beforeEach() throws Exception {
        XmlDataSet setUpDataSet = myDbUnitExtension
            .readXmlDataSet("/sandbox/dbunit/DateTest/setUp.xml");
        myDbUnitExtension.getDatabaseTester().setDataSet(setUpDataSet);

        myDbUnitExtension.getDatabaseTester().onSetup();
    }

    @Test
    void test() throws Exception {
        ITable testTable = myDbUnitExtension
            .getConnection().createDataSet().getTable("test_table");

        Date dateValue = (Date) testTable.getValue(0, "date_value");
        Time timeValue = (Time) testTable.getValue(0, "time_value");
        Timestamp timestampValue = (Timestamp) testTable.getValue(0, "timestamp_value");

        // 以下のテストは全て成功する
        assertEquals(dateValue, Date.valueOf("2022-07-12"));
        assertEquals(timeValue, Time.valueOf("12:13:14"));
        assertEquals(timestampValue, Timestamp.valueOf("2022-08-01 12:30:42.123"));
    }
}

現在時刻からの相対時間を設定する

setUp.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE dataset SYSTEM "dataset.dtd">
<dataset>
    <table name="test_table">
        <column>id</column>
        <column>now</column>
        <column>date_value</column>
        <column>time_value</column>
        <column>timestamp_value</column>
        <row>
            <value>1</value>
            <value>[now]</value>
            <value>[now+2d]</value>
            <value>[now-3h+20m]</value>
            <value>[now+2y+1M 10:00]</value>
        </row>
    </table>
</dataset>
package sandbox.dbunit;

import org.dbunit.dataset.ITable;
import org.dbunit.dataset.xml.XmlDataSet;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

public class RelativeDateTest {
    @RegisterExtension
    static MyDbUnitExtension myDbUnitExtension = new MyDbUnitExtension();

    @BeforeAll
    static void beforeAll() {
        // DB初期化(テーブル作成)
        myDbUnitExtension.sql("""
        create table test_table (
            id integer primary key,
            now timestamp,
            date_value date,
            time_value time,
            timestamp_value timestamp
        )""");
    }

    @BeforeEach
    void beforeEach() throws Exception {
        XmlDataSet setUpDataSet = myDbUnitExtension
            .readXmlDataSet("/sandbox/dbunit/RelativeDateTest/setUp.xml");
        myDbUnitExtension.getDatabaseTester().setDataSet(setUpDataSet);

        myDbUnitExtension.getDatabaseTester().onSetup();
    }

    @Test
    void test() throws Exception {
        ITable testTable = myDbUnitExtension
            .getConnection().createDataSet().getTable("test_table");

        System.out.println("[now]             = " + testTable.getValue(0, "now"));
        System.out.println("[now+2d]          = " + testTable.getValue(0, "date_value"));
        System.out.println("[now-3h+20m]      = " + testTable.getValue(0, "time_value"));
        System.out.println("[now+2y+1M 10:00] = " + testTable.getValue(0, "timestamp_value"));
    }
}
実行結果
[now]             = 2022-07-05 22:31:49.514
[now+2d]          = 2022-07-07
[now-3h+20m]      = 19:51:49
[now+2y+1M 10:00] = 2024-08-05 10:00:00.0
  • 日時系の型には、現在日時からの相対的な時間を設定するための特別な構文が用意されている
相対時間の書式
[now{DIFF}{TIME}]
  • DIFF には、現在日時からの差分を指定する
    • 差分は +1y, -2d のように、 +, - の後に時間量を続けることで記述する
    • 時間量は 1y, 10m のように、量と単位をつなげて記述する
    • 時間量の単位には、以下のいずれかが使用できる
      • y: 年
      • M: 月
      • d: 日
      • h: 時
      • m: 分
      • s: 秒
    • 差分は +1d-2m+3s のように連続して記述することができる
  • TIME を指定した場合は、時分秒が指定された値に置き換わる

データセット

データセットを定義するための様々なファイルの書き方についてまとめる。

FlatXmlDataSet

testStandard.xml
<?xml version="1.0" encoding="UTF-8" ?>
<dataset>
    <foo_table id="1" value="hoge" />
    <bar_table id="1" foo_id="1" />

    <foo_table id="2" />
    <bar_table id="2" foo_id="2" />

    <foo_table id="3" value="piyo" />
</dataset>
FlatXmlDataSetTest.java
package sandbox.dbunit;

import org.dbunit.dataset.xml.FlatXmlDataSet;
import org.dbunit.dataset.xml.FlatXmlDataSetBuilder;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import java.net.URL;

public class FlatXmlDataSetTest {
    @RegisterExtension
    static MyDbUnitExtension myDbUnitExtension = new MyDbUnitExtension();

    @BeforeAll
    static void beforeAll() {
        // DB初期化(テーブル作成)
        myDbUnitExtension.sql("""
        create table foo_table (
            id integer primary key,
            value varchar(32)
        )""");
        myDbUnitExtension.sql("""
        create table bar_table (
            id integer primary key,
            foo_id integer,
            foreign key (foo_id) references foo_table (id)
        )""");
    }

    @Test
    void testStandard() throws Exception {
        FlatXmlDataSetBuilder builder = new FlatXmlDataSetBuilder();
        URL xml = this.getClass()
            .getResource("/sandbox/dbunit/FlatXmlDataSetTest/testStandard.xml");
        FlatXmlDataSet dataSet = builder.build(xml);
        myDbUnitExtension.getDatabaseTester().setDataSet(dataSet);
        myDbUnitExtension.getDatabaseTester().onSetup();

        myDbUnitExtension.printTable("foo_table");
        myDbUnitExtension.printTable("bar_table");
    }
}
実行結果
foo_table {
  ID=1, VALUE='hoge'
  ID=2, VALUE=null
  ID=3, VALUE='piyo'
}
bar_table {
  ID=1, FOO_ID=1
  ID=2, FOO_ID=2
}
  • FlatXmlDataSet では、 XML でデータセットを記述する
    • トップレベルに書くタグが、テーブルの1レコードに対応する
    • タグ名がテーブル名となる
    • カラムの値は、タグの属性に書く
    • 属性を省略した場合、そのカラムには null が設定される
    • データは、 XML ファイル内の上から順番に登録される
  • FlatXmlDataSet のインスタンスは、 FlatXmlDataSetBuilder を使って生成する

1レコード目にnull値を設定する場合の注意

testFirstRecordHasNull.xml
<?xml version="1.0" encoding="UTF-8" ?>
<dataset>
    <foo_table id="1" />
    <foo_table id="2" value="fuga" />
    <foo_table id="3" value="piyo" />
</dataset>
実行結果
foo_table {
  ID=1, VALUE=null
  ID=2, VALUE=null
  ID=3, VALUE=null
}
  • FlatXmlDataSet では、1行目のレコードが持つカラムでテーブルの定義が決まる
  • このため、1行目で value を省略して null にすると、 foo_table には id カラムしか存在しない扱いになり、残りのレコードでは value 属性が無視されてしまう
  • この問題は、以下のいずれかの方法で回避できる
    • DTD を定義する
    • columnSensingtrue を設定する
  • 後者の方法を以下で説明する
FlatXmlDataSetTest.java
    @Test
    void testColumnSensingTrue() throws Exception {
        FlatXmlDataSetBuilder builder = new FlatXmlDataSetBuilder();

        builder.setColumnSensing(true); // true を設定

        URL xml = this.getClass()
            .getResource("/sandbox/dbunit/FlatXmlDataSetTest/testFirstRecordHasNull.xml");
        FlatXmlDataSet dataSet = builder.build(xml);
        myDbUnitExtension.getDatabaseTester().setDataSet(dataSet);
        myDbUnitExtension.getDatabaseTester().onSetup();

        myDbUnitExtension.printTable("foo_table");
    }
実行結果
foo_table {
  ID=1, VALUE=null
  ID=2, VALUE='fuga'
  ID=3, VALUE='piyo'
}
  • FlatXmlDataSetBuildersetColumnSensing()true を設定すると、レコードをすべて読み込んだうえでカラム定義が決まるため、1レコード目に null 値を設定していても残りのレコードが無視されることはなくなる

XmlDataSet

test.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE dataset SYSTEM "dataset.dtd">
<dataset>
    <table name="foo_table">
        <column>id</column>
        <column>value1</column>
        <column>value2</column>
        <row>
            <value>1</value>
            <value>hoge</value>
        </row>
        <row>
            <value>2</value>
            <value>foo</value>
            <value>bar</value>
        </row>
        <row>
            <value>3</value>
            <null />
            <value>fuga</value>
        </row>
    </table>
    <table name="bar_table">
        <column>id</column>
        <column>foo_id</column>
        <row>
            <value>1</value>
            <value>1</value>
        </row>
        <row>
            <value>2</value>
            <value>2</value>
        </row>
    </table>
</dataset>
package sandbox.dbunit;

import org.dbunit.dataset.xml.XmlDataSet;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import java.io.InputStream;

public class XmlDataSetTest {
    @RegisterExtension
    static MyDbUnitExtension myDbUnitExtension = new MyDbUnitExtension();

    @BeforeAll
    static void beforeAll() {
        myDbUnitExtension.sql("""
        create table foo_table (
            id integer primary key,
            value1 varchar(8),
            value2 varchar(8)
        )""");
        myDbUnitExtension.sql("""
        create table bar_table (
            id integer primary key,
            foo_id integer,
            foreign key (foo_id) references foo_table (id)
        )""");
    }

    @Test
    void test() throws Exception {
        try (
            InputStream inputStream = this.getClass()
                .getResourceAsStream("/sandbox/dbunit/XmlDataSetTest/test.xml");
        ) {

            XmlDataSet xmlDataSet = new XmlDataSet(inputStream);
            myDbUnitExtension.getDatabaseTester().setDataSet(xmlDataSet);
            myDbUnitExtension.getDatabaseTester().onSetup();
        }

        myDbUnitExtension.printTable("foo_table");
        myDbUnitExtension.printTable("bar_table");
    }
}
実行結果
foo_table {
  ID=1, VALUE1='hoge', VALUE2=null
  ID=2, VALUE1='foo', VALUE2='bar'
  ID=3, VALUE1=null, VALUE2='fuga'
}
bar_table {
  ID=1, FOO_ID=1
  ID=2, FOO_ID=2
}
  • XmlDataSet では、 XML でデータセットを定義する
  • FlatXmlDataSet と違って、より汎用的なフォーマットとなっている
  • <table> タグで、テーブルごとのデータを定義する
    • name 属性でテーブルの名前を設定する
    • <table> タグ内の先頭で、 <column> タグを使って存在するカラムと順番を定義する
  • <row> タグで、1レコードずつデータを定義する
    • <value> タグで、各カラムの値を設定する
    • <value> に指定した値は、 <column> で定義した順序でカラムに設定される
    • <column> で定義したカラム数に対して <value> の数が不足している場合は、足りないカラムには null が設定される
    • 明示的に null を設定する場合は、 <null /> タグを設定する
  • データは、 XML ファイル内の上から順番に登録される

XlsDataSet

build.gradle
dependencies {
    ...
    testRuntimeOnly "org.apache.poi:poi-ooxml:4.1.0" // 追加
}

test.xlsx

image.png

image.png

package sandbox.dbunit;

import org.dbunit.dataset.excel.XlsDataSet;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import java.io.InputStream;

public class XlsDataSetTest {
    @RegisterExtension
    static MyDbUnitExtension myDbUnitExtension = new MyDbUnitExtension();

    @BeforeAll
    static void beforeAll() {
        myDbUnitExtension.sql("""
        create table foo_table (
            id integer primary key,
            value_1 varchar(8),
            value_2 varchar(8)
        )""");
        myDbUnitExtension.sql("""
        create table bar_table (
            id integer primary key,
            foo_id integer,
            foreign key (foo_id) references foo_table (id)
        )""");
    }

    @Test
    void test() throws Exception {
        try (
            InputStream inputStream = this.getClass()
                .getResourceAsStream("/sandbox/dbunit/XlsDataSetTest/test.xlsx");
        ) {
            XlsDataSet xlsDataSet = new XlsDataSet(inputStream);
            myDbUnitExtension.getDatabaseTester().setDataSet(xlsDataSet);
            myDbUnitExtension.getDatabaseTester().onSetup();
        }

        myDbUnitExtension.printTable("foo_table");
        myDbUnitExtension.printTable("bar_table");
    }
}
実行結果
foo_table {
  ID=1, VALUE_1='foo', VALUE_2=null
  ID=2, VALUE_1=null, VALUE_2=null
  ID=3, VALUE_1='bar', VALUE_2=null
}
bar_table {
  ID=1, FOO_ID=1
  ID=2, FOO_ID=2
}
  • XlsDataSet では、 Excel でデータセットを定義する
  • XlsDataSet を使う場合は、依存関係に Apache POI を追加する必要がある
    • *.xls 形式を使う場合は org.apache.poi:poi を追加する
    • *.xlsx 形式を使う場合は org.apache.poi:poi-ooxml を追加する
    • どのバージョンを入れるのが良いかドキュメントには見当たらなかったのでソースを確認したら 4.1.0 を使ってた
  • 1つのシートに1つのテーブルのデータを設定する
    • シート名がテーブル名となる
  • シートの1行目は、カラム名を設定する
  • 2行目以降にデータを記述する
  • 書式による自動変換による事故を避けるため、全セル文字列形式にしておいたほうがいい気がする(個人的意見)
  • 空セルは null になる
    • 昔のブログとかを見ると空セルは空文字になるってあるけど、挙動変わった?
    • 逆に空文字を設定するために ReplacementDataSet を使うことになるのかも(後述)
  • Excel に定義しなかったカラムには null が設定される
  • データは、左のシートから順番に登録される

空セルを空文字として設定する

昔は空セルが空文字として登録されてしまうため、 ReplacementDataSet というのを使って null に置き換えたりする必要があったっぽい。

しかし、現在は上述のように空セルは null になる。
ということは、逆に空セルを空文字として登録したい場合は、 ReplacementDataSet を使うことになる(少なくとも、ドキュメントには空文字を明示的に設定する方法とかは言及されてない)。

    @Test
    void testWithReplacementDataSet() throws Exception {
        myDbUnitExtension.getDatabaseTester()
            .setOperationListener(new DefaultOperationListener() {
                @Override
                public void connectionRetrieved(IDatabaseConnection connection) {
                    super.connectionRetrieved(connection);
                    DatabaseConfig config = connection.getConfig();
                    config.setProperty(DatabaseConfig.FEATURE_ALLOW_EMPTY_FIELDS, true);
                }
            });
        
        try (
            InputStream inputStream = this.getClass()
                .getResourceAsStream("/sandbox/dbunit/XlsDataSetTest/test.xlsx");
        ) {
            XlsDataSet xlsDataSet = new XlsDataSet(inputStream);
            ReplacementDataSet replacementDataSet = new ReplacementDataSet(xlsDataSet);
            replacementDataSet.addReplacementObject(null, "");
            myDbUnitExtension.getDatabaseTester().setDataSet(replacementDataSet);
            myDbUnitExtension.getDatabaseTester().onSetup();
        }

        myDbUnitExtension.printTable("foo_table");
    }
実行結果
foo_table {
  ID=1, VALUE_1='foo', VALUE_2=null
  ID=2, VALUE_1='', VALUE_2=null
  ID=3, VALUE_1='bar', VALUE_2=null
}
  • ReplacementDataSet を使うと、データセット内の特定の値を別の値に差し替えることができる
  • addReplacementObject(Object, Object) で、差し替えの内容を登録する
    • 第一引数が変更前の値
    • 第二引数が変更後の値
  • そのままだと、 table.column=foo_table.VALUE_1 value is empty but must contain a value (to disable this feature check, set DatabaseConfig.FEATURE_ALLOW_EMPTY_FIELDS to true) というエラーが発生して、空文字での投入はできなかった
  • エラーメッセージに従い、 FEATURE_ALLOW_EMPTY_FIELDStrue を設定するようにしたら、空文字で登録ができた
    • DatabaseConfig の設定方法の詳細については後述

CsvDataSet / CsvURLDataSet

フォルダ構成
`-src/test/
  |-java/
  | `-sandbox/dbunit/CsvURLDataSetTest.java
  `-resources/
    `-sandbox/dbunit/CsvURLDataSetTest/
      |-foo_table.csv
      |-bar_table.csv
      `-table-ordering.txt
foo_table.csv
id,value_1
1,hoge
"2","\"hello,world\""
3,
4,""
5,null
6,"null"
7,'null'
bar_table.csv
id,foo_id
   1,   1
2   ,2
   3   ,"   3   "
table-ordering.txt
foo_table
bar_table
CsvURLDataSetTest.java
package sandbox.dbunit;

import org.dbunit.DefaultOperationListener;
import org.dbunit.database.DatabaseConfig;
import org.dbunit.database.IDatabaseConnection;
import org.dbunit.dataset.csv.CsvURLDataSet;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import java.net.URL;

public class CsvURLDataSetTest {
    @RegisterExtension
    static MyDbUnitExtension myDbUnitExtension = new MyDbUnitExtension();

    @BeforeAll
    static void beforeAll() {
        myDbUnitExtension.sql("""
        create table foo_table (
            id integer primary key,
            value_1 varchar(32),
            value_2 varchar(32)
        )""");
        myDbUnitExtension.sql("""
        create table bar_table (
            id integer primary key,
            foo_id integer,
            foreign key (foo_id) references foo_table (id)
        )""");
    }

    @Test
    void test() throws Exception {
        myDbUnitExtension.getDatabaseTester()
            .setOperationListener(new DefaultOperationListener() {
                @Override
                public void connectionRetrieved(IDatabaseConnection connection) {
                    super.connectionRetrieved(connection);
                    final DatabaseConfig config = connection.getConfig();
                    config.setProperty(DatabaseConfig.FEATURE_ALLOW_EMPTY_FIELDS, true);
                }
            });

        URL base = this.getClass().getResource("/sandbox/dbunit/CsvURLDataSetTest/");
        CsvURLDataSet csvURLDataSet = new CsvURLDataSet(base);
        myDbUnitExtension.getDatabaseTester().setDataSet(csvURLDataSet);
        myDbUnitExtension.getDatabaseTester().onSetup();

        myDbUnitExtension.printTable("foo_table");
        myDbUnitExtension.printTable("bar_table");
    }
}
実行結果
foo_table {
  ID=1, VALUE_1='hoge', VALUE_2=null
  ID=2, VALUE_1='"hello,world"', VALUE_2=null
  ID=3, VALUE_1='', VALUE_2=null
  ID=4, VALUE_1='', VALUE_2=null
  ID=5, VALUE_1=null, VALUE_2=null
  ID=6, VALUE_1=null, VALUE_2=null
  ID=7, VALUE_1=''null'', VALUE_2=null
}
bar_table {
  ID=1, FOO_ID=1
  ID=2, FOO_ID=2
  ID=3, FOO_ID=3
}
  • CsvDataSet または CsvURLDataSet では、 CSV でデータセットを定義する
    • CsvDataSet は、ベースディレクトリを File で指定する
    • CsvURLDataSet は、ベースディレクトリを URL で指定する
      • 末尾の / を忘れるとディレクトリ扱いされないので注意
  • それぞれのクラスのコンストラクタには、データセットを定義したファイルを配置しているディレクトリ(仮にベースディレクトリと呼称)を指定する
  • ベースディレクトリには、以下のファイルを配置する
    • テーブルごとにデータを定義した CSV ファイル
      • ファイル名はテーブル名と一致させる
    • 投入するテーブル名を列挙したファイル
      • ファイル名は table-ordering.txt 固定
      • このファイルに記載されている順序でデータが投入される
  • 各テーブルごとの CSV ファイルは、以下の書式で記述する
    • 基本ルール
      • カラムの区切り文字はカンマ(,)
      • 囲い文字はダブルクォーテーション(")
        • 囲い文字は、あってもなくてもいい
      • 囲い文字の中でダブルクォーテーションを記述したい場合は、バックスラッシュでエスケープする(\")
    • 1行目にはカラム名を記載する
      • ここで定義されていないカラムには null が設定される
    • 2行目以降に、テーブルに投入するデータを記載する
      • 空文字は、そのまま空文字として扱われる
        • 設定で FEATURE_ALLOW_EMPTY_FIELDStrue にしておかないとエラーになるので注意
      • null または "null" と記述すると、 null 値として扱われる
        • 基本的に需要はないと思うけど、 null という文字列を設定したい場合は XlsDataSet のときみたいに ReplacementDataSet を使って何とかすることになると思う
      • ドキュメントでは、カンマ区切り文字の前にある数値項目の末尾に空白スペースを入れたら文字列扱いされてしまうからするな、って書いてある
        • Numeric CSV data file fields must not have trailing spaces before the comma-separator as will interpret it as a String instead of a number.

        • しかし、実際にやってみると普通に数値として登録できているのでドキュメントが間違ってるっぽい

StreamingDataSet

XmlDataSet などの他のデータセットは、すべてのデータをメモリ上に読み込むようになっている。
データのサイズが小さければ問題ないが、大きくなってくると OOME などの問題が発生する可能性がある。

StreamingDataSet を使用すると、使用する分だけデータを順次メモリ上に読み込むようになるので、この問題を回避できるようになる。

以下、 StreamingDataSet を使う場合とそうでない場合とで、メモリの使用量がどう変わるかを検証する。

StreamingDataSet の検証ではメモリ使用量を確認しているため、ここだけ DB にはオンメモリの HSQLDB は使わずに PostgreSQL (14.4)を使用している。
JDBC ドライバの依存は以下。

budil.gradle
dependencies {
    ...
    testRuntimeOnly "org.postgresql:postgresql:42.4.0"
}
setUp.xml
<?xml version="1.0" encoding="UTF-8" ?>
<dataset>
<test_table id="1" value="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" />
<test_table id="2" value="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" />
<test_table id="3" value="aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" />

以下、同様のレコードが 50 万行続く...

</dataset>
  • 巨大なデータセットファイル
  • ファイルサイズは、約 500 MB
build.gradle
test {
    ...
    minHeapSize = "768m"
    maxHeapSize = "768m"
}
  • テスト時のヒープメモリのサイズを 768MB に設定
    • XML のテストデータを全部読み込んでもちょっと余るくらいにしている
    • 2GB とか余裕ありすぎなサイズにすると、全然 GC が発生せず StreamingDataSet を使っているときと使っていないときでメモリ消費に差が出なくなる3ので、これくらいのサイズにしている
package sandbox.dbunit;

import org.dbunit.DefaultOperationListener;
import org.dbunit.database.IDatabaseConnection;
import org.dbunit.dataset.xml.FlatXmlDataSet;
import org.dbunit.dataset.xml.FlatXmlDataSetBuilder;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import java.net.URL;

public class StreamingDataSetTest {
    @RegisterExtension
    static MyDbUnitExtension myDbUnitExtension = new MyDbUnitExtension();

    @BeforeAll
    static void beforeAll() {
        // DB初期化(テーブル作成)
        myDbUnitExtension.sql("drop table if exists test_table");
        myDbUnitExtension.sql("""
        create table test_table (
            id integer primary key,
            value varchar(1024)
        )""");
    }

    @Test
    void testNoStreaming() throws Exception {
        myDbUnitExtension.getDatabaseTester()
            .setOperationListener(new DefaultOperationListener() {
                @Override
                public void connectionRetrieved(IDatabaseConnection connection) {
                    super.connectionRetrieved(connection);
                    connection.getConfig()
                        .setProperty(DatabaseConfig.FEATURE_BATCHED_STATEMENTS, true);
                }
            });

        FlatXmlDataSetBuilder builder = new FlatXmlDataSetBuilder();
        URL url =
            this.getClass().getResource("/sandbox/dbunit/StreamingDataSetTest/setUp.xml");
        FlatXmlDataSet dataSet = builder.build(url);
        myDbUnitExtension.getDatabaseTester().setDataSet(dataSet);

        myDbUnitExtension.getDatabaseTester().onSetup();
    }
}
  • まずは、 StreamingDataSet を使わずに FlatXmlDataSet で読み込んだ場合のメモリ消費の様子を確認
  • 実装の細かい説明は後述

FlatXmlDataSet で読み込んだ場合のメモリ使用量推移

noStreamin_3.jpg

  • 約 600MB 以上消費された状態が続いている
  • XML のファイルサイズは約 500MB なので、 XML の内容がずっとメモリ上に確保され続けていることが分かる

次に、 StreamingDataSet を使って読み込んでみる。

import org.dbunit.dataset.stream.StreamingDataSet;
import org.dbunit.dataset.xml.FlatXmlDataSetBuilder;
import org.dbunit.dataset.xml.FlatXmlProducer;
import org.dbunit.operation.DatabaseOperation;
import org.xml.sax.InputSource;
...
public class StreamingDataSetTest {
    ...

    @Test
    void testStreaming() throws Exception {
        myDbUnitExtension.getDatabaseTester().setSetUpOperation(DatabaseOperation.INSERT);

        myDbUnitExtension.getDatabaseTester()
            .setOperationListener(new DefaultOperationListener() {
                @Override
                public void connectionRetrieved(IDatabaseConnection connection) {
                    super.connectionRetrieved(connection);
                    connection.getConfig()
                        .setProperty(DatabaseConfig.FEATURE_BATCHED_STATEMENTS, true);
                }
            });

        URL url =
            this.getClass().getResource("/sandbox/dbunit/StreamingDataSetTest/setUp.xml");
        FlatXmlProducer producer = new FlatXmlProducer(new InputSource(url.toString()));
        StreamingDataSet dataSet = new StreamingDataSet(producer);
        myDbUnitExtension.getDatabaseTester().setDataSet(dataSet);

        myDbUnitExtension.getDatabaseTester().onSetup();
    }

StreamingDataSet で読み込んだ場合のメモリ使用量推移

streaming_3.jpg

  • 500MB くらいまで行くたびに一気にメモリが解放されているのが分かる
  • XML のデータは順次読み込まれているだけで、全部が一気にメモリ上に読み込まれているわけではないことが分かる

実装の説明

connection.getConfig().setProperty(DatabaseConfig.FEATURE_BATCHED_STATEMENTS, true);
  • デフォルトでは、データの登録に JDBC のバッチ更新は使用されない
    • FEATURE_BATCHED_STATEMENTStrue を設定すると、バッチ更新が有効になる
    • デフォルトが false の理由は、 DB 製品によってはバッチ更新をサポートしていない可能性があるため
    • しかし、 DBUnit としてはサポートされているなら true を設定することを推奨している
      • ただし、データセットのサイズがそれほど大きくない場合は、あまり影響はないとも言っている
  • バッチ更新が使用される場合、デフォルトは 100 件ごとに executeBatch() が実行される
        URL url =
            this.getClass().getResource("/sandbox/dbunit/StreamingDataSetTest/setUp.xml");
        FlatXmlProducer producer = new FlatXmlProducer(new InputSource(url.toString()));
        StreamingDataSet dataSet = new StreamingDataSet(producer);
        myDbUnitExtension.getDatabaseTester().setDataSet(dataSet);
  • StreamingDataSet で順次データを読み込むには、 IDataSetProducer を実装したクラスを使用してデータセットを読み込む必要がある
  • ここでは、 FlatXmlDataSet 用の IDataSetProducer 実装である FlatXmlProducer を使用している
        myDbUnitExtension.getDatabaseTester().setSetUpOperation(DatabaseOperation.INSERT);
  • セットアップ時の処理を、デフォルトの CLEAN_INSERT から INSERT に変更している
  • IDatabaseTester.onSetup()StreamingDataSet を使ってデータを読み込む場合は、こうしないと以下のエラーになる
    • java.lang.UnsupportedOperationException: Only one iterator allowed!
  • StreamingDataSet は、その性質上データセットファイルを一度しかイテレーションできず、もう一度読み込もうとすると上述のエラーが発生する
  • セットアップ時のデフォルトである CLEAN_INSERT は、 CLEAN(DELETE)INSERT でそれぞれ1回ずつ setDataSet() で設定したデータセットを読み込もうとする
  • したがって、デフォルトの CLEAN_INSERT のままだとエラーが発生してしまう
  • このため、 onSetup()StreamingDataSet を使ってデータセットを読み込みたい場合は、 INSERT を設定しておく必要がある

CompositeDataSet

dataSet1.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE dataset SYSTEM "dataset.dtd">
<dataset>
    <table name="foo_table">
        <column>id</column>
        <column>value</column>
        <row>
            <value>1</value>
            <value>hello</value>
        </row>
    </table>
</dataset>
dataSet2.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE dataset SYSTEM "dataset.dtd">
<dataset>
    <table name="bar_table">
        <column>id</column>
        <column>foo_id</column>
        <row>
            <value>1</value>
            <value>1</value>
        </row>
    </table>
    <table name="foo_table">
        <column>id</column>
        <column>value</column>
        <row>
            <value>2</value>
            <value>world</value>
        </row>
    </table>
</dataset>
package sandbox.dbunit;

import org.dbunit.dataset.CompositeDataSet;
import org.dbunit.dataset.xml.XmlDataSet;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

public class CompositeDataSetTest {
    @RegisterExtension
    static MyDbUnitExtension myDbUnitExtension = new MyDbUnitExtension();

    @BeforeAll
    static void beforeAll() {
        myDbUnitExtension.sql("""
        create table foo_table (
            id integer primary key,
            value varchar(8)
        )""");
        myDbUnitExtension.sql("""
        create table bar_table (
            id integer primary key,
            foo_id integer,
            foreign key (foo_id) references foo_table (id)
        )""");
    }

    @Test
    void test() throws Exception {
        XmlDataSet dataSet1 = myDbUnitExtension
            .readXmlDataSet("/sandbox/dbunit/CompositeDataSetTest/dataSet1.xml");
        XmlDataSet dataSet2 = myDbUnitExtension
            .readXmlDataSet("/sandbox/dbunit/CompositeDataSetTest/dataSet2.xml");

        CompositeDataSet compositeDataSet = new CompositeDataSet(dataSet1, dataSet2);
        myDbUnitExtension.getDatabaseTester().setDataSet(compositeDataSet);

        myDbUnitExtension.getDatabaseTester().onSetup();

        myDbUnitExtension.printTable("foo_table");
        myDbUnitExtension.printTable("bar_table");
    }
}
実行結果
foo_table {
  ID=1, VALUE='hello'
  ID=2, VALUE='world'
}
bar_table {
  ID=1, FOO_ID=1
}
  • CompositeDataSet を使うと、複数の IDataSet を1つにまとめることができる
  • 同じテーブルが複数のデータセットに存在する場合は、1つにマージされる
  • データの登録順序は、データセットを前から読んでいって現れた順番になる

FilteredDataSet

testStandard.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE dataset SYSTEM "dataset.dtd">
<dataset>
    <table name="fuga_table">
        <column>id</column>
        <column>value</column>
        <row>
            <value>1</value>
            <value>FUGA</value>
        </row>
    </table>
    <table name="piyo_table">
        <column>id</column>
        <column>hoge_id</column>
        <row>
            <value>1</value>
            <value>1</value>
        </row>
    </table>
    <table name="hoge_table">
        <column>id</column>
        <column>value</column>
        <row>
            <value>1</value>
            <value>HOGE</value>
        </row>
    </table>
</dataset>
package sandbox.dbunit;

import org.dbunit.dataset.FilteredDataSet;
import org.dbunit.dataset.xml.XmlDataSet;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

public class FilteredDataSetTest {
    @RegisterExtension
    static MyDbUnitExtension myDbUnitExtension = new MyDbUnitExtension();

    @BeforeAll
    static void beforeAll() {
        // DB初期化(テーブル作成)
        myDbUnitExtension.sql("""
        create table hoge_table (
            id integer primary key,
            value varchar(32)
        )""");
        myDbUnitExtension.sql("""
        create table fuga_table (
            id integer primary key,
            value varchar(32)
        )""");
        myDbUnitExtension.sql("""
        create table piyo_table (
            id integer primary key,
            hoge_id integer,
            foreign key (hoge_id) references hoge_table (id)
        )""");
    }

    @Test
    void test() throws Exception {
        XmlDataSet xmlDataSet = myDbUnitExtension
            .readXmlDataSet("/sandbox/dbunit/FilteredDataSetTest/testStandard.xml");
        FilteredDataSet filteredDataSet =
            new FilteredDataSet(new String[]{"hoge_table", "piyo_table"}, xmlDataSet);
        myDbUnitExtension.getDatabaseTester().setDataSet(filteredDataSet);
        myDbUnitExtension.getDatabaseTester().onSetup();

        myDbUnitExtension.printTable("hoge_table");
        myDbUnitExtension.printTable("fuga_table");
        myDbUnitExtension.printTable("piyo_table");
    }
}
実行結果
hoge_table {
  ID=1, VALUE='HOGE'
}
fuga_table {
}
piyo_table {
  ID=1, HOGE_ID=1
}
  • FilteredDataSet を使用すると、既存のデータセットから指定したテーブルだけを抽出した新しいデータセットを作成できる
  • 抽出するテーブルは、 FilteredDataSet のコンストラクタの第一引数で String 配列を使って指定する
  • テーブルは、この String 配列で指定された順番で抽出される(元のデータセット上の順番は無視される)

順番は元のデータセットのままにして抽出する

test
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE dataset SYSTEM "dataset.dtd">
<dataset>
    <table name="hoge_table">
        <column>id</column>
        <column>value</column>
        <row>
            <value>1</value>
            <value>HOGE</value>
        </row>
    </table>
    <table name="fuga_table">
        <column>id</column>
        <column>value</column>
        <row>
            <value>1</value>
            <value>FUGA</value>
        </row>
    </table>
    <table name="piyo_table">
        <column>id</column>
        <column>hoge_id</column>
        <row>
            <value>1</value>
            <value>1</value>
        </row>
    </table>
</dataset>
    @Test
    void testIncludeFilter() throws Exception {
        XmlDataSet xmlDataSet = myDbUnitExtension
            .readXmlDataSet("/sandbox/dbunit/FilteredDataSetTest/testIncludeFilter.xml");
        IncludeTableFilter filter =
            new IncludeTableFilter(new String[]{"piyo_table", "hoge_table"});
        FilteredDataSet filteredDataSet = new FilteredDataSet(filter, xmlDataSet);
        myDbUnitExtension.getDatabaseTester().setDataSet(filteredDataSet);
        myDbUnitExtension.getDatabaseTester().onSetup();

        myDbUnitExtension.printTable("hoge_table");
        myDbUnitExtension.printTable("fuga_table");
        myDbUnitExtension.printTable("piyo_table");
    }
実行結果
hoge_table {
  ID=1, VALUE='HOGE'
}
fuga_table {
}
piyo_table {
  ID=1, HOGE_ID=1
}
  • FilteredDataSet のコンストラクタの第一引数には、テーブルのフィルタ方法を定義した ITableFilter を指定することもできる
  • IncludeTableFilter を使用すると、テーブルを読み込む順序は元のデータセットのままにして、テーブル名だけで対象を絞り込むことができる

指定したテーブルを除外する

testExcludeFilter.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE dataset SYSTEM "dataset.dtd">
<dataset>
    <table name="hoge_table">
        <column>id</column>
        <column>value</column>
        <row>
            <value>1</value>
            <value>HOGE</value>
        </row>
    </table>
    <table name="fuga_table">
        <column>id</column>
        <column>value</column>
        <row>
            <value>1</value>
            <value>FUGA</value>
        </row>
    </table>
    <table name="piyo_table">
        <column>id</column>
        <column>hoge_id</column>
        <row>
            <value>1</value>
            <value>1</value>
        </row>
    </table>
</dataset>
    @Test
    void testExcludeFilter() throws Exception {
        XmlDataSet xmlDataSet = myDbUnitExtension
            .readXmlDataSet("/sandbox/dbunit/FilteredDataSetTest/testExcludeFilter.xml");
        ExcludeTableFilter filter =
            new ExcludeTableFilter(new String[]{"piyo_table", "hoge_table"});
        FilteredDataSet filteredDataSet = new FilteredDataSet(filter, xmlDataSet);
        myDbUnitExtension.getDatabaseTester().setDataSet(filteredDataSet);
        myDbUnitExtension.getDatabaseTester().onSetup();

        myDbUnitExtension.printTable("hoge_table");
        myDbUnitExtension.printTable("fuga_table");
        myDbUnitExtension.printTable("piyo_table");
    }
実行結果
hoge_table {
}
fuga_table {
  ID=1, VALUE='FUGA'
}
piyo_table {
}
  • ExcludeTableFilter を使用すると、指定したテーブルを除外できる

パターン指定

IncludeTableFilterExcludeTableFilter は、テーブル名をパターンで指定することができる。

    @Test
    void testPattern() throws Exception {
        IncludeTableFilter filterWithAsterisk =
            new IncludeTableFilter(new String[]{"*d"});

        assertFalse(filterWithAsterisk.accept("first"));
        assertTrue(filterWithAsterisk.accept("second"));
        assertTrue(filterWithAsterisk.accept("third"));
        assertFalse(filterWithAsterisk.accept("forth"));
        assertFalse(filterWithAsterisk.accept("fifth"));

        IncludeTableFilter filterWithQuestion =
            new IncludeTableFilter(new String[]{"f????"});

        assertTrue(filterWithQuestion.accept("first"));
        assertFalse(filterWithQuestion.accept("second"));
        assertFalse(filterWithQuestion.accept("third"));
        assertTrue(filterWithQuestion.accept("forth"));
        assertTrue(filterWithQuestion.accept("fifth"));

        IncludeTableFilter filterWithPatterns =
            new IncludeTableFilter(new String[]{"s*", "*th"});

        assertFalse(filterWithPatterns.accept("first"));
        assertTrue(filterWithPatterns.accept("second"));
        assertFalse(filterWithPatterns.accept("third"));
        assertTrue(filterWithPatterns.accept("forth"));
        assertTrue(filterWithPatterns.accept("fifth"));
    }
  • * は、 0 文字以上の任意の文字列にマッチする
  • ? は、任意の1文字にマッチする

ValueComparer

build.gradle
dependencies {
    ..
    testRuntimeOnly "junit:junit:4.13.2"
}
  • IsActualWithinToleranceOfExpectedTimestampValueComparer というクラスがなぜか JUnit 4 以下の API に依存してしまっているので、使用するなら依存を追加しなければならない
setUp.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE dataset SYSTEM "dataset.dtd">
<dataset>
    <table name="test_table">
        <column>id</column>
        <column>text</column>
        <column>numeric</column>
        <column>timestamp_value</column>
        <row>
            <value>1</value>
            <value>hoge</value>
            <value>10</value>
            <value>2022-07-09 11:12:13.123</value>
        </row>
    </table>
</dataset>
expected.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE dataset SYSTEM "dataset.dtd">
<dataset>
    <table name="test_table">
        <column>id</column>
        <column>text</column>
        <column>numeric</column>
        <column>timestamp_value</column>
        <row>
            <value>1</value>
            <value>hoge</value>
            <value>9</value>
            <value>2022-07-09 11:12:13.000</value>
        </row>
    </table>
</dataset>
  • 投入データと期待値は完全には一致していない
package sandbox.dbunit;

import org.dbunit.assertion.comparer.value.ValueComparer;
import org.dbunit.assertion.comparer.value.ValueComparers;
import org.dbunit.dataset.IDataSet;
import org.dbunit.dataset.ITable;
import org.dbunit.dataset.xml.XmlDataSet;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import java.util.Map;

import static org.dbunit.Assertion.assertWithValueComparer;

public class ValueComparerTest {
    @RegisterExtension
    static MyDbUnitExtension myDbUnitExtension = new MyDbUnitExtension();

    @BeforeAll
    static void beforeAll() {
        // DB初期化(テーブル作成)
        myDbUnitExtension.sql("""
        create table test_table (
            id integer primary key,
            text varchar(32),
            numeric integer,
            timestamp_value timestamp
        )""");
    }

    @BeforeEach
    void setUp() throws Exception {
        XmlDataSet setUp = myDbUnitExtension
            .readXmlDataSet("/sandbox/dbunit/ValueComparerTest/setUp.xml");
        myDbUnitExtension.getDatabaseTester().setDataSet(setUp);
        myDbUnitExtension.getDatabaseTester().onSetup();
    }

    @Test
    void test() throws Exception {
        XmlDataSet expected = myDbUnitExtension
            .readXmlDataSet("/sandbox/dbunit/ValueComparerTest/expected.xml");
        ITable expectedTable = expected.getTable("test_table");

        IDataSet actual = myDbUnitExtension.getConnection().createDataSet();
        ITable actualTable = actual.getTable("test_table");

        Map<String, ValueComparer> comparers = Map.of(
            "numeric", ValueComparers.isActualGreaterThanExpected,
            "timestamp_value", ValueComparers.isActualWithinOneSecondNewerOfExpectedTimestamp
        );
        ValueComparer defaultComparer = ValueComparers.isActualEqualToExpected;

        // テストは成功する
        assertWithValueComparer(expectedTable, actualTable, defaultComparer, comparers);
    }
}
  • assertWithValueComparer(ITable, ITable, ValueComparer, Map<String, ValueComparer>) を使用すると、 ValueComparer を使った検証ができる
  • ValueComparer を使うと、カラムごとに完全一致以外の検証が可能になる
    • 「期待値より大きい(小さい)」
    • 「期待値の時刻との差が1秒以内」
    • etc...
  • assertWithValueComparer() の第三引数には、デフォルトで使用する ValueComparer を指定する
    • ここでは、 ValueComparers に定義されている isActualEqualToExpected を使用している
    • ValueComparers には、よく利用しそうな ValueComparer が定数としてあらかじめ定義されている
    • isActualEqualToExpected は、完全に一致するかどうかを比較する ValueComparer
    • 普通に assertEquals() したときも裏ではこれが使われている
  • assertWithValueComparer() の第四引数には、カラムごとに使用する ValueComparer を定義した Map を指定する
    • ここでは、 numeric カラムに対して isActualGreaterThanExpected を、
      timestamp_value カラムに対しては isActualWithinOneSecondNewerOfExpectedTimestamp を指定している
    • isActualGreaterThanExpected は、期待値よりも実際の値が大きいことを検証する
    • isActualWithinOneSecondNewerOfExpectedTimestamp は、実際の値の時刻が期待値の時刻より1秒以内の未来の時刻であることを検証する

ValueComparers で定義されいている定数

isActualEqualToExpected

期待値と実際の値が完全一致することを検証する。

実際の値 OKとなる期待値の例 NGとなる期待値の例
abc abc Abc

isActualNotEqualToExpected

期待値と実際の値が一致しないことを検証する。

実際の値 OKとなる期待値の例 NGとなる期待値の例
abc Abc abc

isActualContainingExpectedStringValueComparer

実際の値を文字列として扱ったときに、期待値の文字列表現を含んでいることを検証する。

実際の値 OKとなる期待値の例 NGとなる期待値の例
hoge og Og
101 01 11

isActualGreaterThanExpected

実際の値が期待値より大きいことを検証する。

実際の値 OKとなる期待値の例 NGとなる期待値の例
10 9 10

isActualGreaterThanOrEqualToExpected

実際の値が期待値以上であることを検証する。

実際の値 OKとなる期待値の例 NGとなる期待値の例
10 10 11

isActualLessThanExpected

実際の値が期待値より小さいことを検証する。

実際の値 OKとなる期待値の例 NGとなる期待値の例
10 11 10

isActualLessOrEqualToThanExpected

実際の値が期待値以下であることを検証する。

実際の値 OKとなる期待値の例 NGとなる期待値の例
10 10 9

isActualWithinOneSecondNewerOfExpectedTimestamp

実際の値の時刻が、期待値の時刻から1秒以内の未来時刻であることを検証する。
Timestamp 型のカラムにのみ指定可能。

実際の値 OKとなる期待値の例 NGとなる期待値の例
2022-01-01 00:00:00.123 2021-12-31 23:59:59.123 2021-12-31 23:59:59.122

isActualWithinOneMinuteNewerOfExpectedTimestamp

実際の値の時刻が、期待値の時刻から1分以内の未来時刻であることを検証する。
Timestamp 型のカラムにのみ指定可能。

実際の値 OKとなる期待値の例 NGとなる期待値の例
2022-01-01 00:00:00.123 2021-12-31 23:59:00.123 2021-12-31 23:59:00.122

isActualEqualToExpectedTimestampWithIgnoreMillis

isActualWithinOneSecondNewerOfExpectedTimestamp と同じ。

ValueComparer には isActualWithinOneMinuteOlderOfExpectedTimestamp, isActualWithinOneSecondOlderOfExpectedTimestamp という、1秒(分)以内の過去時刻であることを検証できそうな定数が用意されている。
しかし 2.7.3 現在、おそらくこれらの定数はバグっている。

    public static final ValueComparer isActualWithinOneSecondOlderOfExpectedTimestamp =
            new IsActualWithinToleranceOfExpectedTimestampValueComparer(ONE_SECOND_IN_MILLIS, 0);

ValueComparers では、上記のように定数が定義されている。

IsActualWithinToleranceOfExpectedTimestampValueComparer のコンストラクタ引数は、第一引数が許容できる時間の下限で、第二引数が上限を指定するようになっている。
上記の実装では、下限に 1000 (1秒のミリ秒表現)を、上限に 0 を設定していることになる。
下限のほうが大きい値になってしまっており、この結果内部では「0以下かつ1000以上であること」を検証してしまっていて、絶対に検証OKにならない状態になっている。

IsActualWithinToleranceOfExpectedTimestampValueComparer単体テストを見ても下限>上限となっているケースがないので、おそらくバグ。

ValueComparer を自作する

testCustomValueComparer.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE dataset SYSTEM "dataset.dtd">
<dataset>
    <table name="test_table">
        <column>id</column>
        <column>text</column>
        <column>numeric</column>
        <column>timestamp_value</column>
        <row>
            <value>1</value>
            <value>HOGE</value>
            <value>10</value>
            <value>2022-07-09 11:12:13.123</value>
        </row>
    </table>
</dataset>
    @Test
    void testCustomValueComparer() throws Exception {
        XmlDataSet expected = myDbUnitExtension
            .readXmlDataSet("/sandbox/dbunit/ValueComparerTest/testCustomValueComparer.xml");
        ITable expectedTable = expected.getTable("test_table");

        IDataSet actual = myDbUnitExtension.getConnection().createDataSet();
        ITable actualTable = actual.getTable("test_table");

        Map<String, ValueComparer> comparers = Map.of(
            "text", new ValueComparerTemplateBase() {
                @Override
                protected boolean isExpected(
                    ITable expectedTable,
                    ITable actualTable,
                    int rowNum,
                    String columnName,
                    DataType dataType,
                    Object expectedValue,
                    Object actualValue
                ) throws DatabaseUnitException {
                    if (expectedValue == null || actualValue == null) {
                        return expectedValue == actualValue;
                    }
                    return expectedValue.toString()
                        .equalsIgnoreCase(actualValue.toString());
                }

                @Override
                protected String getFailPhrase() {
                    return "not equals ignore case";
                }
            }
        );
        ValueComparer defaultComparer = ValueComparers.isActualEqualToExpected;

        // テストは成功する
        assertWithValueComparer(expectedTable, actualTable, defaultComparer, comparers);
    }
  • ValueComparer を実装すれば、任意の比較方法を定義できる
    • ここでは、大文字・小文字に関係なく文字列が一致していることを検証する ValueComparer を作成している
  • ValueComparerTemplateBase というベースとなるクラスが用意されているので、それを継承して作成すると楽
  • isExpected()getFailPhrase() の2つの抽象メソッドを実装する
  • isExpected() では、実際に検証を行って結果を boolean で返却する(OK なら true
  • getFailPharse() では、検証 NG となったときに使用するエラーメッセージの一部を返却する
    • Actual value='%s' is %s expected value='%s' というメッセージの、真ん中の %s のところに埋め込まれる

プロパティ

IDatabaseConnection connection = databaseTester.getConnection();
DatabaseConfig config = connection.getConfig();
config.setProperty(DatabaseConfig.FEATURE_BATCHED_STATEMENTS, true);
  • DbUnit の細かい振る舞いを調整するためのクラスとして、 DatabaseConfig というクラスが用意されている
  • DatabaseConfig は、 IDatabaseConnection ごとに割り当てられている
    • DatabaseConfig のインスタンスは、 IDatabaseConnection の内部で生成されている
      • 設定済みのインスタンスを外部から渡すわけではない
    • DatabaseConfig のインスタンスは、 IDatabaseConnectiongetConfig() メソッドで取得できる
    • getConfig() で取得した DatabaseConfigsetProperty(String, Object) メソッドを使うことで、そのコネクションにおける設定を調整できる
  • setProperty() で指定できるキーは、 DatabaseConfig に定数として定義されている
  • 設定には「Feature Flags」と「Properties」の2種類存在している
    • 「Feature Flags」は値が boolean で、「Properties」は値が任意のオブジェクトという差がある
    • DatabaseConfig には setFeature(String, boolean)setProperty(String, Object) の2つのメソッドが存在している
    • setProperty() でも「Feature Flags」の設定は可能なので、使うのは setProperty() だけでいい
      • setFeature() は非推奨となっている
      • setProperty() で「Feature Flags」を設定できるようになったのは 2.4.6 以降

onSetup(), onTearDown() で使用される IDatabaseConnection の調整

databaseTester.setOperationListener(new DefaultOperationListener() {
    @Override
    public void connectionRetrieved(IDatabaseConnection connection) {
        super.connectionRetrieved(connection);
        DatabaseConfig config = connection.getConfig();
        config.setProperty(DatabaseConfig.FEATURE_ALLOW_EMPTY_FIELDS, true);
    }
});

...

databaseTester.onSetup();
  • IDatabaseTesteronSetup() および onTearDown() で使用される IDatabaseConnection は、 IDatabaseTester の内部で生成されていて外部から単純にはアクセスできない
    • このため、 onSetup(), onTearDown() で使用される IDatabaseConnection の設定を調整するにはちょっと特殊な手段が必要になる
  • IDatabaseTester には setOperationListener(IOperationListener) というメソッドが用意されており、 DatabaseOperation の実行に対するリスナーを登録できるようになっている
  • IOperationListenerconnectionRetrieved(IDatabaseConnection) を実装すれば、 DatabaseOperation が実行される前に処理を挟むことができる
    • 引数に、 DatabaseOperation の実行で使用される IDatabaseConnection が渡される
    • この IDatabaseConnection から DatabaseConfig を取得すれば、 onSetup()onTearDown() の調整ができる

参考

  1. DbUnitを試してみる - CLOVER🍀

  2. http://dbunit.sourceforge.net/dbunit/howto.html#roworder

  3. どちらのケースも 1GB くらいメモリを使って終わってしまう

11
21
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
21