CLOVER🍀

That was when it all began.

Database Riderを試してみる

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

以前、DbUnitを使ってエントリーを書いてみました。

DbUnitを試してみる - CLOVER🍀

この時にも少し触れていたのですが、今回はDatabase Riderというものを試してみたいと思います。

Database Rider

Database Riderは、DbUnitとJUnitを統合してデータベースのテストをしやすくするものです。

Database Rider integrates DBUnit and JUnit in order to make database testing a breeze!

Database Rider

機能としては、以下のようですね。

ドキュメントは、以下から最新のものを参照します。

Document

現時点で公開されている最新のドキュメントは、1.35.0のようです(一覧にはありませんでしたが、URLからバージョン指定すると参照
できました)。

Database Rider Documentation

Getting Startedはこちら。

Database Rider Getting Started

GitHubリポジトリはこちら。

GitHub - database-rider/database-rider: Database testing made easy!

サンプルは、GitHubリポジトリに含まれています。

https://github.com/database-rider/database-rider/tree/1.35.0/rider-examples

以下のようなサンプルがあります。

  • dbunit-tomee-appcomposer-sample
  • jOOQ-DBUnit-flyway-example
  • jpa-productivity-boosters
  • quarkus-dbunit-sample
  • quarkus-postgres-sample
  • rider-kotlin
  • spring-boot-dbunit-sample

とりあえずこれくらいにして、使っていってみましょう。

お題

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

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

参考にするドキュメントは、まずはGetting Startedにしたいと思います。

Database Rider Getting Started

ドキュメント本体の方も、必要に応じて参照します。

Database Rider Documentation

環境

今回の環境は、こちら。

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


$ mvn --version
Apache Maven 3.8.6 (84538c9988a25aec085021c365c560670ad80f63)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 17.0.4, 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-131-generic", arch: "amd64", family: "unix"

MySQLは、172.17.0.2で動作しているものとします。

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


mysql> select version();
+-----------+
| version() |
+-----------+
| 8.0.31    |
+-----------+
1 row in set (0.00 sec)

データベースはpractice、アカウントはkazuhira/passwordで作成しているものとします。

準備

Maven依存関係など。

    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <version>8.0.31</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.seasar.doma</groupId>
            <artifactId>doma-core</artifactId>
            <version>2.53.1</version>
        </dependency>

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

        <dependency>
            <groupId>com.github.database-rider</groupId>
            <artifactId>rider-junit5</artifactId>
            <version>1.35.0</version>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.jupiter</groupId>
                    <artifactId>*</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.junit.platform</groupId>
                    <artifactId>*</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter</artifactId>
            <version>5.9.1</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.assertj</groupId>
            <artifactId>assertj-core</artifactId>
            <version>3.23.1</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

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

ポイントは、こちらですね。

        <dependency>
            <groupId>com.github.database-rider</groupId>
            <artifactId>rider-junit5</artifactId>
            <version>1.35.0</version>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.jupiter</groupId>
                    <artifactId>*</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.junit.platform</groupId>
                    <artifactId>*</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

Database Riderのアーティファクトのうち、JUnit 5向けのrider-junit5を使っているところですね。

依存しているJUnit 5が古かったので、excludeしてJUnit 5は自分で追加しました。

テーブルは、以下の定義で作成。

create table book(
  isbn varchar(14),
  title varchar(100),
  price int,
  publish_date date,
  category varchar(20),
  primary key(isbn)
);

続いて、Doma 2を使ったプログラムを書いていきます。

エンティティ。

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

package org.littlewings.databaserider;

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.setPublishDate(publishDate);
        book.setCategory(category);

        return book;
    }

    // getter/setterは省略
}

Dao。

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

package org.littlewings.databaserider;

import java.util.List;

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

@Dao
public interface BookDao {
    @Insert
    int insert(Book book);

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

    @Sql("select /*%expand*/* from book order by price desc")
    @Select
    List<Book> selectAllOrderByPriceDesc();

    @Delete
    int delete(Book book);
}

Config。

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

package org.littlewings.databaserider;

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&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;
    }
}

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

