CLOVER🍀

That was when it all began.

DbUnitを試してみる

これは、なにをしたくて書いたもの?

Javaのテストまわりの情報を見ていて、データベースの事情って今はどうなんだろう?と思いまして。

見ていて、DbUnitから変わっていなさそうだったのと、そもそもDbUnitを使ったことがなかったのでちょっと試してみることにしました。

DbUnitと周辺事情

DbUnitはJUnitのExtensionとされていて、データベースドリブンなプロジェクトをターゲットにしています。

DbUnit is a JUnit extension (also usable with Ant) targeted at database-driven projects that, among other things, puts your database into a known state between test runs.

About DbUnit

データのエクスポート・インポートの機能を持ち、データベースが持つ値でのアサーションも行えます。

DbUnit has the ability to export and import your database data to and from XML datasets. Since version 2.0, DbUnit can also work with very large datasets when used in streaming mode. DbUnit can also help you to verify that your database data match an expected set of values.

Spring Frameworkのテストに関するドキュメント内でも、データベースに関するテストのリソースで参照されているのは、DbUnitのみです。

DbUnit: JUnit extension (also usable with Ant and Maven) that is targeted at database-driven projects and, among other things, puts your database into a known state between test runs.

Testing / Further Resources

なので、まずはDbUnitを見てみるのが妥当なのかな、とは思うのですが。

DbUnitのサイトを見てみると、最終リリースが2012年の2.4.9になっていてSourceForgeにあるプロジェクトも更新されていません。

dbUnit download | SourceForge.net

が、よく見るとMaven Centralからは新しいバージョンがダウンロードできると書かれているので、見てみると現時点の最新のバージョンは
2.7.3です。

https://search.maven.org/artifact/org.dbunit/dbunit/2.7.3/jar

現在、ソースコードはこちらでバージョン管理されているようです。

dbUnit / dbunit / [716ef9]

なので、メンテナンスは継続されていそうです。

その他、DbUnitを取り巻くOSSとして、Spring Test向けのSpring Test DBUnitというものがありますが、こちらは更新が止まって
久しそう…。

Spring Test DBUnit – Introduction

Database Riderというものもあるので、今はこちらを見るのがよいのかもしれません。

Database Rider

DbUnitについて

DbUnitに影響を与えた、データベースのテストに関する話はこちら。

Database Testing

いわゆるGetting Started。

Getting Started

ステップとしては、以下のようです。

  • データファイルの作成
  • データベースの接続情報等のセットアップを行い、テストを実施

DbUnitはDBTestCaseクラスを提供しているようですが、JUnit 5時代のものではないのでこちらは使わないでしょうね。

実際、DbUnitの依存関係にはJUnit 5は含まれていません。

https://sourceforge.net/p/dbunit/code.git/ci/dbunit-2.7.3/tree/pom.xml

アサーションのサポートとしては、以下あたりができそうです。

  • クエリーを元にしたスナップショット(ITable)の取得
  • スナップショットの比較の際に、カラムのフィルタリング
  • スナップショットのソート

また、データファイルからのデータロードも可能なようです。

DbUnitを構成する主なコンポーネントは、こちらに書かれています。

Core Components

  • IDatabaseConnectionインターフェース … データベースへの接続を表す
  • IDataSetインターフェース … テーブルのコレクションを表す
  • DatabaseOperationクラス … 各テストの前後でデータベースに対する操作を抽象化する

IDataSetインターフェースの実装として、XML、クエリー、Excel、プログラムで組み立てるもの等いろいろあるようですが、
ドキュメントに記載されているものは古そうなので、ソースコードを見た方がいいんでしょうね。

https://sourceforge.net/p/dbunit/code.git/ci/dbunit-2.7.3/tree/src/main/java/org/dbunit/dataset/

CSVファイルでも扱えそうです。

DatabaseOperationクラスは、各テストの前後でデータベースに対する操作を実行しますが、以下のようなことを行えます。

  • UPDATE
  • INSERT
  • DELETE
  • DELETE_ALL
  • TRUNCATE
  • REFRESH
  • CLEAN_INSERT
  • NONE

直感的にわからなそうなのはREFRESHとCLEAN_INSERTでしょうか。REFRESHは対象となるデータセットをデータベースに反映するもので、
データセットに含まれる範囲のデータのみを対象とし、それ以外のデータには影響を与えません。CLEANはDELETE_ALLの後にINSERTを
実行するものです。

その他、複数の操作をひとつにまとめるCompositeOperationクラス、トランザクション内で操作を実行するTransactionOperationクラスなども
あります。

とまあ、ドキュメントを読むのはこれくらいにして、実際に使っていってみましょう。

お題

今回、お題は以下にします。

  • DbUnitを使ったテストを書く
  • ファイルで用意するデータセットには、XMLを使用する
  • データベースアクセスにはDoma 2を使用する
  • テスティングフレームワークには、JUnit 5を使用する
  • データベースにはMySQL 8.0を使用する

