SpringBootのテストでDBUnitを利用する

# 要旨
SpringBootプロジェクトのテストにdbunitを利用する。

#### 環境
* Java SE 8 (jdk-8u181)
* Spring Boot 2.1.1
* Spring-test-batch 1.3.0
* dbunit 2.6.0

# 前提

### テスト対象

* person

|カラム|型|主キー|
|---|---|---|
|person_id|integer|〇|
|person_name|character varying(20)| |
|birth_date|date| |
|birth_place|character varying(20)| |

というテーブルに対してMyBatisで以下のようなMapperを作成した。

```java
@Mapper
public interface PersonMapper {

	@Select("SELECT * FROM person WHERE person_id = #{personId}")
	public Person selectById(int personId);

	@Insert("INSERT INTO person(person_id, person_name, birth_date, birth_place) VALUES (#{person_id}, #{person_name}, #{birth_date}, #{birth_place})")
	public void insert(Person person);
}
```

# 詳細手順
### 依存関係追加

依存関係に`spring-test-dbunit`と`dbunit`を加える。
※mainコードおよびJUnitが動作するためのライブラリも依存関係に含まれているものとする。

```xml:pom.xml
<dependency>
  <groupId>com.github.springtestdbunit</groupId>
  <artifactId>spring-test-dbunit</artifactId>
  <version>1.3.0</version>
  <scope>test</scope>
</dependency>
<dependency>
  <groupId>org.dbunit</groupId>
  <artifactId>dbunit</artifactId>
  <version>2.6.0</version>
  <scope>test</scope>
</dependency>
```

## 前提データ投入

テスト実行前(テストメソッド実行前)にDBにデータを記述する。

### 基本パターン

__※以下のケースではテストに使用したデータはコミットされる__（ロールバックするケースは後述）

##### 投入データ記述

* `dataset`タグ内にテーブル名のタグを作成し、属性名にカラム名を属性値に投入するデータ値を記述する。
* 複数行のデータを投入する場合は行数分タグを作成する。
* nullにするカラムは属性として記述しない。※先頭にすべてのカラムを記述する必要はない。
* __空文字は標準では投入できない。__（空文字を投入するケースは後述）
* xml内に出現したテーブルについてデフォルトでは全行DELETEされた後INSERTが実行されることでデータが投入される。
* __記述していないテーブルは何も操作されない__

```xml:パターンA.xml
<?xml version="1.0" encoding="UTF-8"?>
<dataset>
	<person person_id="1" person_name="Suzaki" birth_date="1986-12-25" />
	<person person_id="2" person_name="Nishi"                          birth_place="兵庫県" />
	<person person_id="3"                      birth_date="1994-02-22" birth_place="東京都" />
</dataset>
```

※今回はsrc/test/resources配下にdbunitディレクトリを作成し、その下にパターンA.xmlとして作成する。

##### テストクラス作成

```java
// (1)
@RunWith(SpringRunner.class)
@SpringBootTest
@TestExecutionListeners({
	DependencyInjectionTestExecutionListener.class,
	DbUnitTestExecutionListener.class
})
public class PersonMapperTest {

	@Autowired // (2)
	PersonMapper mapper;

	@Test
	@DatabaseSetup("/dbunit/パターンA.xml") // (3)
	public void test() {
		// テストコードを記述
	}
}
```

|||
|---|---|
|(1)|テストクラスに上記アノテーションをすべて記述する|
|(2)|Springを起動するためAutowired可能|
|(3)|テストメソッドに`@DatabaseSetup`を付与し、その引数にxmlのパスを記述する|

##### 実行結果

```
# SELECT * FROM person;
person_id | person_name | birth_date | birth_place
-----------+-------------+------------+-------------
        1 | Suzaki      | 1986-12-25 |
        2 | Nishi       |            | 兵庫県
        3 |             | 1994-02-22 | 東京都
(3 行)
```
### 空行投入パターン

##### 投入データ記述

以下のように属性のないタグを1行記述するとDELETEのみが実行され、空テーブルとなる。

```xml:パターンB.xml
<?xml version="1.0" encoding="UTF-8"?>
<dataset>
	<person />
</dataset>
```

##### 実行結果
```
# SELECT * FROM person;
 person_id | person_name | birth_date | birth_place
-----------+-------------+------------+-------------
(0 行)
```

### 空文字投入パターン

##### 投入データ記述

* 空文字を投入するカラムを属性名として属性値を空にする

```xml:パターンC.xml
<?xml version="1.0" encoding="UTF-8"?>
<dataset>
	<person person_id="4" person_name="" birth_date="1991-12-25" birth_place="兵庫県" />
</dataset>
```

基本パターンのテストコードのみで上記xmlを読み込んだ場合、以下の例外が発生する。
`java.lang.IllegalArgumentException: table.column=person.person_name value is empty but must contain a value (to disable this feature check, set DatabaseConfig.FEATURE_ALLOW_EMPTY_FIELDS to true)`

##### 追加コード

テスト実行時に読み込まれるパスに以下のコンフィグレーションを追加する。