Database Riderを使ってみる

では、Database Riderを使ってみましょう。

まずは、テストコードの雛形を用意。

src/test/java/org/littlewings/databaserider/DatabaseRiderGettingStartedTest.java

package org.littlewings.databaserider;

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

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.DataSetFormat;
import com.github.database.rider.core.api.dataset.ExpectedDataSet;
import com.github.database.rider.core.api.exporter.ExportDataSet;
import com.github.database.rider.junit5.DBUnitExtension;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

import static org.assertj.core.api.Assertions.assertThat;

@ExtendWith(DBUnitExtension.class)
public class DatabaseRiderGettingStartedTest {

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

こちらを見ながら進めていきます。

Database Rider Getting Started

先に、JUnit 5向けの設定から。

@ExtendWith(DBUnitExtension.class)
public class DatabaseRiderGettingStartedTest {

Database Rider Getting Started / Riding database in JUnit 5 tests

Getting Startedなどを見ていると@RunWith(JUnitPlatform.class)が要ると書いていましたが、こちらはなくても良さそうですけどね。

データの例。

Database Rider Getting Started / Example

今回は、こんなデータを用意しました。

src/test/resources/dataset/books.yml

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

こちらを、@DataSetアノテーションで指定します。クラスパス上のファイルを指定する、で良さそうです。

    @Test
    @DataSet("dataset/books.yml")
    public void gettingStarted() {
        BookDao bookDao = new BookDaoImpl(DomaConfig.singleton());

        DomaConfig
                .singleton()
                .getTransactionManager()
                .required(() -> {
                    Book mysqlBook = bookDao.selectByIsbn("978-4798161488");
                    assertThat(mysqlBook).isNotNull();
                    assertThat(mysqlBook.getTitle()).isEqualTo("MySQL徹底入門 第4版 MySQL 8.0対応");
                    assertThat(mysqlBook.getPrice()).isEqualTo(4180);
                    assertThat(mysqlBook.getPublishDate()).isEqualTo(LocalDate.of(2020, 7, 6));
                    assertThat(mysqlBook.getCategory()).isEqualTo("mysql");

                    List<Book> books = bookDao.selectAllOrderByPriceDesc();
                    assertThat(books).hasSize(5);
                    assertThat(books.stream().map(Book::getPrice).toList()).containsExactly(4400, 4180, 3960, 3278, 2860);
                });
    }

@DataSetアノテーションは、データをセットアップするアノテーションです。

Database Rider Getting Started / Configuration

デフォルトでは、指定したDataSetの投入先のテーブルを1度全件削除してからデータを投入する動きになるようです。これはstrategyで
指定します。

deleteの順を指定したり、テストの前後にデータベース内のデータを削除したり、テストの前後にSQLやスクリプトを実行することも
できるようです。

今回は、@DataSetアノテーションで登録されたデータをアサーションするソースコードになっています。

