これは、なにをしたくて書いたもの?
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.
データのエクスポート・インポートの機能を持ち、データベースが持つ値でのアサーションも行えます。
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.
なので、まずは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を取り巻くOSSとして、Spring Test向けのSpring Test DBUnitというものがありますが、こちらは更新が止まって
久しそう…。
Spring Test DBUnit – Introduction
Database Riderというものもあるので、今はこちらを見るのがよいのかもしれません。
DbUnitについて
DbUnitに影響を与えた、データベースのテストに関する話はこちら。
いわゆる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を構成する主なコンポーネントは、こちらに書かれています。
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
は対象となるデータセットをデータベースに反映するもので、データセットに含まれる範囲のデータのみを対象とし、
それ以外のデータには影響を与えません。内部的には、update
して更新されなければinsert
しようとします。
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上でですね。
用意したXMLファイルは、こちら。あらかじめ価格でソートしておきました。
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()); }
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);
各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
クラスなどを使うのかなと思います。
本来はDBTestCase
クラスを継承できない場合に使うようなのですが、DBTestCase
クラスは古いJUnit向けのものなので、現在は
使うことはないでしょう。
IDatabaseTester
インターフェースの実装クラスを使うか、自分で作成したりしてこのあたりの処理はうまく扱えるように考えて
いくんでしょうね。
まとめ
DbUnitを試してみました。
最初はWebサイトを見て「更新されていないのかな?」と思ったのですが、現在もJUnit 5には対応していないとはいえメンテナンスは
続いているようですし、使い方を押さえておいてもよいかなと思いました。
今回は、こんなところで。