データセットで使うファイルはCSVにしようかなと思っていたのですが、CSVでのデータセットはなかなか独特みたいなので、今回はXMLに
しました。

環境

今回の環境は、こちら。

$ java --version
openjdk 17.0.2 2022-01-18
OpenJDK Runtime Environment (build 17.0.2+8-Ubuntu-120.04)
OpenJDK 64-Bit Server VM (build 17.0.2+8-Ubuntu-120.04, mixed mode, sharing)


$ mvn --version
Apache Maven 3.8.5 (3599d3414f046de2324203b78ddcf9b5e4388aa0)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 17.0.2, vendor: Private Build, runtime: /usr/lib/jvm/java-17-openjdk-amd64
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "5.4.0-107-generic", arch: "amd64", family: "unix"

MySQLは172.17.0.2で動作しているものとします。バージョンは、こちら。

$ mysql --version
mysql  Ver 8.0.28 for Linux on x86_64 (MySQL Community Server - GPL)

準備

まずは、プロジェクトを作成していきます。

今回のMaven依存関係等はこちら。

    <dependencies>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.28</version>
            <scope>runtime</scope>
        </dependency>

        <dependency>
            <groupId>org.seasar.doma</groupId>
            <artifactId>doma-core</artifactId>
            <version>2.51.0</version>
        </dependency>

        <dependency>
            <groupId>org.seasar.doma</groupId>
            <artifactId>doma-processor</artifactId>
            <version>2.51.0</version>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>5.8.2</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.dbunit</groupId>
            <artifactId>dbunit</artifactId>
            <version>2.7.3</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.22.2</version>
            </plugin>
        </plugins>
    </build>

DbUnitは2.7.3ですね。

続いて、データベースアクセスを行うプログラムをDoma 2を使って作成します。

エンティティクラス。

src/main/java/org/littlewings/dbunit/Book.java

package org.littlewings.dbunit;

import java.time.LocalDate;

import org.seasar.doma.Entity;
import org.seasar.doma.Id;

@Entity
public class Book {
    @Id
    String isbn;
    String title;
    Integer price;
    LocalDate publishDate;
    String category;

    public static Book create(String isbn, String title, Integer price, LocalDate publishDate, String category) {
        Book book = new Book();
        book.setIsbn(isbn);
        book.setTitle(title);
        book.setPrice(price);
        book.setCategory(category);

        book.setPublishDate(publishDate);

        return book;
    }

    // getter/setterは省略
}

Dao。

src/main/java/org/littlewings/dbunit/BookDao.java

package org.littlewings.dbunit;

import org.seasar.doma.Dao;
import org.seasar.doma.Insert;
import org.seasar.doma.Script;
import org.seasar.doma.Select;
import org.seasar.doma.Sql;

@Dao
public interface BookDao {
    @Sql("""
 drop table if exists book;
 create table book(
   isbn varchar(14),
   title varchar(100),
   price int,
   publish_date date,
   category varchar(20),
   primary key(isbn)
 )
 """)
    @Script
    void createTableIfExistsRecreate();

    @Sql("truncate table book")
    @Script
    void truncate();

    @Insert
    int insert(Book book);

    @Sql("select /*%expand*/* from book where isbn = /* isbn */'dummy'")
    @Select
    Book selectByIsbn(String isbn);
}

Config。

src/main/java/org/littlewings/dbunit/DomaConfig.java

package org.littlewings.dbunit;

import javax.sql.DataSource;

import org.seasar.doma.jdbc.Config;
import org.seasar.doma.jdbc.Naming;
import org.seasar.doma.jdbc.dialect.Dialect;
import org.seasar.doma.jdbc.dialect.MysqlDialect;
import org.seasar.doma.jdbc.tx.LocalTransactionDataSource;
import org.seasar.doma.jdbc.tx.LocalTransactionManager;
import org.seasar.doma.jdbc.tx.TransactionManager;

public class DomaConfig implements Config {
    private static final DomaConfig CONFIG = new DomaConfig();

    Dialect dialect;

    LocalTransactionDataSource dataSource;

    TransactionManager transactionManager;

    public static DomaConfig singleton() {
        return CONFIG;
    }

    private DomaConfig() {
        dialect = new MysqlDialect();
        dataSource = new LocalTransactionDataSource(
                "jdbc:mysql://172.17.0.2:3306/practice?characterEncoding=utf-8&characterSetResults=utf-8&connectionCollation=utf8mb4_0900_bin",
                "kazuhira",
                "password"
        );
        transactionManager = new LocalTransactionManager(dataSource.getLocalTransaction(getJdbcLogger()));
    }

    @Override
    public DataSource getDataSource() {
        return dataSource;
    }

    @Override
    public Dialect getDialect() {
        return dialect;
    }

    @Override
    public TransactionManager getTransactionManager() {
        return transactionManager;
    }

    @Override
    public Naming getNaming() {
        return Naming.SNAKE_LOWER_CASE;
    }
}

