LoginSignup
8
6

More than 1 year has passed since last update.

Database Rider 使い方メモ

Last updated at Posted at 2022-12-29

Database Rider とは

  • DBUnit をもっと使いやすくするライブラリ
  • アノテーションで初期データや期待値のデータファイルを指定できる
  • YAMLやJSONなど、 DBUnit でサポートされていないデータセットが用意されている

Hello World

実装

フォルダ構成
|-build.gradle
`-src/test/
  |-java/
  | `-sandbox/dbrider/
  |   `-HelloDatabaseRiderTest.java
  `-resources/
    `-sandbox/dbrider/
      |-hello.yml
      `-hello-expected.yml
build.gradle
plugins {
    id "java"
}

sourceCompatibility = 17
targetCompatibility = 17

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

repositories {
    mavenCentral()
}

dependencies {
    testImplementation "com.github.database-rider:rider-junit5:1.35.0"
    testImplementation "org.junit.jupiter:junit-jupiter:5.9.1"
    testRuntimeOnly "org.hsqldb:hsqldb:2.7.1"
}

test {
    useJUnitPlatform()
}
HelloDatabaseRiderTest.java
package sandbox.dbrider;

import com.github.database.rider.core.api.connection.ConnectionHolder;
import com.github.database.rider.core.api.dataset.DataSet;
import com.github.database.rider.core.api.dataset.ExpectedDataSet;
import com.github.database.rider.junit5.DBUnitExtension;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;

@ExtendWith(DBUnitExtension.class)
public class HelloDatabaseRiderTest {
    private static final ConnectionHolder connectionHolder =
        () -> DriverManager.getConnection("jdbc:hsqldb:mem:test", "sa", "");

    private static Connection connection;

    @BeforeAll
    static void initDatabase() throws Exception {
        connection = connectionHolder.getConnection();
        executeUpdate("""
        create table test_table (
            id integer,
            value varchar(32)
        )
        """);
    }

    @Test
    @DataSet("sandbox/dbrider/hello.yml")
    @ExpectedDataSet("sandbox/dbrider/hello-expected.yml")
    void hello() throws Exception {
        executeUpdate("""
        update
            test_table
        set
            value = 'WORLD'
        where
            id = 2
        """);
    }

    @AfterAll
    static void closeConnection() throws Exception {
        connection.close();
    }

    private static void executeUpdate(String sql) throws SQLException {
        try (final PreparedStatement ps = connection.prepareStatement(sql);) {
            ps.executeUpdate();
        }
    }
}
hello.yml
test_table:
  - id: 1
    value: hello
  - id: 2
    value: world
hello-expected.yml
test_table:
  - id: 1
    value: hello
  - id: 2
    value: WORLD

説明

build.gradle
dependencies {
    testImplementation "com.github.database-rider:rider-junit5:1.35.0"
    testImplementation "org.junit.jupiter:junit-jupiter:5.9.1"
    testRuntimeOnly "org.hsqldb:hsqldb:2.7.1"
}
  • Database Rider を使い始める場合、通常は com.github.database-rider:rider-core を依存に追加する
    • しかし、 Database Rider はもともと JUnit4 の Rule として構築されているので、そのままでは JUnit5 で使用できない
    • JUnit5 で使用する場合は、 com.github.database-rider:rider-junit5 を依存に追加する
    • rider-core は推移的な依存関係の解決で引っ張られてこられるので、ここでは rider-junit5 だけ指定している
  • データベースとして、 HSQLDB を使用している
