SpringBootでDBUnitを使用したJunitを作成する

SpringBootアプリケーションで、DBUnitを使用したJunitの作成方法を記述する。
DBはH2SQLServerモードを使用し、O/R MapperはDomaを使用する。

依存関係の追加

build.gradle にDBUnitを使用するために必要な依存関係を追加する。
テスト用データをエクセルから投入する場合はpoiの依存関係も追加する。

dependencies {
    // dbunit
    testImplementation "org.dbunit:dbunit:${dbunit_version}"
    // poi
    testImplementation "org.apache.poi:poi:${poi_version}"
    // poi-ooxml
    testImplementation "org.apache.poi:poi-ooxml:${poi_version}"
    // spring-test-dbunit
    testImplementation "com.github.springtestdbunit:spring-test-dbunit:${spring_test_dbunit_version}"
 }

バージョン番号は gradle.properties に記述する。

# DBUnit
dbunit_version = 2.7.2
# spring-test-dbunit
spring_test_dbunit_version = 1.3.0
# poi
poi_version = 5.0.0

テストの作成:DBUnitを使用するための設定

エクセルファイルのローダークラス作成

テストの作成に先立ち、エクセルファイルを読み込むためのLoaderクラスを作成する必要がある。

import java.io.IOException;
import java.io.InputStream;

import org.dbunit.dataset.DataSetException;
import org.dbunit.dataset.IDataSet;
import org.dbunit.dataset.excel.XlsDataSet;

import org.springframework.core.io.Resource;
import com.github.springtestdbunit.dataset.AbstractDataSetLoader;

public class XlsDataSetLoader extends AbstractDataSetLoader {
    @Override
    protected IDataSet createDataSet(Resource resource) throws IOException, DataSetException {
        try (InputStream inputStream = resource.getInputStream()) {
            return new XlsDataSet(inputStream);
        }
    }
}

DBUnitではCSVファイルも使用することができるが、エクセルファイルの場合と同様にLoaderクラスを作成する必要がある。
その際は上記のLoaderクラス内でreturnしているクラスをorg.dbunit.dataset.csv.CsvDataSetとすればよい。

なお、XMLファイルを使用する場合はLoaderクラスを作成する必要はない。

DB接続設定

src/test/resourcesにapplication.ymlを作成し、DBの接続先の設定などを行う。
以下はH2の設定例である。

spring:
    # データベースの接続設定
    datasource:
      driverClassName: org.h2.Driver
      url: jdbc:h2:~/test;MODE=MSSQLServer
      username: sa
      password:
      initialization-mode: always
      # 初期化に使用するSQL
      schema: classpath:schema.sql
      # コネクションプールの設定
      hikari:
        connection-timeout: 60000
        validationTimeout: 30000
        maximum-pool-size: 2
        autoCommit: false
    transaction:
        rollback-on-commit-failure: true

テストクラスの作成

テストクラスに以下のようにアノテーションを付与する。各アノテーションについては後述する。

@SpringBootTest(classes = DbUnitSampleTest.Config.class, webEnvironment = WebEnvironment.NONE)@TestExecutionListeners({
    DependencyInjectionTestExecutionListener.class,
    DirtiesContextTestExecutionListener.class,
    TransactionDbUnitTestExecutionListener.class})
@DbUnitConfiguration(dataSetLoader = XlsDataSetLoader.class)
@Transactional
class DbUnitSampleTest {
}

@SpringBootTest

@SpringBootTest(classes = DbUnitSampleTest.Config.class, webEnvironment = WebEnvironment.NONE)

Spring Bootベースのテストを実行する。classes属性にテストで使用するConfigurationクラスを指定する。
テストに不要なクラスまでbean登録しないようにするためにテストクラス内にConfigurationクラスを準備する。

@TestExecutionListeners

@TestExecutionListeners({
    DependencyInjectionTestExecutionListener.class,
    DirtiesContextTestExecutionListener.class,
    TransactionDbUnitTestExecutionListener.class})

テスト実行時のリスナーを追加する。上記の記述例で追加しているリスナーは以下の通り。

クラス名 概要
DependencyInjectionTestExecutionListener テストで使用するインスタンスへのDI機能を提供する。
DirtiesContextTestExecutionListener テストで使用するDIコンテナのライフサイクル管理機能を提供する。
TransactionDbUnitTestExecutionListener @DatabaseSetupや@ExpectedDatabaseなどを使えるようにする。

@DbUnitConfiguration

@DbUnitConfiguration(dataSetLoader = XlsDataSetLoader.class)

DBUnitでエクセルファイルを使用できるように設定する。
属性dataSetLoaderに作成したXlsDataSetLoaderクラスを指定する。

@Transactional

@Transactional