ここまでで、準備は完了です。あとはこれらを使ってテストコードを書いていきます。

DbUnitを使ってみる

では、DbUnitを使っていきましょう。まずは、テストコードの雛形を用意。

src/test/java/org/littlewings/dbunit/DbUnitGettingStartedTest.java

package org.littlewings.dbunit;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.time.LocalDate;
import java.util.List;

import org.dbunit.Assertion;
import org.dbunit.DatabaseUnitException;
import org.dbunit.database.DatabaseConnection;
import org.dbunit.dataset.IDataSet;
import org.dbunit.dataset.ITable;
import org.dbunit.dataset.SortedTable;
import org.dbunit.dataset.xml.FlatXmlDataSetBuilder;
import org.dbunit.ext.mysql.MySqlConnection;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

public class DbUnitGettingStartedTest {
    @BeforeAll
    public static void setUpAll() {
        BookDao bookDao = new BookDaoImpl(DomaConfig.singleton());
        DomaConfig
                .singleton()
                .getTransactionManager()
                .required(() -> bookDao.createTableIfExistsRecreate());
    }

    @BeforeEach
    public void setUp() {
        BookDao bookDao = new BookDaoImpl(DomaConfig.singleton());
        DomaConfig
                .singleton()
                .getTransactionManager()
                .required(() -> bookDao.truncate());
    }

    // ここにテストを書く!
}

テスト前に1度だけテーブルをdrop & createして、各テストごとにテーブルをtruncateします。

最初はDbUnitを使ってデータベースに接続してみましょう。こんな感じで、JDBCのConnectionからDatabaseConnectionを取得します。

    @Test
    public void createDbUnitConnection() throws DatabaseUnitException, SQLException {
        DatabaseConnection databaseConnection = null;
        try (Connection conn = DriverManager
                .getConnection(
                        "jdbc:mysql://172.17.0.2:3306/practice?characterEncoding=utf-8&characterSetResults=utf-8&connectionCollation=utf8mb4_0900_bin",
                        "kazuhira",
                        "password");) {
            databaseConnection = new MySqlConnection(conn, null);
        } finally {
            if (databaseConnection != null) {
                databaseConnection.close();
            }
        }
    }

今回はMySqlConnectionクラスを使っていますが、汎用的なDatabaseConnectionクラスをnewしても良いですし、他のデータベースの場合は
それぞれに応じたクラスを使ってもよいでしょう。

https://sourceforge.net/p/dbunit/code.git/ci/dbunit-2.7.3/tree/src/main/java/org/dbunit/ext/

見たところ、汎用的なDatabaseConnectionクラスとの差はそれぞれのデータベースを考慮したデータ型やメタデータを扱うクラスを適用して
くれるところみたいですね。

次に、アサーションを行ってみます。

    @Test
    public void firstAssertion() throws DatabaseUnitException, SQLException {
        DatabaseConnection databaseConnection = null;
        try (Connection conn = DriverManager
                .getConnection(
                        "jdbc:mysql://172.17.0.2:3306/practice?characterEncoding=utf-8&characterSetResults=utf-8&connectionCollation=utf8mb4_0900_bin",
                        "kazuhira",
                        "password");) {
            databaseConnection = new MySqlConnection(conn, null);

            DomaConfig
                    .singleton()
                    .getTransactionManager()
                    .required(() -> {
                        BookDao bookDao = new BookDaoImpl(DomaConfig.singleton());

                        List<Book> books = List.of(
                                Book.create("978-4295008477", "新世代Javaプログラミングガイド[Java SE 10/11/12/13と言語拡張プロジェクト]", 2860, LocalDate.of(2020, 3, 13), "java"),
                                Book.create("978-4621303252", "Effective Java 第3版", 4400, LocalDate.of(2018, 10, 30), "java"),
                                Book.create("978-4774189093", "Java本格入門 〜モダンスタイルによる基礎からオブジェクト指向・実用ライブラリまで", 3278, LocalDate.of(2017, 4, 18), "java"),
                                Book.create("978-4798161488", "MySQL徹底入門 第4版 MySQL 8.0対応", 4180, LocalDate.of(2020, 7, 6), "mysql"),
                                Book.create("978-4798147406", "詳解MySQL 5.7 止まらぬ進化に乗り遅れないためのテクニカルガイド", 3960, LocalDate.of(2016, 8, 26), "mysql")
                        );

                        books.forEach(bookDao::insert);
                    });

            IDataSet actualDataSet = databaseConnection.createDataSet();
            ITable actualTable = actualDataSet.getTable("book");

            IDataSet expectedDataSet =
                    new FlatXmlDataSetBuilder()
                            .build(getClass().getClassLoader().getResourceAsStream("DbUnitGettingStartedTest.firstAssertion.xml"));
            ITable expectedTable = expectedDataSet.getTable("book");

            // assert dataset
            Assertion.assertEquals(expectedDataSet, actualDataSet);
            // assert table
            Assertion.assertEquals(expectedTable, actualTable);
        } finally {
            if (databaseConnection != null) {
                databaseConnection.close();
            }
        }
    }