@ExtendWith(DBUnitExtension.class)
public class HelloDatabaseRiderTest {
  • JUnit5 で Database Rider を使う場合は、 JUnit5 の Extension として用意されている DBUnitExtension@ExtendWith に指定する
    • もしくは @DBRider を使うことでも同じ効果が得られる
    • @DBRider は、 @ExtendWith(DBUnitExtension.class)@Test の合成アノテーション
    private static final ConnectionHolder connectionHolder =
        () -> DriverManager.getConnection("jdbc:hsqldb:mem:test", "sa", "");
  • DBUnitExtension は、テストクラスにある ConnectionHolder 型のフィールドか、 ConnectionHolder を返すメソッドからデータベースコネクションを取得する
    • もしくは、後述する設定ファイルで指定した設定からコネクションを生成する
  • ここでは、 static フィールドとして HSQLDB の Connection を返す ConnectionHolder を定義している
    @Test
    @DataSet("sandbox/dbrider/hello.yml")
    @ExpectedDataSet("sandbox/dbrider/hello-expected.yml")
    void hello() throws Exception {
  • @DataSet アノテーションを使うことで、初期セットアップ用のデータファイルを指定できる
  • @ExpectedDataSet アノテーションを使うことで、期待値となるデータファイルを指定できる
test_table:
  - id: 1
    value: hello
  - id: 2
    value: world
  • Database Rider を使うと、 YAML でデータセットを定義できる
    • 本家の DBUnit では YAML のデータセットは存在しない
  • データ構造は、見てのとおり

検証サポート用の拡張機能

  • 以下、検証を楽にするためにDBアクセスの処理をサポートする自作の拡張機能を使用する
package sandbox.dbrider;

import org.junit.jupiter.api.extension.AfterAllCallback;
import org.junit.jupiter.api.extension.BeforeAllCallback;
import org.junit.jupiter.api.extension.ExtensionContext;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;

public class DatabaseSupport implements BeforeAllCallback, AfterAllCallback {
    private final String url;
    private Connection connection;

    public DatabaseSupport() {
        this("jdbc:hsqldb:mem:test");
    }

    public DatabaseSupport(String url) {
        this.url = url;
    }

    @Override
    public void beforeAll(ExtensionContext context) throws Exception {
        connection = DriverManager.getConnection(url, "sa", "");
    }

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

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

    public void printTable(String table) {
        try (
            final PreparedStatement ps =
                connection.prepareStatement("select * from " + table);
            final ResultSet rs = ps.executeQuery();
        ) {
            final ResultSetMetaData metaData = rs.getMetaData();

            while (rs.next()) {
                Map<String, Object> record = new HashMap<>();
                for (int i=1; i<=metaData.getColumnCount(); i++) {
                    final String columnName = metaData.getColumnName(i);
                    Object value = rs.getObject(i);
                    if (value instanceof String) {
                        value = "\"" + value + "\"";
                    }
                    record.put(columnName, value);
                }
                System.out.println(record);
            }
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

    public Connection getConnection() {
        return connection;
    }
}

DB接続定義

  • Hello Worldでは ConnectionHolder を使ってDB接続を定義した
  • これ以外にも、もうちょっと楽にDB接続を定義する方法が用意されている

アノテーションで定義する

package sandbox.dbrider;

import com.github.database.rider.core.api.configuration.DBUnit;
import com.github.database.rider.core.api.dataset.DataSet;
import com.github.database.rider.core.api.dataset.ExpectedDataSet;
import com.github.database.rider.junit5.api.DBRider;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

@DBRider
@DBUnit(url = "jdbc:hsqldb:mem:test", user = "sa", password = "") // ★
public class DefineConnectionByDBUnitAnnotationTest {
    @RegisterExtension
    static DatabaseSupport db = new DatabaseSupport();

    @BeforeAll
    static void beforeAll() {
        db.executeUpdate("""
        create table test_table (
            id integer,
            value varchar(32)
        )
        """);
    }

    @Test
    @DataSet("sandbox/dbrider/hello.yml")
    @ExpectedDataSet("sandbox/dbrider/hello-expected.yml")
    void test() {
        db.executeUpdate("update test_table set value = 'WORLD' where id = 2");
    }
}
  • @DBUnit アノテーションを使うことでDB接続を定義できる
    • 一応 @DBUnit はメソッドにも設定できる

設定ファイルで定義する

フォルダ構成
`-src/test/
  |-java/
  | `-sandbox/dbrider/
  |   `-DefineConnectionByDbUnitYamlTest.java
  `-resources/
    `-dbunit.yml
dbunit.yml
connectionConfig:
  url: "jdbc:hsqldb:mem:test"
  user: "sa"
  password: ""
DefineConnectionByDbUnitYamlTest.java
package sandbox.dbrider;

import com.github.database.rider.core.api.dataset.DataSet;
import com.github.database.rider.core.api.dataset.ExpectedDataSet;
import com.github.database.rider.junit5.api.DBRider;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

@DBRider
public class DefineConnectionByDbUnitYamlTest {
    @RegisterExtension
    static DatabaseSupport db = new DatabaseSupport();

    @BeforeAll
    static void beforeAll() {
        db.executeUpdate("""
        create table test_table (
            id integer,
            value varchar(32)
        )
        """);
    }

    @Test
    @DataSet("sandbox/dbrider/hello.yml")
    @ExpectedDataSet("sandbox/dbrider/hello-expected.yml")
    void test() {
        db.executeUpdate("update test_table set value = 'WORLD' where id = 2");
    }
}
  • クラスパス直下に dbunit.yml というファイルを配置し、そこでDB接続情報を定義できる
  • この設定は全テスト共通の設定となる
  • dbunit.yml@DBUnit アノテーションが同時に指定された場合は、 @DBUnit の設定が優先される
  • 以降の実装例は、特に断りを入れない限りこの方法でDB接続を定義している前提で書いている

設定ファイルで指定できる設定について

  • dbunit.yml で指定できる設定については GitHub トップの READMEドキュメント とかに書かれているが、内容や説明に不足がある
  • 一番確実なのは、 @DBUnit アノテーションの Javadoc を見ることだと思う
    • Javadoc 自体は公開されているものが見当たらないので、 ソース を直接見に行く必要があるっぽい
  • dbunit.yml で設定できる設定値は、大きく次の3つに分かれている
    1. Database Rider の設定
    2. DBUnit の設定
    3. データベースコネクションの設定
dbunit.yml
cacheConnection: true
...
alwaysCleanAfter: false
properties:
  batchedStatements: false
  ...
  tableType: ["TABLE"]
connectionConfig:
  driver: ""
  url: ""
  user: ""
  password: ""
  • 上の方のトップレベルに並んでいる設定(cacheConnection とか)が Database Rider の設定
  • properties 以下が DBUnit の設定
  • connectionConfig がデータベースコネクションの設定

比較方法

色々なパターンを試して、期待値(@ExpectedDataSet)に設定したデータがどのように比較されるのかを確認する。

以下のようなテストクラスで検証する。

package sandbox.dbrider;

import com.github.database.rider.core.api.dataset.DataSet;
import com.github.database.rider.core.api.dataset.ExpectedDataSet;
import com.github.database.rider.junit5.api.DBRider;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

@DBRider
public class CompareTest {
    @RegisterExtension
    static DatabaseSupport db = new DatabaseSupport();

    @BeforeAll
    static void beforeAll() {
        db.executeUpdate("create table foo_table (id integer, value varchar(32))");
        db.executeUpdate("create table bar_table (id integer, value varchar(32))");
    }

    @Test
    @DataSet("sandbox/dbrider/CompareTest/期待値の方がテーブル数が少ない/data-set.yml")
    @ExpectedDataSet("sandbox/dbrider/CompareTest/期待値の方がテーブル数が少ない/expected.yml")
    void 期待値の方がテーブル数が少ない() {}

    @Test
    @DataSet("sandbox/dbrider/CompareTest/期待値の方がテーブル数が多い/data-set.yml")
    @ExpectedDataSet("sandbox/dbrider/CompareTest/期待値の方がテーブル数が多い/expected.yml")
    void 期待値の方がテーブル数が多い() {}

    @Test
    @DataSet("sandbox/dbrider/CompareTest/期待値の方がレコード数が少ない/data-set.yml")
    @ExpectedDataSet("sandbox/dbrider/CompareTest/期待値の方がレコード数が少ない/expected.yml")
    void 期待値の方がレコード数が少ない() {}

    @Test
    @DataSet("sandbox/dbrider/CompareTest/期待値の方がレコード数が多い/data-set.yml")
    @ExpectedDataSet("sandbox/dbrider/CompareTest/期待値の方がレコード数が多い/expected.yml")
    void 期待値の方がレコード数が多い() {}

    @Test
    @DataSet("sandbox/dbrider/CompareTest/期待値の方がカラム数が少ない/data-set.yml")
    @ExpectedDataSet("sandbox/dbrider/CompareTest/期待値の方がカラム数が少ない/expected.yml")
    void 期待値の方がカラム数が少ない() {}

    @Test
    @DataSet("sandbox/dbrider/CompareTest/期待値の方がカラム数が多い/data-set.yml")
    @ExpectedDataSet("sandbox/dbrider/CompareTest/期待値の方がカラム数が多い/expected.yml")
    void 期待値の方がカラム数が多い() {}

    @Test
    @DataSet("sandbox/dbrider/CompareTest/レコードの順序が異なる/data-set.yml")
    @ExpectedDataSet("sandbox/dbrider/CompareTest/レコードの順序が異なる/expected.yml")
    void レコードの順序が異なる() {}
}

期待値の方がテーブル数が少ない

data-set.yml
foo_table:
  - id: 1
    value: FOO
bar_table:
  - id: 1
    value: BAR
expected.yml
foo_table:
  - id: 1
    value: FOO

結果

成功する。

期待値の方がテーブル数が多い

data-set.yml
foo_table:
  - id: 1
    value: FOO
bar_table:
  - id: 1
    value: BAR
expected.yml
foo_table:
  - id: 1
    value: FOO
bar_table:
  - id: 1
    value: BAR
unknown_table:
  - id: 1
    value: unknown

結果

失敗する。

DataSet comparison failed due to following exception: 
java.lang.RuntimeException: DataSet comparison failed due to following exception: 
	at com.github.database.rider.core.dataset.DataSetExecutorImpl.compareCurrentDataSetWith(DataSetExecutorImpl.java:833)
	at com.github.database.rider.core.RiderRunner.performDataSetComparison(RiderRunner.java:144)
	at com.github.database.rider.core.RiderRunner.runAfterTest(RiderRunner.java:63)
	at com.github.database.rider.junit5.DBUnitExtension.afterTestExecution(DBUnitExtension.java:79)
	(中略)
Caused by: org.dbunit.dataset.NoSuchTableException: UNKNOWN_TABLE
	at app//org.dbunit.database.DatabaseDataSet.getTableMetaData(DatabaseDataSet.java:305)
	at app//org.dbunit.database.DatabaseDataSet.getTable(DatabaseDataSet.java:333)
	at app//com.github.database.rider.core.dataset.DataSetExecutorImpl.compareCurrentDataSetWith(DataSetExecutorImpl.java:831)
	... 72 more

期待値の方がレコード数が少ない

data-set.yml
foo_table:
  - id: 1
    value: FOO
  - id: 2
    value: BAR
expected.yml
foo_table:
  - id: 1
    value: FOO

結果

失敗する。

row count (table=FOO_TABLE) expected:<1> but was:<2>
Expected :1
Actual   :2

期待値の方がレコード数が多い

data-set.yml
foo_table:
  - id: 1
    value: FOO
expected.yml
foo_table:
  - id: 1
    value: FOO
  - id: 2
    value: BAR

結果

失敗する。

row count (table=FOO_TABLE) expected:<2> but was:<1>
Expected :2
Actual   :1

期待値の方がカラム数が少ない

data-set.yml
foo_table:
  - id: 1
    value: FOO
expected.yml
foo_table:
  - id: 1

結果

成功する。

期待値の方がカラム数が多い

data-set.yml
foo_table:
  - id: 1
    value: FOO
expected.yml
foo_table:
  - id: 1
    value: FOO
    unknown_column: unknown

結果

失敗する。

column count (table=FOO_TABLE, expectedColCount=3, actualColCount=2) expected:<[ID, UNKNOWN_COLUMN, VALUE]> but was:<[ID, VALUE]>
Expected :[ID, UNKNOWN_COLUMN, VALUE]
Actual   :[ID, VALUE]

レコードの順序が異なる

data-set.yml
foo_table:
  - id: 1
    value: ONE
  - id: 2
    value: TWO
  - id: 3
    value: THREE
expected.yml
foo_table:
  - id: 3
    value: THREE
  - id: 1
    value: ONE
  - id: 2
    value: TWO

結果

失敗する。

value (table=FOO_TABLE, row=0, col=ID) expected:<3> but was:<1>
Expected :3
Actual   :1

まとめ

パターン 結果
期待値の方がテーブル数が少ない 成功
期待値の方がテーブル数が多い 失敗
期待値の方がレコード数が少ない 失敗
期待値の方がレコード数が多い 失敗
期待値の方がカラム数が少ない 成功
期待値の方がカラム数が多い 失敗
レコードの順序が異なる 失敗
  • @ExpectedDataSet に記載しているテーブルのみが比較対象となる
  • @ExpectedDataSet に記載しているカラムのみが比較対象となる
  • 存在しないテーブルやカラムを @ExpectedDataSet に記載した場合はエラー(当たり前)
  • レコード数が等しいことも比較される
  • レコードの順序が等しいことも比較される

比較時のソート条件を指定する

package sandbox.dbrider;

import com.github.database.rider.core.api.dataset.DataSet;
import com.github.database.rider.core.api.dataset.ExpectedDataSet;
import com.github.database.rider.junit5.api.DBRider;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

@DBRider
public class OrderByTest {
    @RegisterExtension
    static DatabaseSupport db = new DatabaseSupport();

    @BeforeAll
    static void beforeAll() {
        db.executeUpdate("create table foo_table (id integer, value varchar(32))");
    }

    @Test
    @DataSet("sandbox/dbrider/OrderByTest/data-set.yml")
    @ExpectedDataSet(
        value = "sandbox/dbrider/OrderByTest/expected.yml",
        orderBy = "id" // ★
    )
    void test() {}
}
  • @ExpectedDataSetorderByid を指定している
data-set.yml
foo_table:
  - id: 1
    value: ONE
  - id: 2
    value: TWO
  - id: 3
    value: THREE
expected.yml
foo_table:
  - id: 3
    value: THREE
  - id: 1
    value: ONE
  - id: 2
    value: TWO
  • data-set.ymlexpected.yml は、データの順序が異なるように定義している
  • このテストは成功する
  • @ExpectedDataSetorderBy でデータを比較するときのソート条件(カラム名)が指定できる
    • 実際のデータと期待値の両方がこの条件でソートされて比較される
  • orderBy には配列で複数のカラム名を指定できる

ExpectedDataSet で指定したデータが含まれることを検証する

package sandbox.dbrider;

import com.github.database.rider.core.api.dataset.CompareOperation;
import com.github.database.rider.core.api.dataset.DataSet;
import com.github.database.rider.core.api.dataset.ExpectedDataSet;
import com.github.database.rider.junit5.api.DBRider;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

@DBRider
public class CompareContainsTest {
    @RegisterExtension
    static DatabaseSupport db = new DatabaseSupport();

    @BeforeAll
    static void beforeAll() {
        db.executeUpdate("create table test_table (id integer, value varchar(32))");
    }

    @Test
    @DataSet("sandbox/dbrider/CompareContainsTest/data-set.yml")
    @ExpectedDataSet(
        value = "sandbox/dbrider/CompareContainsTest/expected.yml",
        compareOperation = CompareOperation.CONTAINS // ★
    )
    void test() {}
}
data-set.yml
test_table:
  - id: 1
    value: one
  - id: 2
    value: two
  - id: 3
    value: three
expected.yml
test_table:
  - id: 3
    value: three
  - id: 2
    value: two

このテストは成功する

    @ExpectedDataSet(
        value = "sandbox/dbrider/CompareContainsTest/expected.yml",
        compareOperation = CompareOperation.CONTAINS
    )
  • @ExpectedDataSetcompareOperationCompareOperation.CONTAINS を指定すると、データセットに記載したデータが実際のテーブルに含まれるかどうかで検証が行われるようになる
    • デフォルトは CompareOperation.EQUALS で、上で試したように順序や件数まで含めた完全一致することが検証される
  • レコードが含まれるかどうかだけなので、順序はチェックされない

VIEW や SYNONYM を検証する

package sandbox.dbrider;

import com.github.database.rider.core.api.dataset.DataSet;
import com.github.database.rider.core.api.dataset.ExpectedDataSet;
import com.github.database.rider.junit5.api.DBRider;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

@DBRider
public class TableTypeTest {
    @RegisterExtension
    static DatabaseSupport db = new DatabaseSupport();

    @BeforeAll
    static void beforeAll() {
        db.executeUpdate("create table foo_table (id integer, value varchar(32))");
        db.executeUpdate("create view foo_view as select * from foo_table");
    }

    @Test
    @DataSet("sandbox/dbrider/TableTypeTest/data-set.yml")
    @ExpectedDataSet("sandbox/dbrider/TableTypeTest/expected.yml")
    void test() {}
}
  • foo_table テーブルを丸コピーした foo_view ビューを作成している
data-set.yml
foo_table:
  - id: 1
    value: foo
  - id: 2
    value: bar
expected.yml
foo_view:
  - id: 1
    value: foo
  - id: 2
    value: bar
  • 期待値は foo_view ビューの内容を検証している
実行結果
DataSet comparison failed due to following exception: 
java.lang.RuntimeException: DataSet comparison failed due to following exception: 
	at com.github.database.rider.core.dataset.DataSetExecutorImpl.compareCurrentDataSetWith(DataSetExecutorImpl.java:833)
    ...
Caused by: org.dbunit.dataset.NoSuchTableException: FOO_VIEW
	at app//org.dbunit.database.DatabaseDataSet.getTableMetaData(DatabaseDataSet.java:305)
	...
  • デフォルトでは、読み込みの対象となるものは実際のテーブルに限られている
  • このため、 VIEW などを検査対象にしようとするとテーブルが見つからないというエラーになる
  • VIEW などの実テーブル以外も対象にしたい場合は、以下のように設定する
dbunit.yml
properties:
  tableType:
    - TABLE
    - VIEW
  • 設定ファイル (dbunit.yml) で properties.tableType に読み込み対象として VIEW を追加する
    • デフォルトは TABLE のみとなっている
  • properties.tableType には DatabaseMetaData.getTableTypes() で取得できる値のいずれかを指定できる
  • @DBUnit アノテーションの tableType でも指定できる
  • 上記設定を入れたうえでテストを実行すると、テストは成功する

バッチ更新

dbunit.yml
properties:
  batchedStatements: true
  batchSize: 100
  • 設定ファイル(dbunit.yml)で、properties.batchedStatementstrue を指定することで、JDBCのバッチ更新が有効になる
    • デフォルトは false で無効になっている
    • 理由は こちら を参照
  • properties.batchSize で、更新間隔を指定できる
    • デフォルトは 100
  • @DBUnit アノテーションでも指定可能

クリーンアップ

基本

package sandbox.dbrider;

import com.github.database.rider.core.api.dataset.DataSet;
import com.github.database.rider.core.api.dataset.ExpectedDataSet;
import com.github.database.rider.junit5.api.DBRider;
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;

@DBRider
public class CleanUpTest {
    @RegisterExtension
    static DatabaseSupport db = new DatabaseSupport();

    @BeforeAll
    static void beforeAll() {
        db.executeUpdate("create table foo_table (id integer, value varchar(32))");
        db.executeUpdate("create table bar_table (id integer, value varchar(32))");
    }

    @BeforeEach
    void setUp() {
        db.executeUpdate("insert into foo_table values (1, 'hello')");
        db.executeUpdate("insert into foo_table values (2, 'world')");
        db.executeUpdate("insert into bar_table values (1, 'bar')");
    }

    @Test
    @DataSet("sandbox/dbrider/CleanUpTest/data-set.yml")
    @ExpectedDataSet("sandbox/dbrider/CleanUpTest/expected.yml")
    void test() {}
}
  • foo_tablebar_table の2つを作成し、 foo_table にはレコードを2件、bar_table にはレコードを1件登録しておく
data-set.yml
foo_table:
  - id: 3
    value: FOO
  • @DataSet で、 foo_table のみレコードを1件を登録するような記述にしておく
expected.yml
foo_table:
  - id: 3
    value: FOO
bar_table:
  - id: 1
    value: bar
  • foo_table@DataSet で指定した状態に、 bar_table は変化なしを期待する形で @ExpectedDataSet を設定する
  • このテストは成功する

説明

  • @DataSet で指定したデータファイルに存在するテーブルは、 DELETE-INSERT される
  • @DataSet に含まれないテーブルは、変更されない

外部参照制約が設定されたテーブルが存在する場合

package sandbox.dbrider;

import com.github.database.rider.core.api.dataset.DataSet;
import com.github.database.rider.core.api.dataset.ExpectedDataSet;
import com.github.database.rider.junit5.api.DBRider;
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;

@DBRider
public class CleanUpForeignKeyTest {
    @RegisterExtension
    static DatabaseSupport db = new DatabaseSupport();

    @BeforeAll
    static void beforeAll() {
        db.executeUpdate("""
        create table parent_table (
            id integer primary key,
            value varchar(32)
        )""");
        db.executeUpdate("""
        create table child_table (
            parent_id integer primary key,
            value varchar(32),
            foreign key (parent_id) references parent_table (id)
        )""");
        db.executeUpdate("""
        create table grand_child (
            child_id integer primary key,
            value varchar(32),
            foreign key (child_id) references child_table (parent_id)
        )""");
    }

    @BeforeEach
    void setUp() {
        db.executeUpdate("insert into parent_table values (1, 'parent')");
        db.executeUpdate("insert into child_table values (1, 'child')");
        db.executeUpdate("insert into grand_child_table values (1, 'grand_child')");
    }

    @Test
    @DataSet(value = "sandbox/dbrider/CleanUpForeignKeyTest/data-set.yml")
    @ExpectedDataSet("sandbox/dbrider/CleanUpForeignKeyTest/expected.yml")
    void test() {}
}
  • 外部参照制約を設定したテーブルを用意し、それぞれのテーブルにレコードを登録しておく
data-set.yml
child_table:
  - id: 9
    value: fuga
parent_table:
  - id: 9
    value: hoge
grand_child_table:
  • 親・子テーブルだけ値を設定、孫テーブルは名前は記載するがデータは空
  • テーブル名の記述の順序は適当にしておく
expected.yml
parent_table:
  - id: 9
    value: hoge
child_table:
  - id: 9
    value: fuga
grand_child_table:
  • data-set.yml と同じ内容(テーブル名の順序だけ整理)
  • このテストは成功する

説明

  • @DataSet で指定したテーブルに外部参照制約が設定されている場合、 Database Rdier はテーブルの依存関係を調べて適切な順序で DELETE-INSERT をしてくれるようになっている
  • つまり、依存するテーブルを列挙しておけば、記述の順番は気にしなくていい
    • 子テーブルを記載しないと DELETE-INSERT の対象にならないので、親テーブルを DELETE しようとしたときに外部参照制約エラーになる
    • したがって、依存するテーブルはすべて記載しておかなければならない

DELETE-INSERT 以外の処理にする

package sandbox.dbrider;

import com.github.database.rider.core.api.dataset.DataSet;
import com.github.database.rider.core.api.dataset.ExpectedDataSet;
import com.github.database.rider.core.api.dataset.SeedStrategy;
import com.github.database.rider.junit5.api.DBRider;
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;

@DBRider
public class StrategyTest {
    @RegisterExtension
    static DatabaseSupport db = new DatabaseSupport();

    @BeforeAll
    static void beforeAll() {
        db.executeUpdate("create table foo_table (id integer, value varchar(32))");
    }

    @BeforeEach
    void setUp() {
        db.executeUpdate("insert into foo_table values (9, 'default value')");
    }

    @Test
    @DataSet(
        value = "sandbox/dbrider/StrategyTest/data-set.yml",
        strategy = SeedStrategy.INSERT // ★
    )
    @ExpectedDataSet("sandbox/dbrider/StrategyTest/expected.yml")
    void test() {}
}
  • @BeforeEach で初期データを登録している
  • @DataSetstrategySeedStrategy.INSERT を設定している
data-set.yml
foo_table:
  - id: 1
    value: foo
expected.yml
foo_table:
  - id: 9
    value: default value
  - id: 1
    value: foo
  • 期待値は、初期データと data-set.yml で指定したデータの両方が存在することを期待するように定義している
  • このテストは成功する
  • @DataSetstrategy を変更することで、デフォルトの DELETE-INSERT の動きを変更できる
    • INSERT の場合は、 DELETE が行われず INSERT だけが行われる
    • 何が指定できるかについては、 SeedStrategy に定義されている定数と このあたり を参考のこと

DataSetで指定していないテーブルもすべてをクリーンする

package sandbox.dbrider;

import com.github.database.rider.core.api.dataset.DataSet;
import com.github.database.rider.core.api.dataset.ExpectedDataSet;
import com.github.database.rider.junit5.api.DBRider;
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;

@DBRider
public class CleanUpAllTest {
    @RegisterExtension
    static DatabaseSupport db = new DatabaseSupport();

    @BeforeAll
    static void beforeAll() {
        db.executeUpdate("create table foo_table (id integer, value varchar(32))");
        db.executeUpdate("create table bar_table (id integer, value varchar(32))");
    }

    @BeforeEach
    void setUp() {
        db.executeUpdate("insert into foo_table values (1, 'foo')");
        db.executeUpdate("insert into bar_table values (1, 'bar')");
    }

    @Test
    @DataSet(
        value = "sandbox/dbrider/CleanUpAllTest/data-set.yml",
        cleanBefore = true // ★
    )
    @ExpectedDataSet("sandbox/dbrider/CleanUpAllTest/expected.yml")
    void test() {}
}
  • テーブルを2つ用意し、それぞれレコードを登録しておく
  • @DataSet アノテーションの cleanBeforetrue を設定する
data-set.yml
foo_table:
  - id: 9
    value: FOO
  • foo_table のみ、 @DataSet でデータを登録するように設定する
expected.yml
foo_table:
  - id: 9
    value: FOO
bar_table:
  • foo_tabledata-set.yml で設定した状態と同じになっていることを、 bar_table は空になっていることを期待値として設定している
  • このテストは成功する

説明

    @DataSet(
        value = "sandbox/dbrider/CleanUpAllTest/data-set.yml",
        cleanBefore = true // ★
    )
  • @DataSet アノテーションの cleanBeforetrue を設定すると、データベースに存在するすべてのテーブルがクリアされてからテストが実行される
    • デフォルトは false
    • value を指定しなくても cleanBefore だけ指定することも可能(@DataSet(cleanBefore=true))なので、とりあえず全テーブルクリアしてテストを実行、といったことができる
  • テスト終了後にクリアする場合は cleanAftertrue を設定する
  • クリアを行う直前にデータベースに存在する外部参照制約が無効化されるため、外部参照制約のあるテーブルもすべてクリアできるようになっている
  • クリーンが終わったら外部参照制約は有効化される
  • @DataSet ごとではなく、すべてのテストでデータベースのクリーンを行いたい場合は、 dbunit.yml ファイルで alwaysCleanBeforetrue を設定する
    • alwaysCleanBeforecleanBefore が同時に設定された場合は、どちらか片方が true だったらクリーンが行われる
    • 実装箇所
dbunit.yml
alwaysCleanBefore: true

特定のテーブルはクリーンアップの対象外にする

package sandbox.dbrider;

import com.github.database.rider.core.api.dataset.DataSet;
import com.github.database.rider.core.api.dataset.ExpectedDataSet;
import com.github.database.rider.junit5.api.DBRider;
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;

@DBRider
public class CleanUpAllSkipTest {
    @RegisterExtension
    static DatabaseSupport db = new DatabaseSupport();

    @BeforeAll
    static void beforeAll() {
        db.executeUpdate("create table hoge_table (id integer, value varchar(32))");
        db.executeUpdate("create table fuga_table (id integer, value varchar(32))");
        db.executeUpdate("create table piyo_table (id integer, value varchar(32))");
    }

    @BeforeEach
    void setUp() {
        db.executeUpdate("insert into hoge_table values (1, 'hoge')");
        db.executeUpdate("insert into fuga_table values (1, 'fuga')");
        db.executeUpdate("insert into piyo_table values (1, 'piyo')");
    }

    @Test
    @DataSet(
        value = "sandbox/dbrider/CleanUpAllSkipTest/data-set.yml",
        cleanBefore = true,
        skipCleaningFor = "PIYO_TABLE" // ★
    )
    @ExpectedDataSet("sandbox/dbrider/CleanUpAllSkipTest/expected.yml")
    void test() {}
}
  • skipCleaningFor に、クリーンアップの除外対象として piyo_table の名前を設定している
    • 配列で複数指定可能
    • 内部的にデータベースからテーブル名の一覧を取得しており、そのときのテーブル名が大文字で取得されているため、大文字で指定しないとうまく適用されなかった(多分データベース製品に依存する)
data-set.yml
hoge_table:
  - id: 9
    value: HOGE
  • hoge_table だけ設定している
expected.yml
hoge_table:
  - id: 9
    value: HOGE
fuga_table:
piyo_table:
  - id: 1
    value: piyo
  • fuga_table は空を期待するが、 pyio_table はクリアされていないことを期待している
  • このテストは成功する

説明

    @DataSet(
        value = "sandbox/dbrider/CleanUpAllSkipTest/data-set.yml",
        cleanBefore = true,
        skipCleaningFor = "PIYO_TABLE" // ★
    )
  • skipCleaningFor にテーブル名を指定することで、 cleanBefore, cleanAfter の対象から除外できる

複数のデータセットを指定する

package sandbox.dbrider;

import com.github.database.rider.core.api.dataset.DataSet;
import com.github.database.rider.core.api.dataset.ExpectedDataSet;
import com.github.database.rider.junit5.api.DBRider;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

@DBRider
public class MultipleDataSetsTest {
    @RegisterExtension
    static DatabaseSupport db = new DatabaseSupport();

    @BeforeAll
    static void beforeAll() {
        db.executeUpdate("create table hoge_table (id integer, value varchar(32))");
        db.executeUpdate("create table fuga_table (id integer, value varchar(32))");
        db.executeUpdate("create table piyo_table (id integer, value varchar(32))");

        db.executeUpdate("insert into hoge_table values (9, 'default value')");
        db.executeUpdate("insert into fuga_table values (9, 'default value')");
        db.executeUpdate("insert into piyo_table values (9, 'default value')");
    }

    @Test
    @DataSet({
        "sandbox/dbrider/MultipleDataSetsTest/data-set1.yml",
        "sandbox/dbrider/MultipleDataSetsTest/data-set2.yml"
    })
    @ExpectedDataSet("sandbox/dbrider/MultipleDataSetsTest/expected.yml")
    void test() {}
}
  • @DataSetvalue に複数のデータセットファイルを指定している
  • また、各テーブルには1レコードずつデフォルトデータを登録している
data-set1.yml
hoge_table:
  - id: 1
    value: from data-set1.yml
data-set2.yml
hoge_table:
  - id: 2
    value: from data-set2.yml
fuga_table:
  - id: 1
    value: from data-set2.yml
  • hoge_table は、 data-set1.ymldata-set2.yml の両方に定義し、 fuga_tabledata-set2.yml でだけ定義している
  • piyo_table はいずれのデータセットファイルにも記載していない
expected.yml
hoge_table:
  - id: 1
    value: from data-set1.yml
  - id: 2
    value: from data-set2.yml
fuga_table:
  - id: 1
    value: from data-set2.yml
piyo_table:
  - id: 9
    value: default value
  • hoge_table は、 data-set1.ymldata-set2.yml の両方で定義したレコードが登録されていることを期待している
  • fuga_table は、 data-set2.yml で定義したレコードが登録されていることを期待している
  • piyo_table は、デフォルトで登録したレコードだけが存在していることを期待している
  • このテストは成功する
  • @DataSetvalue で複数のデータセットを指定した場合、それらのデータセットに記載されたレコードが全て登録されるようになる
    • 単純に追加 INSERT されるだけなので、主キーの重複などがあった場合は実行時にエラーになる

複数の期待値を指定する

package sandbox.dbrider;

import com.github.database.rider.core.api.dataset.DataSet;
import com.github.database.rider.core.api.dataset.ExpectedDataSet;
import com.github.database.rider.junit5.api.DBRider;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

@DBRider
public class MultipleExpectedDataSetsTest {
    @RegisterExtension
    static DatabaseSupport db = new DatabaseSupport();

    @BeforeAll
    static void beforeAll() {
        db.executeUpdate("create table foo_table (id integer, value varchar(32))");
        db.executeUpdate("create table bar_table (id integer, value varchar(32))");
    }

    @Test
    @DataSet("sandbox/dbrider/MultipleExpectedDataSetsTest/data-set.yml")
    @ExpectedDataSet({
        "sandbox/dbrider/MultipleExpectedDataSetsTest/expected1.yml",
        "sandbox/dbrider/MultipleExpectedDataSetsTest/expected2.yml"
    })
    void test() {}
}
  • @ExpectedDataSetvalue に複数のデータセットファイルを指定している
data-set.yml
foo_table:
  - id: 1
    value: foo
  - id: 2
    value: FOO
bar_table:
  - id: 1
    value: bar
expected1.yml
foo_table:
  - id: 1
    value: foo
expected2.yml
foo_table:
  - id: 2
    value: FOO
bar_table:
  - id: 1
    value: bar
  • このテストは成功する
  • @ExpectedDataSetvalue に複数のデータセットファイルを指定した場合、それらを複合した状態で検証が行われる

データ形式

YAML

    @Test
    @DataSet("sandbox/dbrider/DataSetTypeTest/testYaml/data-set.yml")
    @ExpectedDataSet("sandbox/dbrider/DataSetTypeTest/testYaml/expected.yaml")
    void testYaml() {}
data-set.yml
foo_table:
  - &template
    id: 1
    value: foo
bar_table:
  - value: bar
    <<: *template
expected.yaml
foo_table:
  - id: 1
    value: foo
bar_table:
  - id: 1
    value: bar
  • YAML でデータセットのファイルを定義できる
    • 本家 DBUnit には存在しない形式で、 Database Rider 独自の実装となっている(YamlDataSet)
  • トップレベルはMap構造にして、キーにテーブル名、値にレコードの配列を記載する
    • 各レコードはMap構造で記載し、キーにカラム名、値にカラムの値を記載する
  • 内部的にはSnakeYAMLが使われているので、SnakeYAMLがサポートしている記法なら書くことができる
  • 拡張子を yml または yaml にすると、 YAML として処理される

JSON

    @Test
    @DataSet("sandbox/dbrider/DataSetTypeTest/testJson/data-set.json")
    @ExpectedDataSet("sandbox/dbrider/DataSetTypeTest/testJson/expected.json")
    void testJson() {}
data-set.json
{
  "foo_table": [
    {
      "id": 1,
      "value": "foo"
    }
  ],
  "bar_table": [
    {
      "id": 1,
      "value": "bar"
    }
  ]
}
  • JSON でデータセットを定義できる
    • 本家 DBUnit には存在しない形式で、 Database Rider 独自の実装となっている(JSONDataSet)
  • トップレベルはオブジェクトにして、キーにテーブル名、値にレコードの配列を記載する
    • 各レコードはオブジェクトで記載し、キーにカラム名、値にカラムの値を記載する
  • 拡張子を json にすると、 JSON として処理される

XML

    @Test
    @DataSet("sandbox/dbrider/DataSetTypeTest/testXml/data-set.xml")
    @ExpectedDataSet("sandbox/dbrider/DataSetTypeTest/testXml/expected.xml")
    void testXml() {}
<?xml version="1.0" encoding="UTF-8" ?>
<dataset>
    <foo_table id="1" value="foo" />
    <bar_table id="1" value="bar" />
</dataset>
  • XML でデータセットを定義できる
    • これは、本家DBUnitの FlatXmlDataSet が使用されている
    • FlatXmlDataSet の使い方については こちら を参照

XLS, XLSX

    @Test
    @DataSet("sandbox/dbrider/DataSetTypeTest/testXls/data-set.xls")
    @ExpectedDataSet("sandbox/dbrider/DataSetTypeTest/testXls/expected.xls")
    void testXls() {}

image.png

image.png

  • Excel でデータセットを定義できる
    • これは、本家DBUnitの XlsDataSet が使用されている
    • XlsDataSet の使い方については こちら を参照
  • ただし、使用できるのは Excel 97-2003 形式(*.xls)のみ
    • 実装で拡張子が xls のものしかハンドリングしていないので、 xlsx は読み込めない
    • 「Excel」とか「xlsx」で issue を検索しても何も引っかからないので、多分対応予定はなさそう
    • (DBUnit 自体は xlsx も読み込めるので、条件追加するだけでよさそうだけど需要ないのかな?)
    • xlsx もサポートする PR を出してマージしてもらえたので、 1.35.1 から xlsx も使えるようになった

CSV

フォルダ構成
`-src/test/
  |-java/
  | `-sandbox/dbrider/
  |   `-DataSetTypeTest.java
  `-resources/
    `-sandbox/dbrider/DataSetTypeTest/testCsv/
      |-data-set/
      | |-foo_table.csv
      | |-bar_table.csv
      | `-table_ordering.txt
      `-expected/
        |-foo_table.csv
        |-bar_table.csv
        `-table_ordering.txt
    @Test
    @DataSet("sandbox/dbrider/DataSetTypeTest/testCsv/data-set/foo_table.csv")
    @ExpectedDataSet("sandbox/dbrider/DataSetTypeTest/testCsv/expected/foo_table.csv")
    void testCsv() {}
foo_table.csv
id,value
1,foo
bar_table.csv
id,value
1,bar
table_ordering.txt
foo_table
bar_table
  • Excel でデータセットを定義できる
    • これは、本家DBUnitの CsvDataSet が使用されている
    • CsvDataSet の使い方については こちら を参照
  • @DataSet および @ExpectedDataSetvalue に設定したパスの1つ上のパスが CsvDataSet のコンストラクタに渡される
    • したがって、用意した CSV ファイルのどれを指定しても問題ない(ファイルの存在チェックはされるので、存在しないファイル名だとエラーになる)
  • 拡張子を csv にすると、 CSV として処理される

正規表現

package sandbox.dbrider;

import com.github.database.rider.core.api.dataset.DataSet;
import com.github.database.rider.core.api.dataset.ExpectedDataSet;
import com.github.database.rider.junit5.api.DBRider;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

@DBRider
public class RegularExpressionsTest {
    @RegisterExtension
    static DatabaseSupport db = new DatabaseSupport();

    @BeforeAll
    static void beforeAll() {
        db.executeUpdate("create table foo_table (id integer, value varchar(32))");
    }

    @Test
    @DataSet("sandbox/dbrider/RegularExpressionsTest/data-set.yml")
    @ExpectedDataSet("sandbox/dbrider/RegularExpressionsTest/expected.yml")
    void test() {}
}
data-set.yml
foo_table:
  - id: 10
    value: Hello World!!
expected.yml
foo_table:
  - id: "regex:\\d+"
    value: "regex:^Hello.*"
  • @ExpectedDataSet で指定したデータセットの値を regex:<正規表現> と記述することで、正規表現を使った検証ができるようになる
  • このテストは成功する

DataSet Replacers

DBUnitには、データセットに書かれた特定の文字列を実行時に別の値に置き換えるReplacementDataSetというデータセットが存在する。
Database Riderは、この ReplacementDataSet を使いやすくした DataSet Replacers という仕組みを提供している。

Date replacer

package sandbox.dbrider;

import com.github.database.rider.core.api.dataset.DataSet;
import com.github.database.rider.junit5.api.DBRider;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

@DBRider
public class DateTimeReplacerTest {
    @RegisterExtension
    static DatabaseSupport db = new DatabaseSupport();

    @BeforeAll
    static void beforeAll() {
        db.executeUpdate("create table foo_table (id integer, date_value timestamp)");
    }

    @Test
    @DataSet("sandbox/dbrider/ReplacerTest/testDateTimeReplacer/data-set.yml")
    void testDateTimeReplacer() {
        db.printTable("foo_table");
    }
}
data-set.yml
foo_table:
  - id: 1
    date_value: "[DAY,NOW]"
  - id: 2
    date_value: "[DAY,YESTERDAY]"
  - id: 3
    date_value: "[DAY,WEEK_AFTER]"
  - id: 4
    date_value: "[MIN,NOW]"
  - id: 5
    date_value: "[MIN,PLUS_ONE]"
  - id: 6
    date_value: "[MIN,MINUS_30]"
  - id: 7
    date_value: "[SEC,NOW]"
  - id: 8
    date_value: "[SEC,MINUS_ONE]"
  - id: 9
    date_value: "[SEC,PLUS_TEN]"
実行結果(分かりやすいように末尾にコメントを追加)
{DATE_VALUE=2022-12-14 20:34:55.0, ID=1}  // [DAY,NOW]
{DATE_VALUE=2022-12-13 20:34:55.0, ID=2}  // [DAY,YESTERDAY]
{DATE_VALUE=2022-12-21 20:34:55.0, ID=3}  // [DAY,WEEK_AFTER]

{DATE_VALUE=2022-12-14 20:34:55.0, ID=4}  // [MIN,NOW]
{DATE_VALUE=2022-12-14 20:35:55.0, ID=5}  // [MIN,PLUS_ONE]
{DATE_VALUE=2022-12-14 20:04:55.0, ID=6}  // [MIN,MINUS_30]

{DATE_VALUE=2022-12-14 20:34:55.0, ID=7}  // [SEC,NOW]
{DATE_VALUE=2022-12-14 20:34:54.0, ID=8}  // [SEC,MINUS_ONE]
{DATE_VALUE=2022-12-14 20:35:05.0, ID=9}  // [SEC,PLUS_TEN]
  • [<種別>,<差分>] と記述することで、実行時の現在日時からの相対日時に置き換えることができる
  • <種別> には、差分の種類として以下のいずれかを指定する
種別 説明
DAY
HOUR
MIN
SEC
  • <差分> には、 <種別> ごとに実行日時からどれくらいの相対時間に置き換えるかを指定する
種別 差分 説明
DAY NOW 現在日時
YESTERDAY 1日前
TOMORROW 1日後
WEEK_BEFORE 7日前
WEEK_AFTER 7日後
MONTH_BEFORE 30日前
MONTH_AFTER 30日後
YEAR_BEFORE 365日前
YEAR_AFTER 365日後
HOUR NOW 現在日時
MINUS_ONE 1時間前
PLUS_ONE 1時間後
MINUS_TEN 10時間前
PLUS_TEN 10時間後
MIN NOW 現在日時
MINUS_ONE 1分前
PLUS_ONE 1分後
MINUS_TEN 10分前
PLUS_TEN 10分後
MINUS_30 30分前
PLUS_30 30分後
SEC NOW 現在日時
MINUS_ONE 1秒前
PLUS_ONE 1秒後
MINUS_TEN 10秒前
PLUS_TEN 10秒後
MINUS_30 30秒前
PLUS_30 30秒後

Unix Timestamp replacer

package sandbox.dbrider;

import com.github.database.rider.core.api.dataset.DataSet;
import com.github.database.rider.junit5.api.DBRider;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import java.time.Instant;

@DBRider
public class UnixTimestampReplacerTest {
    @RegisterExtension
    static DatabaseSupport db = new DatabaseSupport();

    @BeforeAll
    static void beforeAll() {
        db.executeUpdate("create table foo_table (id integer, value bigint)");
    }

    @Test
    @DataSet("sandbox/dbrider/UnixTimestampReplacerTest/data-set.yml")
    void test() {
        System.out.println("epoch seconds = " + Instant.now().getEpochSecond());
        db.printTable("foo_table");
    }
}
data-set.yml
foo_table:
  - id: 1
    value: "[UNIX_TIMESTAMP]"
実行結果
epoch seconds = 1671103956
{ID=1, VALUE=1671103956}
  • データセットの値を [UNIX_TIMESTAMP] と記述すると、Unix時間に置換される

Null replacer

package sandbox.dbrider;

import com.github.database.rider.core.api.dataset.DataSet;
import com.github.database.rider.junit5.api.DBRider;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

@DBRider
public class NullReplacerTest {
    @RegisterExtension
    static DatabaseSupport db = new DatabaseSupport();

    @BeforeAll
    static void beforeAll() {
        db.executeUpdate("create table foo_table (id integer, value varchar(32))");
    }

    @Test
    @DataSet("sandbox/dbrider/NullReplacerTest/data-set.yml")
    void test() {
        db.printTable("foo_table");
    }
}
data-set.yml
foo_table:
  - id: 1
    value: null
  - id: 2
    value: "null"
  - id: 3
    value: "[null]"
実行結果
{ID=1, VALUE=null}
{ID=2, VALUE="null"}
{ID=3, VALUE=null}
  • データセットの値を [null] と記述すると、その値は null に置き換えられる
  • YAML のデータセットだと普通に null と書けば null になるので、ぶっちゃけ使う機会はない
  • null を表現できない形式のデータセットでなら活用できる

Include replacer

dbunit.yml
connectionConfig:
  url: "jdbc:hsqldb:mem:test"
  user: "sa"
  password: ""
properties:
  replacers: [!!com.github.database.rider.core.replacers.IncludeReplacer {}]
  • IncludeReplacer はデフォルトでは無効なので、設定ファイル(dbunit.yml)に修正が必要になる
  • properties.replacers に、 IncludeReplacer を追加することで有効になる
  • dbunit.yml は、クラスパス直下に配置する
package sandbox.dbrider;

import com.github.database.rider.core.api.dataset.DataSet;
import com.github.database.rider.core.api.dataset.ExpectedDataSet;
import com.github.database.rider.junit5.api.DBRider;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

@DBRider
public class IncludeReplacerTest {
    @RegisterExtension
    static DatabaseSupport db = new DatabaseSupport();

    @BeforeAll
    static void beforeAll() {
        db.executeUpdate("create table foo_table (id integer, value varchar(32))");
    }

    @Test
    @DataSet("sandbox/dbrider/IncludeReplacerTest/data-set.yml")
    @ExpectedDataSet("sandbox/dbrider/IncludeReplacerTest/expected.yml")
    void test() {}
}
data-set.yml
foo_table:
  - id: "1"
    value: "[INCLUDE]sandbox/dbrider/IncludeReplacerTest/message.txt"
sandbox/dbrider/IncludeReplacerTest/message.txt
Hello World!!
expected.yml
foo_table:
  - id: "1"
    value: Hello World!!
  • データセットの値を [INCLUDE]<リソースのパス> と記述することで、指定されたリソースファイルの内容で置換される
  • リソースは、ClassLoadergetResourceAsStream()で読み込まれているので、それで読み込めるようにファイルやパスを調整する
  • ところで、 IncludeReplacer の内部では各カラムの値に対して [INCLUDE] のプレースホルダがあるかどうかを判定しているところがあり、すべてカラムの値を一律 String にキャストしている部分がある
    • 実装箇所
    • これが原因で、 IncludeReplacer を使う場合は置換対象以外のカラムもすべて文字列で記載しておかないと、実行時に ClassCastException がスローされるという残念な状態になっている
    • 上記の例だと、 id を数値で記載(id: 1 のように記載)すると例外が発生する
  • IncludeReplacer はデフォルトでは有効にはなっていないので、必要であれば冒頭のように設定ファイル(dbunit.yml)に記載が必要となる
    • properties.replacers を設定すると、デフォルトで有効になっている Replacer (ここまでに紹介した Replacer 達)はすべて無効になる
    • デフォルトの Replacer も必要な場合は、以下のように明示的に設定してあげる必要がある
dbunit.yml
properties:
  replacers:
    - !!com.github.database.rider.core.replacers.DateTimeReplacer {}
    - !!com.github.database.rider.core.replacers.UnixTimestampReplacer {}
    - !!com.github.database.rider.core.replacers.NullReplacer {}
    - !!com.github.database.rider.core.replacers.IncludeReplacer {}

自作の Replacer を使用する

CustomReplacer.java
package sandbox.dbrider;

import com.github.database.rider.core.replacers.Replacer;
import org.dbunit.dataset.ReplacementDataSet;

public class CustomReplacer implements Replacer {
    @Override
    public void addReplacements(ReplacementDataSet dataSet) {
        dataSet.addReplacementSubstring("[replace-me]", "World");
    }
}
  • 自作の Replacer
  • Replacer インタフェースを実装して作成する
  • addReplacements(ReplacementDataSet) メソッドをオーバーライドして、置換内容を定義する
    • ReplacementDataSetaddReplacementSubstring(String, String) メソッドで、置換対象のプレースホルダと置換後の文字列を登録する
    • ここでは、 [replace-me] という文字列を World に置換するということを定義している
    • 置換は実行時に都度行われるのではなく、あらかじめ定義しておいた固定値で置換される
  • 公式ガイドの実装例 では、なぜか equals()hashCode() をオーバーライドしているが、デフォルトで提供されている NullReplacer とかでは実装していないので必要はなさそう
CustomReplacerTest.java
package sandbox.dbrider;

import com.github.database.rider.core.api.dataset.DataSet;
import com.github.database.rider.core.api.dataset.ExpectedDataSet;
import com.github.database.rider.junit5.api.DBRider;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

@DBRider
public class CustomReplacerTest {
    @RegisterExtension
    static DatabaseSupport db = new DatabaseSupport();

    @BeforeAll
    static void beforeAll() {
        db.executeUpdate("create table foo_table (id integer, value varchar(32))");
    }

    @Test
    @DataSet("sandbox/dbrider/CustomReplacerTest/data-set.yml")
    @ExpectedDataSet("sandbox/dbrider/CustomReplacerTest/expected.yml")
    void test() {}
}
data-set.yml
foo_table:
  - id: 1
    value: "Hello [replace-me]!!"
expected.yml
foo_table:
  - id: 1
    value: "Hello World!!"
dbunit.yml
connectionConfig:
  url: "jdbc:hsqldb:mem:test"
  user: "sa"
  password: ""
properties:
  replacers:
    - !!sandbox.dbrider.CustomReplacer {}
  • 自作の Replacer を有効にするため、 dbunit.yml に設定する

アノテーションで Replacer を指定する

package sandbox.dbrider;

import com.github.database.rider.core.api.dataset.DataSet;
import com.github.database.rider.core.api.dataset.ExpectedDataSet;
import com.github.database.rider.junit5.api.DBRider;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

@DBRider
public class CustomReplacerAtAnnotationTest {
    @RegisterExtension
    static DatabaseSupport db = new DatabaseSupport();

    @BeforeAll
    static void beforeAll() {
        db.executeUpdate("create table foo_table (id integer, value varchar(32))");
    }

    @Test
    @DataSet(
        value = "sandbox/dbrider/CustomReplacerAtAnnotationTest/data-set.yml",
        replacers = CustomReplacer.class // ★
    )
    @ExpectedDataSet("sandbox/dbrider/CustomReplacerAtAnnotationTest/expected.yml")
    void test() {}
}
  • @DataSetreplacers に、自作の Replacer を指定している
data-set.yml
foo_table:
  - id: 1
    value: Hello [replace-me]!?
expected.yml
foo_table:
  - id: 1
    value: Hello World!?
  • このテストは成功する
  • @DataSetreplacers で、その DataSet だけで使用する Replacer を指定できる
  • replacers を指定した場合は、デフォルトで適用される Replacer (NullReplacer など)は適用されなくなる
  • @ExpectedDataSet にも replacers は存在しており、同じようにその @ExpectedDataSet でだけ適用したい Replacer を指定できる

Scriptable DataSets

データセットに記述する値を、スクリプト言語で記述できる。

Groovy

build.gradle
dependencies {
    ...
    testRuntimeOnly "org.codehaus.groovy:groovy-all:2.4.6"
}
package sandbox.dbrider;

import com.github.database.rider.core.api.dataset.DataSet;
import com.github.database.rider.core.api.dataset.ExpectedDataSet;
import com.github.database.rider.junit5.api.DBRider;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

@DBRider
public class ScriptableDataSetsTest {
    @RegisterExtension
    static DatabaseSupport db = new DatabaseSupport();

    @BeforeAll
    static void beforeAll() {
        db.executeUpdate("create table foo_table (id integer, value varchar(32))");
    }

    @Test
    @DataSet("sandbox/dbrider/ScriptableDataSetsTest/testGroovy/data-set.yml")
    @ExpectedDataSet("sandbox/dbrider/ScriptableDataSetsTest/testGroovy/expected.yml")
    void testGroovy() {}
}
data-set.yml
foo_table:
  - id: 1
    value: "groovy:['hello', 'world'].collect { it.toUpperCase() }.join('-')"
expected.yml
foo_table:
  - id: 1
    value: "HELLO-WORLD"
  • データセットの値を groovy:<任意のスクリプト> と記述することで、 Groovy の式として処理された結果が設定される
  • 仕組みとしては : より前の部分(groovy)が、Javaの標準APIであるScriptEngineManager.getEngineByName(String)に渡されてScriptEngineが取得され、 : より後ろがスクリプトとして処理される、という形になっている
  • ScriptEngineのプロバイダを登録すれば任意のスクリプトで実行できる?
    • jsgroovyで判定しているところがあるからできない(実装箇所)

Script Assertions

data-set.yml
foo_table:
  - id: 1
    value: hello world
expected.yml
foo_table:
  - id: 1
    value: "groovy:(value.equalsIgnoreCase('HELLO WORLD'))"
  • 期待値の値で、スクリプトを使って任意のアサーションを実施できる
  • groovy:(<式>) と記述する
    • 括弧(())は必須なので注意
    • <式> は、真偽値を返すように記述する
      • true の場合はアサートは成功扱いになる
    • アサート対象となる実際の値は value という名前で参照できる
      • 式の中に value があることが Script Assertion として処理される条件になっているので、必ず記述が必要
      • 実装箇所
  • 整理すると、 Script Assertions で使用する式は以下の条件を満たしている必要がある
    • 全体が () で囲われている
    • 式の中に value の記述がある
  • Script Assertions の条件を満たさない場合は、普通に式の評価結果が期待値になって実際の値と比較される

JavaScript

Java 15 で Nashorn(組み込みのJavaScriptエンジン)が削除されたことで、Java 15 以上では利用できなくなっている(自力でエンジンを追加すれば使えると思うけど、面倒なので試してない)。

Java17で試したときのエラーログ

Could not create dataset for test 'testJavaScript'.
java.lang.RuntimeException: Could not create dataset for test 'testJavaScript'.
	...
Caused by: java.lang.RuntimeException: Could not find script engine by name 'js'
	at com.github.database.rider.core.script.ScriptEngineManagerWrapper.getScriptEngine(ScriptEngineManagerWrapper.java:68)
	at com.github.database.rider.core.script.ScriptEngineManagerWrapper.getScriptResult(ScriptEngineManagerWrapper.java:42)
	at com.github.database.rider.core.api.dataset.ScriptableTable.getValue(ScriptableTable.java:45)
	... 74 more

テスト実行前に任意のSQLを実行する

package sandbox.dbrider;

import com.github.database.rider.core.api.dataset.DataSet;
import com.github.database.rider.core.api.dataset.ExpectedDataSet;
import com.github.database.rider.junit5.api.DBRider;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

@DBRider
public class ExecuteStatementsBeforeTest {
    @RegisterExtension
    static DatabaseSupport db = new DatabaseSupport();

    @Test
    @DataSet(
        value = "sandbox/dbrider/ExecuteStatementsBeforeTest/data-set.yml",
        executeStatementsBefore =
            "create table foo_table (id integer, value varchar(32))" // ★
    )
    @ExpectedDataSet("sandbox/dbrider/ExecuteStatementsBeforeTest/expected.yml")
    void test() {}
}
  • @DataSetexecuteStatementsBeforefoo_table を作成する DDL を指定している
data-set.yml
foo_table:
  - id: 1
    value: foo
expected.yml
foo_table:
  - id: 1
    value: foo
  • このテストは成功する
  • @DataSetexecuteStatementsBefore を指定することで、テストの実行前に任意の SQL を実行できる
  • SQL は value で指定したデータセットのデータが投入される前に実行される
  • テスト後に任意の SQL を実行したい場合は executeStatementsAfter を指定する
  • いずれも、配列で複数の SQL を指定できる

任意の SQL をファイルで指定する

package sandbox.dbrider;

import com.github.database.rider.core.api.dataset.DataSet;
import com.github.database.rider.core.api.dataset.ExpectedDataSet;
import com.github.database.rider.junit5.api.DBRider;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

@DBRider
public class ExecuteScriptsBeforeTest {
    @RegisterExtension
    static DatabaseSupport db = new DatabaseSupport();

    @Test
    @DataSet(
        value = "sandbox/dbrider/ExecuteScriptsBeforeTest/data-set.yml",
        executeScriptsBefore =
            "sandbox/dbrider/ExecuteScriptsBeforeTest/create-table.sql" // ★
    )
    @ExpectedDataSet("sandbox/dbrider/ExecuteScriptsBeforeTest/expected.yml")
    void test() {}
}
  • @DataSetexecuteScriptsBefore に、実行したい SQL ファイルを指定している
create-table.sql
create table foo_table (
    id integer,
    value varchar(32)
)
data-set.yml
foo_table:
  - id: 1
    value: foo
expected.yml
foo_table:
  - id: 1
    value: foo
  • このテストは成功する
  • @DataSetexecuteScriptsBefore に SQL ファイルを指定することで、テスト前に任意の SQL を実行できる
  • SQL ファイルは value で指定したデータセットのデータが投入される前に実行される
  • テスト後に任意の SQL ファイルを実行したい場合は executeScriptsAfter を指定する
  • いずれも、配列で複数のファイルを指定できる

データセットのマージ

デフォルトの動作

package sandbox.dbrider;

import com.github.database.rider.core.api.dataset.DataSet;
import com.github.database.rider.junit5.api.DBRider;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

@DBRider
@DataSet(
    value = "sandbox/dbrider/MergeDataSetTest/class-level-data-set.yml",
    executeStatementsBefore = "insert into bar_table values (10, 'class level')"
)
public class MergeDataSetTest {
    @RegisterExtension
    static DatabaseSupport db = new DatabaseSupport();

    @BeforeAll
    static void beforeAll() {
        db.executeUpdate("create table foo_table (id integer, value varchar(32))");
        db.executeUpdate("create table bar_table (id integer, value varchar(32))");
    }

    @Test
    @DataSet(
        value = "sandbox/dbrider/MergeDataSetTest/method-level-data-set.yml",
        executeStatementsBefore = "insert into bar_table values (1, 'method level')"
    )
    void test() {
        System.out.println("=== foo_table ===");
        db.printTable("foo_table");
        System.out.println("=== bar_table ===");
        db.printTable("bar_table");
    }
}
  • クラスとメソッドの両方に @DataSet アノテーションを設定している
  • valueexecuteStatementsBefore を使って、 foo_table, bar_table の両方にデータを登録しようとしている
class-level-data-set.yml
foo_table:
  - id: 10
    value: class level
method-level-data-set.yml
foo_table:
  - id: 1
    value: method level
実行結果
=== foo_table ===
{ID=1, VALUE="method level"}
=== bar_table ===
{ID=1, VALUE="method level"}
  • クラスに設定した @DataSet アノテーションによるデータの登録は一切行われていない
  • クラスとメソッドの両方に @DataSet アノテーションが設定された場合、デフォルトではメソッドに設定した @DataSet のみが使用される

マージを有効にする

dbunit.yml
mergeDataSets: true
  • 設定ファイル (dbunit.yml) で、 mergeDataSets: true を設定する
実行結果
=== foo_table ===
{ID=10, VALUE="class level"}
{ID=1, VALUE="method level"}
=== bar_table ===
{ID=10, VALUE="class level"}
{ID=1, VALUE="method level"}
  • クラスに設定した @DataSet アノテーションによるデータ登録も行われるようになった
  • mergeDataSetstrue を設定すると、クラスとメソッドの両方に @DataSet アノテーションが設定されている場合に、その設定がマージされて実行されるようになる
  • なお、マージできるのは valueexecuteStatementsBefore のように値を配列で指定できるものに限られる
  • イメージとしては、以下のような @DataSet がメソッドに設定されるようになった感じになる
    @DataSet(
        value = {
            "sandbox/dbrider/MergeDataSetTest/class-level-data-set.yml",
            "sandbox/dbrider/MergeDataSetTest/method-level-data-set.yml"
        },
        executeStatementsBefore = {
            "insert into bar_table values (10, 'class level')",
            "insert into bar_table values (1, 'method level')"
        }
    )
  • 配列以外の単一の設定値については、デフォルトではメソッドに設定した値が優先される
package sandbox.dbrider;

import com.github.database.rider.core.api.dataset.DataSet;
import com.github.database.rider.junit5.api.DBRider;
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;

@DBRider
@DataSet(
    cleanBefore = true,
    executeStatementsBefore = "insert into bar_table values (10, 'class level')"
)
public class MergeDataSetTest {
    @RegisterExtension
    static DatabaseSupport db = new DatabaseSupport();

    @BeforeAll
    static void beforeAll() {
        db.executeUpdate("create table foo_table (id integer, value varchar(32))");
        db.executeUpdate("create table bar_table (id integer, value varchar(32))");
    }

    @BeforeEach
    void setUp() {
        db.executeUpdate("insert into foo_table values (9, 'initial value')");
    }

    @Test
    @DataSet(
        cleanBefore = false,
        executeStatementsBefore = "insert into bar_table values (1, 'method level')"
    )
    void test2() {
        System.out.println("=== foo_table ===");
        db.printTable("foo_table");
        System.out.println("=== bar_table ===");
        db.printTable("bar_table");
    }
}
  • クラスとメソッドの両方に @DataSet を設定し、 cleanBeforeexecuteStatementsBefore を設定している
    • クラスは true で、メソッドは false
    • executeStatementsBeforebar_table にだけデータを登録している
  • @BeforeEachfoo_table に初期データを登録している
  • mergeDataSetstrue を設定している
実行結果
=== foo_table ===
{ID=9, VALUE="initial value"}
=== bar_table ===
{ID=10, VALUE="class level"}
{ID=1, VALUE="method level"}
  • foo_table に初期データが残っている
    • cleanBefore が有効になっていれば foo_table は空になっているはず
    • 初期データが残っているということは、メソッドに設定した cleanBefore = false が有効になっていることが分かる
  • bar_table は、 executeStatementsBefore の設定がマージされてクラスとメソッド両方で指定したデータが登録されている
  • 単一の設定値についてクラスで設定した値を優先させたい場合は、 dbunit.yml で以下のように設定する
dbunit.yml
mergeDataSets: true
mergingStrategy: CLASS
  • mergingStrategyCLASS と指定することで、単一の設定値のマージでクラスに設定した値を優先させられるようになる
    • デフォルトは METHOD が設定されて、メソッドに設定した値が優先されるようになっている
mergingStrategy=CLASSでの実行結果
=== foo_table ===
=== bar_table ===
{ID=1, VALUE="method level"}
{ID=10, VALUE="class level"}
  • クラスの方に設定した cleanBefore = true が優先されて foo_table がクリアされるようになる

メタデータセット

MyMetaDataSet.java
package sandbox.dbrider;

import com.github.database.rider.core.api.dataset.DataSet;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
@DataSet("sandbox/dbrider/MetaDataSetTest/meta-data-set.yml")
public @interface MyMetaDataSet {
}
  • @DataSet を設定した独自のアノテーションを作成する
MetaDataSetTest.java
package sandbox.dbrider;

import com.github.database.rider.junit5.api.DBRider;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

@DBRider
public class MetaDataSetTest {
    @RegisterExtension
    static DatabaseSupport db = new DatabaseSupport();

    @BeforeAll
    static void beforeAll() {
        db.executeUpdate("create table foo_table (id integer, value varchar(32))");
    }

    @Test
    @MyMetaDataSet
    void test() {
        db.printTable("foo_table");
    }
}
  • テストメソッドに、先ほどの自作アノテーションを設定する
実行結果
{ID=9, VALUE="meta data set"}
  • @MyMetaDataSet に設定した @DataSet の内容が登録されている
  • @DataSet を設定した独自のアノテーション(メタデータセット)を定義することで、 @DataSet の設定を簡単に使いまわすことができるようになる
  • @TargetElementType.TYPE を設定しておけば、クラスに設定することも可能
    • その場合、データのマージは @DataSet を使った場合と同じようになる
    • デフォルトはメソッドに設定した方が優先され、 mergeDataSetstrue を設定することでマージが行われるようになる
  • 同じメソッドにメタデータセットと @DataSet を同時に設定した場合は、 @DataSet の方が優先された
    • 試したらそうなっただけで、仕様としてそう定められているかは不明(ドキュメントには記述なし)
    • mergeDataSetstrue を設定していた場合でも同じ結果となった
  • 同じメソッドに2つ以上の異なるメタデータセットを同時に設定した場合は、いずれか1つのメタデータセットだけが適用された
    • こちらも mergeDataSetstrue を設定していた場合でも同じ結果となった
  • 同じメソッドの複数のデータセットを指定するような使われ方は想定していない様子なので、やらないのが無難っぽい

コネクションリーク防止機能

dbunit.yml
leakHunter: true
  • 設定ファイル(dbunit.yml)で、 leakHuntertrue を設定する
package sandbox.dbrider;

import com.github.database.rider.junit5.api.DBRider;
import org.junit.jupiter.api.Test;

import java.sql.DriverManager;

@DBRider
public class LeakHunterTest {

    @Test
    void test() throws Exception {
        createLeak();
    }

    private void createLeak() throws Exception {
        DriverManager.getConnection("jdbc:hsqldb:mem:test", "sa", "");
    }
}
  • テスト中にデータベースコネクションを新規に作成して、クローズせずに放置する
実行結果
Execution of method test left 1 open connection(s).
com.github.database.rider.core.leak.LeakHunterException: Execution of method test left 1 open connection(s).
  • 例外が発生してテストは失敗する
  • leakHuntertrue を設定すると、テスト後に JDBC 接続のコネクションリークが発生していないかどうかが検証されるようになる
    • テストの前後で接続数をカウントして、テスト終了後に接続数が増加していた場合にエラーとなる

複数のコネクション

package sandbox.dbrider;

import com.github.database.rider.core.api.dataset.DataSet;
import com.github.database.rider.core.api.dataset.ExpectedDataSet;
import com.github.database.rider.core.dataset.DataSetExecutorImpl;
import com.github.database.rider.junit5.api.DBRider;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import java.sql.DriverManager;

@DBRider
public class MultiDataSourceTest {
    @RegisterExtension
    static DatabaseSupport db1 = new DatabaseSupport("jdbc:hsqldb:mem:test1");
    @RegisterExtension
    static DatabaseSupport db2 = new DatabaseSupport("jdbc:hsqldb:mem:test2");

    @BeforeAll
    static void beforeAll() {
        DataSetExecutorImpl.instance("db1",
                () -> DriverManager.getConnection("jdbc:hsqldb:mem:test1", "sa", ""));
        DataSetExecutorImpl.instance("db2",
                () -> DriverManager.getConnection("jdbc:hsqldb:mem:test2", "sa", ""));

        db1.executeUpdate("create table foo_table (id integer, value varchar(32))");
        db2.executeUpdate("create table bar_table (id integer, value varchar(32))");
    }

    @Test
    @DataSet(
        value = "sandbox/dbrider/MultiDataSourceTest/db1-data-set.yml",
        executorId = "db1"
    )
    @ExpectedDataSet("sandbox/dbrider/MultiDataSourceTest/db1-expected.yml")
    void test1() {}

    @Test
    @DataSet(
        value = "sandbox/dbrider/MultiDataSourceTest/db2-data-set.yml",
        executorId = "db2"
    )
    @ExpectedDataSet("sandbox/dbrider/MultiDataSourceTest/db2-expected.yml")
    void test2() {}
}
  • test1test2 の2つのデータベースをインメモリで作成
  • test1 データベースには foo_table テーブルを、 test2 データベースには bar_table テーブルを作成している
  • test1 メソッドでは test1 データベースを使ったテストを、 test2 メソッドでは test2 データベースを使ったテストを実施している
db1-data-set.yml
foo_table:
  - id: 1
    value: foo
db2-data-set.yml
bar_table:
  - id: 1
    value: bar
  • 期待値のファイルの内容は、それぞれ @DataSet で指定したファイルの内容と同じにしているので割愛
  • このテストは成功する

説明

  • @DataSet で指定したデータセットをデータベースに登録する際、裏では DataSetExecutorImpl が使用されている
  • DataSetExecutorImpl は、それぞれのインスタンスがデータベースコネクションを保持している
  • DataSetExecutorImpl のインスタンスは、同クラスに static で定義されている各種 instance() メソッドで生成できる
    • instance() メソッドで生成されたインスタンスは、内部でキャッシュされて再利用される
  • DataSetExecutorImpl には、個々のインスタンスを識別するためのID (executorId)が割り当てられている
    • 未指定の場合は default という ID が割り当てられる
    • DataSetExecutorImpl.instance() メソッドでインスタンスを生成するときに、引数で任意の ID を指定することも可能
  • 以上の前提を踏まえて、前述の実装を見返す
        DataSetExecutorImpl.instance("db1",
                () -> DriverManager.getConnection("jdbc:hsqldb:mem:test1", "sa", ""));
        DataSetExecutorImpl.instance("db2",
                () -> DriverManager.getConnection("jdbc:hsqldb:mem:test2", "sa", ""));
  • ここで、 DataSetExecutorImpl のインスタンスを生成している
  • インスタンス生成時に、それぞれのインスタンスの ID を引数で指定している (db1, db2)
    @DataSet(
        value = "sandbox/dbrider/MultiDataSourceTest/db1-data-set.yml",
        executorId = "db1"
    )
  • 次に、 @DataSetexecutorId で使用する DataSetExecutorImpl の ID を指定している
  • これにより、各テストでのデータベースコネクションを切り替えている

Spring Boot で使う

実装

フォルダ構成
|-build.gradle
`-src/
  |-main/
  | |-java/
  | | `-sandbox/dbrider/
  | |   `-Main.java
  | `-resources/
  |   `-application.yml
  `-test/
    |-java/
    | `-sandbox/dbrider/
    |   `-SpringBootTest.java
    `-resources/
      `-sandbox/dbrider/SpringBootTest/
        |-data-set.yml
        `-expected.yml
build.gradle
plugins {
    id "java"
    id 'org.springframework.boot' version '2.7.7'
    id 'io.spring.dependency-management' version '1.0.15.RELEASE'
}

sourceCompatibility = 17
targetCompatibility = 17

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

ext {
    dbriderVersion = "1.35.0"
}

repositories {
    mavenCentral()
}

dependencies {
    runtimeOnly "org.hsqldb:hsqldb"
    implementation 'org.springframework.boot:spring-boot-starter-jdbc'

    testImplementation "org.junit.jupiter:junit-jupiter:5.9.1"
    testImplementation "com.github.database-rider:rider-junit5:${dbriderVersion}"

    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation "com.github.database-rider:rider-spring:${dbriderVersion}"
}

test {
    useJUnitPlatform()
}
Main.java
package sandbox.dbrider;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Main {
    public static void main(String[] args) {
        SpringApplication.run(Main.class, args);
    }
}
application.yml
spring.datasource:
  url: jdbc:hsqldb:mem:test
  username: sa
  password:
SpringBootTest.java
package sandbox.dbrider;

import com.github.database.rider.core.api.dataset.DataSet;
import com.github.database.rider.core.api.dataset.ExpectedDataSet;
import com.github.database.rider.junit5.api.DBRider;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;

@DBRider
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = Main.class)
public class SpringBootTest {

    @Autowired
    JdbcTemplate jdbcTemplate;

    @BeforeEach
    void setUp() {
        jdbcTemplate
            .execute("create table if not exists foo_table (id integer, value varchar(32))");
    }

    @Test
    @DataSet("sandbox/dbrider/SpringBootTest/data-set.yml")
    @ExpectedDataSet("sandbox/dbrider/SpringBootTest/expected.yml")
    void test() {}
}
data-set.yml (expected.ymlも同じ内容)
foo_table:
  - id: 1
    value: foo

説明

build.gradle
dependencies {
    ...
    testImplementation "com.github.database-rider:rider-spring:${dbriderVersion}"
}
SpringBootTest.java
@DBRider
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = Main.class)
public class SpringBootTest {
  • Spring のテストを有効にするためのアノテーションと、 Database Rider を有効にするためのアノテーションを両方とも設定する
    • @SpringBootTest は Web に依存するのでここでは使っていないが、 Web アプリな Spring Boot のテストをするのなら @SpringBootTest を使えばいい
  • あとは Database Rider が自動的に Spring 上で実行されているかどうかを検出して、 Spring で実行されている場合は ApplicationContext から DataSource を取得するように裏で勝手に振るまってくれる

データソースの Bean を指定する

Main.java
package sandbox.dbrider;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.jdbc.core.JdbcTemplate;

import javax.sql.DataSource;

@SpringBootApplication
public class Main {
    public static void main(String[] args) {
        SpringApplication.run(Main.class, args);
    }

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.primary")
    public DataSourceProperties primaryDataSourceProperties() {
        return new DataSourceProperties();
    }

    @Bean
    public DataSource primaryDataSource() {
        return primaryDataSourceProperties()
                .initializeDataSourceBuilder()
                .build();
    }

    @Bean
    public JdbcTemplate primaryJdbcTemplate() {
        return new JdbcTemplate(primaryDataSource());
    }

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.secondary")
    public DataSourceProperties secondaryDataSourceProperties() {
        return new DataSourceProperties();
    }

    @Bean
    public DataSource secondaryDataSource() {
        return secondaryDataSourceProperties()
                .initializeDataSourceBuilder()
                .build();
    }

    @Bean
    public JdbcTemplate secondaryJdbcTemplate() {
        return new JdbcTemplate(secondaryDataSource());
    }
}
application.yml
spring.datasource:
  primary:
    url: jdbc:hsqldb:mem:test1
    username: sa
    password:
  secondary:
    url: jdbc:hsqldb:mem:test2
    username: sa
    password:
  • 2つのデータソースを定義
SpringBootTest.java
package sandbox.dbrider;

import com.github.database.rider.core.api.dataset.DataSet;
import com.github.database.rider.core.api.dataset.ExpectedDataSet;
import com.github.database.rider.junit5.api.DBRider;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;

@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = Main.class)
public class SpringBootTest {

    @Autowired
    @Qualifier("primaryJdbcTemplate")
    JdbcTemplate primaryJdbcTemplate;
    @Autowired
    @Qualifier("secondaryJdbcTemplate")
    JdbcTemplate secondaryJdbcTemplate;

    @BeforeEach
    void setUp() {
        primaryJdbcTemplate
            .execute("create table if not exists foo_table (id integer, value varchar(32))");
        secondaryJdbcTemplate
            .execute("create table if not exists bar_table (id integer, value varchar(32))");
    }

    @Test
    @DBRider(dataSourceBeanName = "primaryDataSource") // ★
    @DataSet("sandbox/dbrider/SpringBootTest/test1/data-set.yml")
    @ExpectedDataSet("sandbox/dbrider/SpringBootTest/test1/expected.yml")
    void test1() {}

    @Test
    @DBRider(dataSourceBeanName = "secondaryDataSource") // ★
    @DataSet("sandbox/dbrider/SpringBootTest/test2/data-set.yml")
    @ExpectedDataSet("sandbox/dbrider/SpringBootTest/test2/expected.yml")
    void test2() {}
}
  • primaryDataSource には foo_table テーブルを作成し、 secondaryDataSource には bar_table テーブルを作成している
  • @DBRider アノテーションの dataSourceBeanName で使用する DataSource の Bean 名を指定している
test1/data-set.yml (expected は同じなので省略)
foo_table:
  - id: 1
    value: foo
test2/data-set.yml (expected は同じなので省略)
bar_table:
  - id: 1
    value: foo
  • このテストは成功する
  • @DBRiderdataSourceBeanName に使用する DataSource の Bean 名を指定することで、その DataSource を使用してデータベースアクセスが行われるようになる

参考

8
6
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
8
6