@DatabaseSetupで投入するデータをテスト処理と同じトランザクション制御とするために付与する。
テストクラスに付与することで、テスト後に投入データもロールバックできるようになる。

テスト用Configurationクラスの作成

テストクラス内部にテスト用のConfigurationクラスを作成する。
Configurationクラスでは、テストに必要なクラスをbean登録する処理などを記述する。

@SpringBootTest(classes = DbUnitSampleTest.Config.class, webEnvironment = WebEnvironment.NONE)
@TestExecutionListeners({
    DependencyInjectionTestExecutionListener.class,
    DirtiesContextTestExecutionListener.class,
    TransactionDbUnitTestExecutionListener.class})
@DbUnitConfiguration(dataSetLoader = XlsDataSetLoader.class)
@Transactionalclass 
DbUnitSampleTest {

    @JdbcTest
    @Import({ DomaAutoConfiguration.class})
    @ComponentScan(basePackages = { "jp.co.sample.service" })
    static class Config {
        // テストに必要なクラスをbean登録する処理など
    }
}

ここまででDBUnitをSpringBootで使用するための設定が完了となる。
以降は事前データや期待値データを記述するエクセルファイルの作成方法と、実際に簡単なテストメソッドを作成した例を示す。


エクセルデータの作成方法

エクセルファイルを使用した事前データや、期待値データの作成方法について説明する。

エクセルファイルの作成

エクセルファイルはsrc/test/resources配下の任意の場所に任意のファイル名で作成する。

対象テーブルとカラム名を指定する

エクセルのシート名に対象となるテーブル名を指定する。
テーブル名は大文字でも、小文字でも、大文字小文字混在でも構わないが、正しいテーブル名を指定しないとテスト実行時に対象テーブルが見つからずエラーとなるので注意する。

エクセルの1行目には対象テーブルのカラム名を指定する。
カラム名もテーブル名と同様で小文字でも、大文字小文字混在でも構わないが、正しいカラム名を指定しないとテスト実行時にエラーとなるので注意する。
カラム名を入力する列の書式は「文字列」とする。

以下のDDLで作成されるテーブルを対象にエクセルファイルを作成した例を示す。

CREATE TABLE IF NOT EXISTS goods (
    id int IDENTITY(0,1) NOT NULL,
    name nvarchar(20)  NULL,
    price decimal(9,0) NULL,
    create_date datetime2(7) NULL,
    create_id varchar(128)  NULL,
    update_date datetime2(7) NULL,
    update_id varchar(128)  NULL,
    CONSTRAINT PK_goods PRIMARY KEY (id));

nullや空文字の設定方法

エクセルファイル内でデータにnullを設定する場合は、nullにしたいカラムのセルを空欄にする。
空文字を設定する場合は、セルに「’(シングルクォーテーション)」を設定する。

以下に記述例を示す。B3セルには空文字、B4セルにはnullを設定している。

DBUnitではデフォルトの設定では空文字を設定することが出来ない。
空文字の設定を許容する場合は、テストクラス内に以下の記述を追加する。

@Configuration
static class DbunitConfig {
    @Bean
    public DatabaseConfigBean dbUnitDatabaseConfig() {
        DatabaseConfigBean bean = new DatabaseConfigBean();
        bean.setAllowEmptyFields(true);
        return bean;
    }

    @Bean
    public DatabaseDataSourceConnectionFactoryBean dbUnitDatabaseConnection(DatabaseConfigBean dbUnitDatabaseConfig,
            DataSource dataSource) {
        DatabaseDataSourceConnectionFactoryBean bean = new DatabaseDataSourceConnectionFactoryBean(dataSource);
        bean.setDatabaseConfig(dbUnitDatabaseConfig);
        return bean;
    }
}

日付カラムの記述方法

日時はYY-MM-dd HH:mm:ss.SSSの書式となるように記述する。
以下に記述例を示す。D3セルのように日付のみを指定することもできる。

複数テーブルの記述方法

エクセルファイルで複数テーブルのデータを記述する場合は、テーブルごとにシートを分ける。
これまで記述例で使用してきた goods テーブルに加えて以下のDDLで作成されるテーブルのデータを記述する例を示す。

CREATE TABLE IF NOT EXISTS employees (
    id int IDENTITY(0,1) NOT NULL,
    dept_id int NULL,
    name nvarchar(20) NULL,
    CONSTRAINT PK_employees PRIMARY KEY (id));

CREATE TABLE IF NOT EXISTS depts (
    dept_id int NOT NULL,
    dept_name   nvarchar(40) NOT NULL,
    CONSTRAINT PK_depts PRIMARY KEY (dept_id));

テストクラスでエクセルを読み込む方法