テーブルにデータを登録。

            DomaConfig
                    .singleton()
                    .getTransactionManager()
                    .required(() -> {
                        BookDao bookDao = new BookDaoImpl(DomaConfig.singleton());

                        List<Book> books = List.of(
                                Book.create("978-4295008477", "新世代Javaプログラミングガイド[Java SE 10/11/12/13と言語拡張プロジェクト]", 2860, LocalDate.of(2020, 3, 13), "java"),
                                Book.create("978-4621303252", "Effective Java 第3版", 4400, LocalDate.of(2018, 10, 30), "java"),
                                Book.create("978-4774189093", "Java本格入門 〜モダンスタイルによる基礎からオブジェクト指向・実用ライブラリまで", 3278, LocalDate.of(2017, 4, 18), "java"),
                                Book.create("978-4798161488", "MySQL徹底入門 第4版 MySQL 8.0対応", 4180, LocalDate.of(2020, 7, 6), "mysql"),
                                Book.create("978-4798147406", "詳解MySQL 5.7 止まらぬ進化に乗り遅れないためのテクニカルガイド", 3960, LocalDate.of(2016, 8, 26), "mysql")
                        );

                        books.forEach(bookDao::insert);
                    });

この後、DatabaseConnectionクラスのインスタンスからデータセットおよびテーブルのデータを取得します。

            IDataSet actualDataSet = databaseConnection.createDataSet();
            ITable actualTable = actualDataSet.getTable("book");

そして、確認を行うデータセットをXMLファイルから読み込みます。こちらもデータセットと、データセットからテーブルに対応する
データとして扱えます。

            IDataSet expectedDataSet =
                    new FlatXmlDataSetBuilder()
                            .build(getClass().getClassLoader().getResourceAsStream("DbUnitGettingStartedTest.firstAssertion.xml"));
            ITable expectedTable = expectedDataSet.getTable("book");

用意したXMLファイルはこちら。

src/test/resources/DbUnitGettingStartedTest.firstAssertion.xml

<?xml version="1.0" encoding="UTF-8"?>
<dataset>
    <book isbn="978-4295008477" title="新世代Javaプログラミングガイド[Java SE 10/11/12/13と言語拡張プロジェクト]" price="2860" publish_date="2020-03-13" category="java"/>
    <book isbn="978-4621303252" title="Effective Java 第3版" price="4400" publish_date="2018-10-30" category="java"/>
    <book isbn="978-4774189093" title="Java本格入門 〜モダンスタイルによる基礎からオブジェクト指向・実用ライブラリまで" price="3278" publish_date="2017-04-18" category="java"/>
    <book isbn="978-4798147406" title="詳解MySQL 5.7 止まらぬ進化に乗り遅れないためのテクニカルガイド" price="3960" publish_date="2016-08-26" category="mysql"/>
    <book isbn="978-4798161488" title="MySQL徹底入門 第4版 MySQL 8.0対応" price="4180" publish_date="2020-07-06" category="mysql"/>
</dataset>

要素名にテーブル名、属性にカラム名といった感じですね。

その他のファイル形式の場合は、リポジトリのテストに関する部分を見るとよいでしょう。

https://sourceforge.net/p/dbunit/code.git/ci/dbunit-2.7.3/tree/src/test/resources/

https://sourceforge.net/p/dbunit/code.git/ci/dbunit-2.7.3/tree/src/test/java/org/dbunit/dataset/

アサーション。データセット単位でも行えますし、テーブル単位でも行えます。

            // assert dataset
            Assertion.assertEquals(expectedDataSet, actualDataSet);
            // assert table
            Assertion.assertEquals(expectedTable, actualTable);

こちらで、現在データセットおよびテーブル(今回はデータセットとテーブルが同じ範囲ですが)に格納されているデータと、XMLファイルで
用意したデータが等しいことが確認できました。