        DomaConfig
                .singleton()
                .getTransactionManager()
                .required(() -> {
                    Book mysqlBook = bookDao.selectByIsbn("978-4798161488");
                    assertThat(mysqlBook).isNotNull();
                    assertThat(mysqlBook.getTitle()).isEqualTo("MySQL徹底入門 第4版 MySQL 8.0対応");
                    assertThat(mysqlBook.getPrice()).isEqualTo(4180);
                    assertThat(mysqlBook.getPublishDate()).isEqualTo(LocalDate.of(2020, 7, 6));
                    assertThat(mysqlBook.getCategory()).isEqualTo("mysql");

                    List<Book> books = bookDao.selectAllOrderByPriceDesc();
                    assertThat(books).hasSize(5);
                    assertThat(books.stream().map(Book::getPrice).toList()).containsExactly(4400, 4180, 3960, 3278, 2860);
                });

ところで、接続先に関する情報がまだ出てきていません。データベース接続は、ConnectionHolderというものを使って定義するようです。

@ExtendWith(DBUnitExtension.class)
public class DatabaseRiderGettingStartedTest {
    ConnectionHolder connectionHolder = () ->
            DriverManager.getConnection(
                    "jdbc:mysql://172.17.0.2:3306/practice?characterEncoding=utf-8&connectionCollation=utf8mb4_0900_bin",
                    "kazuhira",
                    "password"
            );

ただ、今回はこちらを使わずにDatabase Riderの設定ファイルで定義することにしました。

Database Rider Getting Started / Configuration

dbunit.ymlというファイルですね(@DBUnitアノテーションでもよいみたいですが)。

src/test/resources/dbunit.yml

properties:
  caseSensitiveTableNames: true
connectionConfig:
  url: "jdbc:mysql://172.17.0.2:3306/practice?characterEncoding=utf-8&connectionCollation=utf8mb4_0900_bin"
  user: "kazuhira"
  password: "password"

デフォルトだとcaseSensitiveTableNamesがfalseなので、データセット内のテーブル名やカラム名の大文字小文字が一致しないと
ダメなのですが、今回はtrueにしました。

次は、アサーションもDatabase Riderに任せる方法を試してみます。@ExpectedDataSetアノテーションを使うようです。

Database Rider Getting Started / Database assertion with ExpectedDataSet

@ExpectedDataSetアノテーションを使うと、テスト実行後のテーブルの状態と@ExpectedDataSetアノテーションで指定したデータセットの
内容をアサーションしてくれます。

    @Test
    @DataSet("dataset/books.yml")
    @ExpectedDataSet(value = "dataset/expectedBooks.yml", orderBy = "isbn", ignoreCols = "price")
    public void expectedDataSet() {
        BookDao bookDao = new BookDaoImpl(DomaConfig.singleton());

        DomaConfig
                .singleton()
                .getTransactionManager()
                .required(() -> {
                    assertThat(
                            bookDao
                                    .delete(
                                            Book.create("978-4774189093", "Java本格入門 〜モダンスタイルによる基礎からオブジェクト指向・実用ライブラリまで", 3278, LocalDate.of(2017, 4, 18), "java")
                                    )
                    ).isEqualTo(1);

                    bookDao.insert(Book.create("978-1492080510", "High Performance MySQL: Proven Strategies for Operating at Scale", 6312, LocalDate.of(2021, 12, 24), "mysql"));

                    List<Book> books = bookDao.selectAllOrderByPriceDesc();
                    assertThat(books).hasSize(5);
                });
    }

@DataSetアノテーションでデータをロードし、テスト内でデータの削除・追加をして、@ExpectedDataSetでアサーションします。

用意したファイルは、こんな感じです。

src/test/resources/dataset/expectedBooks.yml

book:
  - isbn: "978-4295008477"
    title: "新世代Javaプログラミングガイド[Java SE 10/11/12/13と言語拡張プロジェクト]"
    price: 2860
    publish_date: "2020-03-13"
    category: "java"
  - isbn: "978-4621303252"
    title: "Effective Java 第3版"
    price: 4400
    publish_date: "2018-10-30"
    category: "java"
  - isbn: "978-4798161488"
    title: "MySQL徹底入門 第4版 MySQL 8.0対応"
    price: 4180
    publish_date: "2020-07-06"
    category: "mysql"
  - isbn: "978-4798147406"
    title: "詳解MySQL 5.7 止まらぬ進化に乗り遅れないためのテクニカルガイド"
    price: 3960
    publish_date: "2016-08-26"
    category: "mysql"
  - isbn: "978-1492080510"
    title: "High Performance MySQL: Proven Strategies for Operating at Scale"
    price: 1006312
    publish_date: "2021-12-24"
    category: "mysql"

データのアサーションの際には、orderByでソート順を指定した方が良さそうですね。

    @ExpectedDataSet(value = "dataset/expectedBooks.yml", orderBy = "isbn", ignoreCols = "price")

今回はisbnカラムでソートしました。これを指定していなくて、最初アサーションに失敗しましたね…。

また、比較対象にしないカラムをignoreColsで指定しています。これを確認するために、登録するデータと