エクセルファイルの読み込みにはアノテーションを使用する。

テスト実行前の事前データを投入する際には以下のアノテーションをクラスに付与する。 この場合、テストメソッド実行前に毎回データが投入される。

アノテーション 概要
@DatabaseSetup テスト実行前の事前データを読み込む。属性にsrc/test/resources以降のパスを指定する。
@DatabaseSetups テスト実行前の事前データを読み込む。属性に@DatabaseSetupを複数定義する。
@DatabaseSetup("/data/sampledata.xlsx")
class DbUnitSampleTest {}

@DatabaseSetups(value = { @DatabaseSetup("/data/sampledata.xlsx"), @DatabaseSetup("/data/sample.xlsx") })
class DbUnitSampleTest {}

テスト実行後の期待値データを投入する際には以下のアノテーションをクラスに付与する。

アノテーション 概要
@ExpectedDatabase テスト実行後の期待値データを読み込む。属性にsrc/test/resources以降のパスを指定する。
@ExpectedDatabases テスト実行後の期待値データを読み込む。属性に@ExpectedDatabaseを複数定義する。
@ExpectedDatabase("/data/sampledata.xlsx")
class DbUnitSampleTest {}

@ExpectedDatabases(value = { @ExpectedDatabase("/data/sampledata.xlsx"), @ExpectedDatabase("/data/sample.xlsx") })
class DbUnitSampleTest {}

テストメソッドごとに異なるエクセルを読み込む方法

テストメソッドごとに異なるエクセルファイルを読み込む場合は、テストメソッドに@DatabaseSetup@ExpectedDatabaseなどのアノテーションを付与する。
クラスにもアノテーションが付与されている場合でも、テストメソッドに付与されたアノテーションの属性に指定されたエクセルファイルのデータのみが読み込まれる。

@Test@DatabaseSetup("/data/sample.xlsx")void test() {}

テストの作成:テストメソッドの作成

簡単なSELECT, INSERT, UPDATE, DELETEのテストの作成例を示す。
テストに使用するDaoとEntityは以下の通り。

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.Update;
import org.seasar.doma.boot.ConfigAutowireable;

@ConfigAutowireable
@Dao
public interface GoodsDao {
    /** select */
    @Select
    List<Goods> selectByGoods(Goods dto);

    /** insert */
    @Insert
    int insert(Goods goods);

    /** update */
    @Update
    int update(Goods goods);

    /** delete */
    @Delete
    int delete(Goods goods);
}
import java.time.LocalDateTime;

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

import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;

@Entity
@Data
@EqualsAndHashCode
@ToString
public class Goods {
    /** 主キー(自動採番) */
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;

    /** 名前 */
    private String name;

    /** 値段 */
    private double price;

    /** 登録日時 */
    private LocalDateTime createDate;

    /** 登録ID */
    private String createId;

    /** 更新日時 */
    private LocalDateTime updateDate;

    /** 更新ID */
    private String updateId;
}

テストに使用する事前データのエクセルファイルは以下の通り。

SELECT

@Test
void select_検索結果が取得できること() {
    Goods goods = new Goods();
    goods.setName("筆箱");

    List<Goods> resultList = this.service.selectByGoods(goods);
    assertEquals(1, resultList.size());

    Goods result = resultList.get(0);
    assertEquals(3, result.getId());
    assertEquals("筆箱", result.getName());
    assertEquals(500, result.getPrice());
    assertEquals(LocalDateTime.of(2021, 10, 31, 12, 15, 30, 500000000), result.getCreateDate());
    assertEquals("AA001", result.getCreateId());
    assertNull(result.getUpdateDate());
    assertNull(result.getUpdateId());
}

INSERT