アサーションを行う対象のデータを絞り込むこともできます。たとえば、SQLを使う場合はこちら。

    @Test
    public void createDataSetFromQuery() throws DatabaseUnitException, SQLException {
        DatabaseConnection databaseConnection = null;
        try (Connection conn = DriverManager
                .getConnection(
                        "jdbc:mysql://172.17.0.2:3306/practice?characterEncoding=utf-8&characterSetResults=utf-8&connectionCollation=utf8mb4_0900_bin",
                        "kazuhira",
                        "password");) {
            databaseConnection = new MySqlConnection(conn, null);

            DomaConfig
                    .singleton()
                    .getTransactionManager()
                    .required(() -> {
                        BookDao bookDao = new BookDaoImpl(DomaConfig.singleton());

                        List<Book> books = List.of(
                                Book.create("978-4295008477", "新世代Javaプログラミングガイド[Java SE 10/11/12/13と言語拡張プロジェクト]", 2860, LocalDate.of(2020, 3, 13), "java"),
                                Book.create("978-4621303252", "Effective Java 第3版", 4400, LocalDate.of(2018, 10, 30), "java"),
                                Book.create("978-4774189093", "Java本格入門 〜モダンスタイルによる基礎からオブジェクト指向・実用ライブラリまで", 3278, LocalDate.of(2017, 4, 18), "java"),
                                Book.create("978-4798161488", "MySQL徹底入門 第4版 MySQL 8.0対応", 4180, LocalDate.of(2020, 7, 6), "mysql"),
                                Book.create("978-4798147406", "詳解MySQL 5.7 止まらぬ進化に乗り遅れないためのテクニカルガイド", 3960, LocalDate.of(2016, 8, 26), "mysql")
                        );

                        books.forEach(bookDao::insert);
                    });

            ITable actualTable =
                    databaseConnection
                            .createQueryTable("query_result", "select * from book where category = 'mysql' order by isbn asc");

            IDataSet expectedDataSet =
                    new FlatXmlDataSetBuilder()
                            .build(getClass().getClassLoader().getResourceAsStream("DbUnitGettingStartedTest.createDataSetFromQuery.xml"));
            ITable expectedTable = expectedDataSet.getTable("book");

            Assertion.assertEquals(expectedTable, actualTable);
        } finally {
            if (databaseConnection != null) {
                databaseConnection.close();
            }
        }
    }

この部分ですね。先ほどの場合はデータセットとして選択した範囲および、テーブルに関してはそのテーブルの全データが対象になりますが、
今回はカテゴリーがmysqlのものに絞っています。

            ITable actualTable =
                    databaseConnection
                            .createQueryTable("query_result", "select * from book where category = 'mysql' order by isbn asc");

この場合はテーブルのデータとして扱うことになるので、アサーションもテーブル単位になります。

            IDataSet expectedDataSet =
                    new FlatXmlDataSetBuilder()
                            .build(getClass().getClassLoader().getResourceAsStream("DbUnitGettingStartedTest.createDataSetFromQuery.xml"));
            ITable expectedTable = expectedDataSet.getTable("book");

            Assertion.assertEquals(expectedTable, actualTable);

用意したXMLファイルはこちら。カテゴリーがmysqlとなっているもののみになりますね。

src/test/resources/DbUnitGettingStartedTest.createDataSetFromQuery.xml

<?xml version="1.0" encoding="UTF-8"?>
<dataset>
    <book isbn="978-4798147406" title="詳解MySQL 5.7 止まらぬ進化に乗り遅れないためのテクニカルガイド" price="3960" publish_date="2016-08-26" category="mysql"/>
    <book isbn="978-4798161488" title="MySQL徹底入門 第4版 MySQL 8.0対応" price="4180" publish_date="2020-07-06" category="mysql"/>
</dataset>

データセット内のデータは、主キーによりソートされます。このソートキーを変更するには、SortedTableクラスを使います。

    @Test
    public void sortTable() throws DatabaseUnitException, SQLException {
        DatabaseConnection databaseConnection = null;
        try (Connection conn = DriverManager
                .getConnection(
                        "jdbc:mysql://172.17.0.2:3306/practice?characterEncoding=utf-8&characterSetResults=utf-8&connectionCollation=utf8mb4_0900_bin",
                        "kazuhira",
                        "password");) {
            databaseConnection = new MySqlConnection(conn, null);

            DomaConfig
                    .singleton()
                    .getTransactionManager()
                    .required(() -> {
                        BookDao bookDao = new BookDaoImpl(DomaConfig.singleton());

                        List<Book> books = List.of(
                                Book.create("978-4295008477", "新世代Javaプログラミングガイド[Java SE 10/11/12/13と言語拡張プロジェクト]", 2860, LocalDate.of(2020, 3, 13), "java"),
                                Book.create("978-4621303252", "Effective Java 第3版", 4400, LocalDate.of(2018, 10, 30), "java"),
                                Book.create("978-4774189093", "Java本格入門 〜モダンスタイルによる基礎からオブジェクト指向・実用ライブラリまで", 3278, LocalDate.of(2017, 4, 18), "java"),
                                Book.create("978-4798161488", "MySQL徹底入門 第4版 MySQL 8.0対応", 4180, LocalDate.of(2020, 7, 6), "mysql"),
                                Book.create("978-4798147406", "詳解MySQL 5.7 止まらぬ進化に乗り遅れないためのテクニカルガイド", 3960, LocalDate.of(2016, 8, 26), "mysql")
                        );

                        books.forEach(bookDao::insert);
                    });

            IDataSet actualDataSet = databaseConnection.createDataSet();
            SortedTable actualTable = new SortedTable(actualDataSet.getTable("book"), new String[]{"price"});
            actualTable.setUseComparable(true);

            IDataSet expectedDataSet =
                    new FlatXmlDataSetBuilder()
                            .build(getClass().getClassLoader().getResourceAsStream("DbUnitGettingStartedTest.sortTable.xml"));
            ITable expectedTable = expectedDataSet.getTable("book");
            //SortedTable expectedTable = new SortedTable(expectedDataSet.getTable("book"), new String[]{"price"});
            //expectedTable.setUseComparable(true);

            Assertion.assertEquals(expectedTable, actualTable);
        } finally {
            if (databaseConnection != null) {
                databaseConnection.close();
            }
        }
    }