                    bookDao.insert(Book.create("978-1492080510", "High Performance MySQL: Proven Strategies for Operating at Scale", 6312, LocalDate.of(2021, 12, 24), "mysql"));

アサーションするデータを微妙にずらしています。priceカラムですね。

  - isbn: "978-1492080510"
    title: "High Performance MySQL: Proven Strategies for Operating at Scale"
    price: 1006312
    publish_date: "2021-12-24"
    category: "mysql"

これで、アサーションに失敗するとどうなるか確認してみます。データの削除・登録するコードは残しつつ、アサーションで使用するファイルを
入力と同じにしてみます。

    @Test
    @DataSet("dataset/books.yml")
    @ExpectedDataSet(value = "dataset/books.yml", orderBy = "isbn", ignoreCols = "price")
    public void expectedDataSetFailure() {
        BookDao bookDao = new BookDaoImpl(DomaConfig.singleton());

        DomaConfig
                .singleton()
                .getTransactionManager()
                .required(() -> {
                    assertThat(
                            bookDao
                                    .delete(
                                            Book.create("978-4774189093", "Java本格入門 〜モダンスタイルによる基礎からオブジェクト指向・実用ライブラリまで", 3278, LocalDate.of(2017, 4, 18), "java")
                                    )
                    ).isEqualTo(1);

                    bookDao.insert(Book.create("978-1492080510", "High Performance MySQL: Proven Strategies for Operating at Scale", 6312, LocalDate.of(2021, 12, 24), "mysql"));

                    List<Book> books = bookDao.selectAllOrderByPriceDesc();
                    assertThat(books).hasSize(5);
                });
    }

結果。

org.dbunit.assertion.DbComparisonFailure: value (table=book, row=0, col=category) expected:<java> but was:<mysql>

ちょっとわかりにくい気もしますが…row(行番号)をヒントに探す感じでしょうね…。

最後に、データのエクスポートを行ってみます。プログラムで出力する方法と、@ExportDataSetアノテーションを使う方法があるようです。

Database Rider Getting Started / Exporting DataSets

今回は、@ExportDataSetアノテーションを使ってみます。

以下のテストメソッド実行時の全テーブル(といっても、テーブルはひとつだけですが)のデータをエクスポートします。

    @Test
    @ExportDataSet(format = DataSetFormat.YML, outputName = "target/exported/dataset/exportedAllTables.yml")
    public void exportAllTables() {

    }

フォーマットは、JSON、YAML、XML、XLS、CSVからの指定ですね。デフォルトはYAML(DataSetFormat.YML)です。

出力結果はこちら。

target/exported/dataset/exportedAllTables.yml

book:
  - isbn: "978-1492080510"
    title: "High Performance MySQL: Proven Strategies for Operating at Scale"
    price: 6312
    publish_date: "2021-12-24"
    category: "mysql"
  - isbn: "978-4295008477"
    title: "新世代Javaプログラミングガイド[Java SE 10/11/12/13と言語拡張プロジェクト]"
    price: 2860
    publish_date: "2020-03-13"
    category: "java"
  - isbn: "978-4621303252"
    title: "Effective Java 第3版"
    price: 4400
    publish_date: "2018-10-30"
    category: "java"
  - isbn: "978-4798147406"
    title: "詳解MySQL 5.7 止まらぬ進化に乗り遅れないためのテクニカルガイド"
    price: 3960
    publish_date: "2016-08-26"
    category: "mysql"
  - isbn: "978-4798161488"
    title: "MySQL徹底入門 第4版 MySQL 8.0対応"
    price: 4180
    publish_date: "2020-07-06"
    category: "mysql"

今回は、こんなところでしょうか。

Database Riderの実装的には、こちらのクラスを見ると良さそうな感じでした。

https://github.com/database-rider/database-rider/blob/1.35.0/rider-core/src/main/java/com/github/database/rider/core/dataset/DataSetExecutorImpl.java

まとめ

Database Riderを使ってみました。

背後にDbUnitがあるのですが、かなり簡単に使えるのではないかなと思います。DbUnitを直接使うよりは、こちらの方が良さそうです。

これからは、Database Riderを使ってデータベース関係のテストを行うのも十分ありですね。