@Test
void insert_データが1件登録されること() {
    List<Goods> beforeList = this.service.selectByGoods(new Goods());
    // insert実行前は3件データが登録されている
    assertEquals(3, beforeList.size());

    Goods goods = new Goods();
    goods.setName("万年筆");
    goods.setPrice(5000);
    goods.setCreateId("BB001");
    goods.setCreateDate(LocalDateTime.of(2021, 11, 11, 11, 11, 11));

    int insert = this.service.insert(goods);
    assertEquals(1, insert);

    List<Goods> goodsList = this.service.selectByGoods(new Goods());
    // insert実行後は4件データが登録されている
    assertEquals(4, goodsList.size());

    // 登録内容の確認
    Goods goods01 = goodsList.get(0);
    assertEquals(1, goods01.getId());
    assertEquals("消しゴム", goods01.getName());
    assertEquals(100, goods01.getPrice());
    assertEquals(LocalDateTime.of(2021, 10, 31, 12, 15, 30, 500000000), goods01.getCreateDate());
    assertEquals("AA001", goods01.getCreateId());
    assertNull(goods01.getUpdateDate());
    assertNull(goods01.getUpdateId());

    Goods goods02 = goodsList.get(1);
    assertEquals(2, goods02.getId());
    assertEquals("鉛筆", goods02.getName());
    assertEquals(60, goods02.getPrice());
    assertEquals(LocalDateTime.of(2021, 10, 31, 12, 15, 30, 500000000), goods02.getCreateDate());
    assertEquals("AA001", goods02.getCreateId());
    assertNull(goods02.getUpdateDate());
    assertNull(goods02.getUpdateId());

    Goods goods03 = goodsList.get(2);
    assertEquals(3, goods03.getId());
    assertEquals("筆箱", goods03.getName());
    assertEquals(500, goods03.getPrice());
    assertEquals(LocalDateTime.of(2021, 10, 31, 12, 15, 30, 500000000), goods03.getCreateDate());
    assertEquals("AA001", goods03.getCreateId());
    assertNull(goods03.getUpdateDate());
    assertNull(goods03.getUpdateId());

    Goods goods04 = goodsList.get(3);
    assertEquals(4, goods04.getId());
    assertEquals("万年筆", goods04.getName());
    assertEquals(5000, goods04.getPrice());
    assertEquals(LocalDateTime.of(2021, 11, 11, 11, 11, 11), goods04.getCreateDate());
    assertEquals("BB001", goods04.getCreateId());
    assertNull(goods04.getUpdateDate());
    assertNull(goods04.getUpdateId());
}

UPDATE

@Test
void update_データの内容が変更されること() {
    Goods goods = new Goods();
    goods.setName("筆箱");

    List<Goods> beforeList = this.service.selectByGoods(goods);
    assertEquals(1, beforeList.size());

    // 変更前のデータを確認する
    Goods result = beforeList.get(0);
    assertEquals(3, result.getId());
    assertEquals("筆箱", result.getName());
    assertEquals(500, result.getPrice());
    assertEquals(LocalDateTime.of(2021, 10, 31, 12, 15, 30, 500000000), result.getCreateDate());
    assertEquals("AA001", result.getCreateId());
    assertNull(result.getUpdateDate());
    assertNull(result.getUpdateId());

    result.setPrice(1000);
    result.setUpdateDate(LocalDateTime.of(2021, 11, 11, 10, 20, 30));
    result.setUpdateId("BB001");
    int update = this.service.update(result);
    assertEquals(1, update);

    List<Goods> updatetList = this.service.selectByGoods(goods);
    assertEquals(1, updatetList.size());

    // 変更後のデータを確認する
    Goods updateGoods = updatetList.get(0);
    assertEquals(3, updateGoods.getId());
    assertEquals("筆箱", updateGoods.getName());
    assertEquals(1000, updateGoods.getPrice());
    assertEquals(LocalDateTime.of(2021, 10, 31, 12, 15, 30, 500000000), updateGoods.getCreateDate());
    assertEquals("AA001", updateGoods.getCreateId());
    assertEquals(LocalDateTime.of(2021, 11, 11, 10, 20, 30), updateGoods.getUpdateDate());
    assertEquals("BB001", updateGoods.getUpdateId());
}

DELETE

@Test
void delete_データが1件削除されること() {
    List<Goods> beforeList = this.service.selectByGoods(new Goods());
    // delete実行前は3件データが登録されている
    assertEquals(3, beforeList.size());

    Goods goods = new Goods();
    goods.setId(1);
    int delete = this.service.delete(goods);
    assertEquals(1, delete);

    List<Goods> goodsList = this.service.selectByGoods(new Goods());
    // delete実行後はデータは2件となる
    assertEquals(2, goodsList.size());

    // 登録されているデータに削除対象のものが残っていないことを確認する
    Goods goods01 = goodsList.get(0);
    assertEquals(2, goods01.getId());
    assertEquals("鉛筆", goods01.getName());
    assertEquals(60, goods01.getPrice());
    assertEquals(LocalDateTime.of(2021, 10, 31, 12, 15, 30, 500000000), goods01.getCreateDate());
    assertEquals("AA001", goods01.getCreateId());
    assertNull(goods01.getUpdateDate());
    assertNull(goods01.getUpdateId());

    Goods goods02 = goodsList.get(1);
    assertEquals(3, goods02.getId());
    assertEquals("筆箱", goods02.getName());
    assertEquals(500, goods02.getPrice());
    assertEquals(LocalDateTime.of(2021, 10, 31, 12, 15, 30, 500000000), goods02.getCreateDate());
    assertEquals("AA001", goods02.getCreateId());
    assertNull(goods02.getUpdateDate());
    assertNull(goods02.getUpdateId());
}