こんな感じで、テーブルをさらにSortedTableクラスでラップします。SortedTableクラスのインスタンス作成後に、

SortedTable#setUseComparableメソッドを呼び出す必要があるようです。

            IDataSet actualDataSet = databaseConnection.createDataSet();
            SortedTable actualTable = new SortedTable(actualDataSet.getTable("book"), new String[]{"price"});
            actualTable.setUseComparable(true);

今回は、価格でソートしました。ソート順の指定はできないようです。というか、ソートを行っているのはデータベース上ではなく
Java上でですね。

https://sourceforge.net/p/dbunit/code.git/ci/dbunit-2.7.3/tree/src/main/java/org/dbunit/dataset/SortedTable.java

用意したXMLファイルは、こちら。あらかじめ価格でソートしておきましt.あ

src/test/resources/DbUnitGettingStartedTest.sortTable.xml

<?xml version="1.0" encoding="UTF-8"?>
<dataset>
    <book isbn="978-4295008477" title="新世代Javaプログラミングガイド[Java SE 10/11/12/13と言語拡張プロジェクト]" price="2860" publish_date="2020-03-13" category="java"/>
    <book isbn="978-4774189093" title="Java本格入門 〜モダンスタイルによる基礎からオブジェクト指向・実用ライブラリまで" price="3278" publish_date="2017-04-18" category="java"/>
    <book isbn="978-4798147406" title="詳解MySQL 5.7 止まらぬ進化に乗り遅れないためのテクニカルガイド" price="3960" publish_date="2016-08-26" category="mysql"/>
    <book isbn="978-4798161488" title="MySQL徹底入門 第4版 MySQL 8.0対応" price="4180" publish_date="2020-07-06" category="mysql"/>
    <book isbn="978-4621303252" title="Effective Java 第3版" price="4400" publish_date="2018-10-30" category="java"/>
</dataset>

よってこちらはソートしなくてもよいのですが、ソートする場合はコメントアウトしている箇所のような感じになります。

            ITable expectedTable = expectedDataSet.getTable("book");
            //SortedTable expectedTable = new SortedTable(expectedDataSet.getTable("book"), new String[]{"price"});
            //expectedTable.setUseComparable(true);

アサーション時に、データのソート結果が合わないとテストが失敗するので注意しましょう。

DatabaseOperationクラスを使ってみる

最後に、DatabaseOperationクラスを使ってみましょう。このクラスを使うと、テスト前後で行いたいようなデータセットの操作を簡単に
行えます。

ただ、今回はテスト前後で実行するのではなく、テストコード内でアサーションと組み合わせてDatabaseOperationクラスの機能自体を
確認していこうかなと思います。

最初にテストコードを丸ごと載せてしまいましょう。

src/test/java/org/littlewings/dbunit/DbUnitOperationTest.java

package org.littlewings.dbunit;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.time.LocalDate;
import java.util.List;

import org.dbunit.Assertion;
import org.dbunit.DatabaseUnitException;
import org.dbunit.database.DatabaseConnection;
import org.dbunit.dataset.IDataSet;
import org.dbunit.dataset.ITable;
import org.dbunit.dataset.xml.FlatXmlDataSetBuilder;
import org.dbunit.ext.mysql.MySqlConnection;
import org.dbunit.operation.DatabaseOperation;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

public class DbUnitOperationTest {
    @BeforeAll
    public static void setUpAll() {
        BookDao bookDao = new BookDaoImpl(DomaConfig.singleton());
        DomaConfig
                .singleton()
                .getTransactionManager()
                .required(() -> bookDao.createTableIfExistsRecreate());
    }

