これは、なにをしたくて書いたもの?
以前、DbUnitを使ってエントリーを書いてみました。
この時にも少し触れていたのですが、今回はDatabase Riderというものを試してみたいと思います。
Database Rider
Database Riderは、DbUnitとJUnitを統合してデータベースのテストをしやすくするものです。
Database Rider integrates DBUnit and JUnit in order to make database testing a breeze!
機能としては、以下のようですね。
- アノテーションを使ってJUnit Ruleと統合
- インターセプターによるCDI統合
- JSON、YAML、XML、XLS、CSVデータフォーマットをサポート
- アノテーションまたはYAMLファイルによる構成
- Cucumberとの統合
- 複数のデータベースのサポート
- 日付/時刻のサポート
- GroovyやJavaScriptを使ったスクリプトでのデータセットのサポート
- 期待するデータセットへの正規表現サポート
- JUnit 5との統合
- データセットのエクスポート
- コネクションリークの検出
- 多くのサンプル
ドキュメントは、以下から最新のものを参照します。
現時点で公開されている最新のドキュメントは、1.35.0のようです(一覧にはありませんでしたが、URLからバージョン指定すると参照
できました)。
Getting Startedはこちら。
Database Rider Getting Started
GitHub - database-rider/database-rider: Database testing made easy!
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
ドキュメント本体の方も、必要に応じて参照します。
環境
今回の環境は、こちら。
$ 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)
が要ると書いていましたが、こちらはなくても良さそうですけどね。
なお、@DBRider
というアノテーションを使っても良さそうです(中身は@DBUnitExtension
と@Test
の合成)。
import com.github.database.rider.junit5.api.DBRider; @DBRider public class DatabaseRiderGettingStartedTest {
データの例。
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の実装的には、こちらのクラスを見ると良さそうな感じでした。
まとめ
Database Riderを使ってみました。
背後にDbUnitがあるのですが、かなり簡単に使えるのではないかなと思います。DbUnitを直接使うよりは、こちらの方が良さそうです。
これからは、Database Riderを使ってデータベース関係のテストを行うのも十分ありですね。