```java
@Configuration
public class DatasourceConfig {

	// (1)
	@Bean
	public DatabaseConfigBean dbUnitDatabaseConfig() {
		DatabaseConfigBean bean = new DatabaseConfigBean();

		bean.setAllowEmptyFields(true); // (2)

		return bean;
	}

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

|||
|---|---|
|(1)|実行時のコンフィグをBeanとして登録する|
|(2)|空文字を許容するスイッチをオンにする(デフォルトではfalseになっている)|
|(3)|(1)の設定をSpringで使用するDatasourceに反映する|

##### 実行結果

```
# SELECT * FROM person;
person_id | person_name | birth_date | birth_place
-----------+-------------+------------+-------------
        4 |             | 1991-12-25 | 兵庫県
(1 行)
```

```java
@Test
@DatabaseSetup("/dbunit/パターンC.xml")
public void test() {
  Person actual = mapper.selectById(4);
  assertThat(actual.getPerson_name()).isEqualTo(""); // -> 成功
}
```

### テスト後にデータを戻すパターン
##### テストクラス作成

基本パターンにリスナーおよびアノテーションを追加する。

```java
@RunWith(SpringRunner.class)
@SpringBootTest
@TestExecutionListeners({
	DependencyInjectionTestExecutionListener.class,
	TransactionalTestExecutionListener.class, // (1)
	DbUnitTestExecutionListener.class
})
@Transactional //(2)
public class PersonMapperTest3 {
  ...
}
```

|||
|---|---|
|(1)|トランザクション管理を行うためのリスナーを追加する|
|(2)|実際にトランザクションを管理する単位(基本はクラス)で`@Transactional`を付与する|

メソッドに`@Transactional`を付与することもできる。

```java
@Test
@Transactional
@DatabaseSetup("/dbunit/パターンA.xml")
public void test() {
  ...
}
```

### 警告文の回避

上のパターンで実行するとWARNレベルで以下のようなログが出ている。
`Potential problem found: The configured data type factory 'class org.dbunit.dataset.datatype.DefaultDataTypeFactory' might cause problems with the current database 'PostgreSQL' (e.g. some datatypes may not be supported properly). In rare cases you might see this message because the list of supported database products is incomplete (list=[derby]). If so please request a java-class update via the forums.If you are using your own IDataTypeFactory extending DefaultDataTypeFactory, ensure that you override getValidDbProducts() to specify the supported database products.`

DataTypeFactoryが未指定というログなので、空文字投入パターンで使用したコンフィグレーションクラスに以下のような記述を追加する。

```java
@Bean
public DatabaseConfigBean dbUnitDatabaseConfig() {
  DatabaseConfigBean bean = new DatabaseConfigBean();

  bean.setAllowEmptyFields(true);
  bean.setDatatypeFactory(new PostgresqlDataTypeFactory()); // (1)

  return bean;
}
```

|||
|---|---|
|(1)|PostgreSQLを使用しているのでdbunitから提供されているPostgreSQL用のDataTypeFactoryを指定している。|

## 想定データ定義
### 基本パターン
##### 想定データ記述

前提データと同じ形式で記述する。
nullや空文字を想定する場合も前提データ投入と同様。

```xml:パターンD.xml
<?xml version="1.0" encoding="UTF-8"?>
<dataset>
	<person person_id="1" person_name="Suzaki" birth_date="1986-12-25" />
	<person person_id="2" person_name="Nishi"                          birth_place="兵庫県" />
	<person person_id="3"                      birth_date="1994-02-22" birth_place="" />
</dataset>
```

```xml:想定結果1.xml
<?xml version="1.0" encoding="UTF-8"?>
<dataset>
	<person person_id="1" person_name="Suzaki" birth_date="1986-12-25" />
	<person person_id="2" person_name="Nishi"                          birth_place="兵庫県" />
	<person person_id="3"                      birth_date="1994-02-22" birth_place="" />
	<person person_id="11" person_name="Uesaka" birth_date="1991-12-19" birth_place="神奈川県" />
</dataset>
```

##### テストメソッド作成

```java
@Test
@DatabaseSetup("/dbunit/パターンD.xml")
@ExpectedDatabase(
	value="/dbunit/想定結果1.xml",
	assertionMode=DatabaseAssertionMode.NON_STRICT
) // (1)
public void test() {
	Person param = new Person();
	param.setPerson_id(11);
	param.setPerson_name("Uesaka");
	param.setBirth_date(new Date(91, 11, 19));
	param.setBirth_place("神奈川県");

	mapper.insert(param);
}
```

|||
|---|---|
|(1)|想定結果を指定するアノテーションを記述する。デフォルトではすべてのテーブルが検査されるが、assertionMode=NON_STRICTとするとxmlに記述したテーブルのみが検査される|

# 参考サイト
[SpringBoot 45.Testing](https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-testing.html)
[Spring FrameworkのUnitテスト実装方法　1-3.Repositoryテスト(Junit4, spring-test, DBUnit) │ ヰ刀のおもちゃ箱](https://ito-u-oti.com/post-105/)
[How to set default properties in spring dataset DatabaseConfig - Stack Overflow](https://stackoverflow.com/questions/42325905/how-to-set-default-properties-in-spring-dataset-databaseconfig)