    @Test
    public void test() throws SQLException, DatabaseUnitException {
        DatabaseConnection databaseConnection = null;
        try (Connection conn = DriverManager
                .getConnection(
                        "jdbc:mysql://172.17.0.2:3306/practice?characterEncoding=utf-8&characterSetResults=utf-8&connectionCollation=utf8mb4_0900_bin",
                        "kazuhira",
                        "password");) {
            databaseConnection = new MySqlConnection(conn, null);

            // INSERT Operation
            IDataSet insertedDataSet =
                    new FlatXmlDataSetBuilder()
                            .build(getClass().getClassLoader().getResourceAsStream("DbUnitOperationTest.test.xml"));

            DatabaseOperation.INSERT.execute(databaseConnection, insertedDataSet);
            // JUnit 5 Assersions
            Assertions.assertEquals(2, databaseConnection.getRowCount("book"));

            DomaConfig
                    .singleton()
                    .getTransactionManager()
                    .required(() -> {
                        BookDao bookDao = new BookDaoImpl(DomaConfig.singleton());

                        List<Book> books = List.of(
                                Book.create("978-4295008477", "新世代Javaプログラミングガイド[Java SE 10/11/12/13と言語拡張プロジェクト]", 2860, LocalDate.of(2020, 3, 13), "java"),
                                Book.create("978-4621303252", "Effective Java 第3版", 4400, LocalDate.of(2018, 10, 30), "java"),
                                Book.create("978-4774189093", "Java本格入門 〜モダンスタイルによる基礎からオブジェクト指向・実用ライブラリまで", 3278, LocalDate.of(2017, 4, 18), "java"),
                                Book.create("978-4798161488", "MySQL徹底入門 第4版 MySQL 8.0対応", 4180, LocalDate.of(2020, 7, 6), "mysql"),
                                Book.create("978-4798147406", "詳解MySQL 5.7 止まらぬ進化に乗り遅れないためのテクニカルガイド", 3960, LocalDate.of(2016, 8, 26), "mysql")
                        );

                        books.forEach(bookDao::insert);
                    });

            Assertions.assertEquals(7, databaseConnection.getRowCount("book"));

            // REFRESH(update or insert) Operation
            IDataSet refreshedDataSet =
                    new FlatXmlDataSetBuilder()
                            .build(getClass().getClassLoader().getResourceAsStream("DbUnitOperationTest.test.xml"));

            DatabaseOperation.REFRESH.execute(databaseConnection, refreshedDataSet);
            // JUnit 5 Assersions
            Assertions.assertEquals(7, databaseConnection.getRowCount("book"));
            ITable categories = databaseConnection.createQueryTable("result", "select category from book group by category order by category");
            Assertions.assertEquals("java", categories.getValue(0, "category"));
            Assertions.assertEquals("linux", categories.getValue(1, "category"));
            Assertions.assertEquals("mysql", categories.getValue(2, "category"));

            // CLEAN_INSERT(delete all + insert) Operation
            DatabaseOperation.CLEAN_INSERT.execute(databaseConnection, insertedDataSet);

            IDataSet actualDataSet = databaseConnection.createDataSet();

            // JUnit 5 Assersions
            Assertions.assertEquals(2, databaseConnection.getRowCount("book"));
            // DbUnit Assertion
            Assertion.assertEquals(insertedDataSet, actualDataSet);
        } finally {
            if (databaseConnection != null) {
                databaseConnection.close();
            }
        }
    }
}

先ほどのテストコードと同様、1回目にテーブルをdrop & createすることにします。

    @BeforeAll
    public static void setUpAll() {
        BookDao bookDao = new BookDaoImpl(DomaConfig.singleton());
        DomaConfig
                .singleton()
                .getTransactionManager()
                .required(() -> bookDao.createTableIfExistsRecreate());
    }

XMLファイルでのデータセットも用意。

src/test/resources/DbUnitOperationTest.test.xml

<?xml version="1.0" encoding="UTF-8"?>
<dataset>
    <book isbn="978-4797397642" title="本気で学ぶ Linux実践入門 サーバ運用のための業務レベル管理術" price="3278" publish_date="2019-05-30" category="linux"/>
    <book isbn="978-4798155760" title="Ubuntuサーバー徹底入門" price="4180" publish_date="2018-06-13" category="linux"/>
</dataset>

DatabaseOperationクラスを使ったデータセットの操作に絞って見ていきます。

まずは、DatabaseOperation.INSERTで用意したXMLファイルでのデータセットの内容をinsertします。

            // INSERT Operation
            IDataSet insertedDataSet =
                    new FlatXmlDataSetBuilder()
                            .build(getClass().getClassLoader().getResourceAsStream("DbUnitOperationTest.test.xml"));

            DatabaseOperation.INSERT.execute(databaseConnection, insertedDataSet);
            // JUnit 5 Assersions
            Assertions.assertEquals(2, databaseConnection.getRowCount("book"));

DatabaseConnection#getRowCountを使うと、対象のテーブルのレコード数を取得できます。

ここでは、DatabaseOperation.INSERT#executeメソッド実行後に、用意したXMLファイルに含まれるデータ件数、つまり2レコードが
存在していることが確認できました。

次に、Doma 2を使ってデータを登録。

            DomaConfig
                    .singleton()
                    .getTransactionManager()
                    .required(() -> {
                        BookDao bookDao = new BookDaoImpl(DomaConfig.singleton());

                        List<Book> books = List.of(
                                Book.create("978-4295008477", "新世代Javaプログラミングガイド[Java SE 10/11/12/13と言語拡張プロジェクト]", 2860, LocalDate.of(2020, 3, 13), "java"),
                                Book.create("978-4621303252", "Effective Java 第3版", 4400, LocalDate.of(2018, 10, 30), "java"),
                                Book.create("978-4774189093", "Java本格入門 〜モダンスタイルによる基礎からオブジェクト指向・実用ライブラリまで", 3278, LocalDate.of(2017, 4, 18), "java"),
                                Book.create("978-4798161488", "MySQL徹底入門 第4版 MySQL 8.0対応", 4180, LocalDate.of(2020, 7, 6), "mysql"),
                                Book.create("978-4798147406", "詳解MySQL 5.7 止まらぬ進化に乗り遅れないためのテクニカルガイド", 3960, LocalDate.of(2016, 8, 26), "mysql")
                        );

                        books.forEach(bookDao::insert);
                    });

            Assertions.assertEquals(7, databaseConnection.getRowCount("book"));

テーブル内には7件のデータがあることが確認できます。

DatabaseOperation.REFRESHを使うと、対象のデータセットを使ってupdate、データがなければinsertを行います。

            // REFRESH(update or insert) Operation
            IDataSet refreshedDataSet =
                    new FlatXmlDataSetBuilder()
                            .build(getClass().getClassLoader().getResourceAsStream("DbUnitOperationTest.test.xml"));

            DatabaseOperation.REFRESH.execute(databaseConnection, refreshedDataSet);
            // JUnit 5 Assersions
            Assertions.assertEquals(7, databaseConnection.getRowCount("book"));
            ITable categories = databaseConnection.createQueryTable("result", "select category from book group by category order by category");
            Assertions.assertEquals("java", categories.getValue(0, "category"));
            Assertions.assertEquals("linux", categories.getValue(1, "category"));
            Assertions.assertEquals("mysql", categories.getValue(2, "category"));

よって、データの数は変わっていません。

最後にDatabaseOperation.CLEAN_INSERT。こちらは対象のデータセットの範囲を全件削除した後、指定のデータセットのデータを
登録します。

            // CLEAN_INSERT(delete all + insert) Operation
            DatabaseOperation.CLEAN_INSERT.execute(databaseConnection, insertedDataSet);

            IDataSet actualDataSet = databaseConnection.createDataSet();

            // JUnit 5 Assersions
            Assertions.assertEquals(2, databaseConnection.getRowCount("book"));
            // DbUnit Assertion
            Assertion.assertEquals(insertedDataSet, actualDataSet);

よって、今回Doma 2を使って登録したデータは削除され、XMLファイルで用意した2件だけが残っている状態になります。

これらの操作は、このように独立したクラスやその組み合わせで実現されています。

    /** @see DummyOperation */
    public static final DatabaseOperation NONE = new DummyOperation();
    /** @see UpdateOperation */
    public static final DatabaseOperation UPDATE = new UpdateOperation();
    /** @see InsertOperation */
    public static final DatabaseOperation INSERT = new InsertOperation();
    /** @see RefreshOperation */
    public static final DatabaseOperation REFRESH = new RefreshOperation();
    /** @see DeleteOperation */
    public static final DatabaseOperation DELETE = new DeleteOperation();
    /** @see DeleteAllOperation */
    public static final DatabaseOperation DELETE_ALL = new DeleteAllOperation();
    /** @see TruncateTableOperation */
    public static final DatabaseOperation TRUNCATE_TABLE = new TruncateTableOperation();
    /**
     * @see DeleteAllOperation
     * @see InsertOperation
     * @see CompositeOperation
     */
    public static final DatabaseOperation CLEAN_INSERT = new CompositeOperation(
            DELETE_ALL, INSERT);

https://sourceforge.net/p/dbunit/code.git/ci/dbunit-2.7.3/tree/src/main/java/org/dbunit/operation/DatabaseOperation.java

各DatabaseOperationクラスの実装が気になるのであれば、ドキュメントの説明およびソースコードを見ると良いでしょう。

Core Components / DatabaseOperation

https://sourceforge.net/p/dbunit/code.git/ci/dbunit-2.7.3/tree/src/main/java/org/dbunit/operation/

その他

今回は扱いませんでした、IDatabaseTesterインターフェースというここまで見てきたような操作をある程度まとめられるクラスも
存在します。実装としてはJdbcDatabaseTesterクラスやDataSourceDatabaseTesterクラスなどを使うのかなと思います。

Getting Started

本来はDBTestCaseクラスを継承できない場合に使うようなのですが、DBTestCaseクラスは古いJUnit向けのものなので、現在は
使うことはないでしょう。

IDatabaseTesterインターフェースの実装クラスを使うか、自分で作成したりしてこのあたりの処理はうまく扱えるように考えて
いくんでしょうね。

まとめ

DbUnitを試してみました。

最初はWebサイトを見て「更新されていないのかな?」と思ったのですが、現在もJUnit 5には対応していないとはいえメンテナンスは
続いているようですし、使い方を押さえておいてもよいかなと思いました。

今回は、こんなところで。