DBUnit入門

## DBUnitとは
データベースの参照や更新を行うクラスの単体テストのテストプログラムを作成するのにいろいろ苦労することが多いと思います。
単体テストのデータベース確認には以下のような問題が発生しがちです。

* テストのためのデータをDBに投入したり、テスト後にデータを削除してDBを元に戻す作業に時間がかかる
* 「テストデータの投入と後片付け」でプログラムが複雑になり、保守性が下がる。とにかく、他人が書いたテストプログラムは読めない、直せない。
* プログラムが複雑になるため、本当に正しくメソッドを実行し評価しているのか、わかりづらい。

そこで、```DBUnit```と呼ばれるフレームワークを紹介します。
DBUnitとは、データベースを操作するクラスのテストプログラムを作成するためのフレームワークで、JUnitを拡張しています。

* テストデータをDBに投入する機能
* DBの後片付けをする機能
* メソッド実行後のDBの状態を確認する機能

などの機能を備えています。この章では、DBUnitを使用して、データベース処理を行うテストクラスを説明する方法について、説明します。

## DBUtilのインストール
mavenプロジェクトであればpom.xmlに以下の記載をすればDBUtilを使用できます。

```
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<version>8.0.22</version>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.dbunit</groupId>
			<artifactId>dbunit</artifactId>
			<version>2.6.0</version>
		</dependency>
```
JUnitはバージョン4.13を利用しています。

mavenプロジェクトではなく、DBUtilを利用する場合は、以下のリンクより　jarファイルをダウンロードし、クラスパスを通せば利用できます。（お勧めはされていませんが）
http://www.dbunit.org/

今回はDBUnitを利用して、以下のことを行っていきます

* テスト前にDBデータを削除、XMLからデータをインサート
* 更新確認のためのテストデータをXMLから取得、テストクラスの実施
* テストデータの削除

## テストテーブルの作成
今回はmysqlデータベースを利用しているため、以下のSQLにてテーブルを作成する。

```sql
CREATE TABLE user (
  Id INT NOT NULL,
  name  VARCHAR(255) NOT NULL,
  age INT NOT NULL,
  salary INT,
  job_type VARCHAR(20),
  hiredate  TIMESTAMP(6),
  department_id INT
);
```

## テスト対象の実装
レコードを一件UPDATEするクラスを作成します。

```java:TestMain

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * DBUnitテストのmainを持つクラス。
 */
public class TestMain {
  /** ロガー */
  private static Logger logger = LoggerFactory.getLogger(TestMain.class);

  /** 実行するSQL */
  private static final String SQL
      = " update user set name = 'abc' where id = 1";

  /**
   * @param args
   */
  public static void main(String[] args) {
    logger.info("処理開始");

    // ---------------------------------
    // DBを更新する
    // ---------------------------------
    try {
    	Class.forName("com.mysql.jdbc.Driver");
    	Connection conn = DriverManager.getConnection(
    	        "jdbc:mysql://localhost:3306/[データベース名]?serverTimezone=JST", "[ユーザ名]", "[パスワード]");
    	        PreparedStatement stmt = conn.prepareStatement(SQL);

      conn.setAutoCommit(false);
      int i = stmt.executeUpdate();

      // 処理件数を表示する
      logger.info("処理件数:[" + i + "]");

      conn.commit();
    } catch(Exception e) {
      logger.error("エラー", e);
    }

    logger.info("処理終了");
  }
}
```
## テスト実行時に使用するXMLファイルの作成

```xml:Before.xml
<?xml version="1.0" encoding="UTF-8"?>
<dataset>
  <!--
    データセット用のXMLでは数値データも""で囲む
    DATEやTIMESTAMP型の日付は「-」つなぎで指定する
  -->
  <user
      ID="1"
      NAME="scott"
      AGE="22"
      SALARY="200000"
      JOB_TYPE="employee"
      HIREDATE="2017-01-01 12:34:56"
      DEPARTMENT_ID="1"
  />
</dataset>
```

```xml:After.xml
<?xml version="1.0" encoding="UTF-8"?>
<dataset>
  <!--
    日付は検証対象外だが一応記載します。
    フィルターできていることを検証するために、わざと違う日付を入力しておきます。
  -->
  <user
      ID="1"
      NAME="abc"
      AGE="22"
      SALARY="200000"
      JOB_TYPE="employee"
      HIREDATE="2017-12-31 24:12:36"
      DEPARTMENT_ID="1"
  />
</dataset>
```

Before.xmlは一件だけ更新されていることを確認するためにテストクラス実施前にインサートするデータ。
After.xmlはupdateが行われたあとのDBのデータです。テストクラスのアサートの際に利用します。

## テストクラスの実装

```java:TestMainTest

import static org.junit.Assert.*;

import java.io.File;

import org.dbunit.Assertion;
import org.dbunit.IDatabaseTester;
import org.dbunit.JdbcDatabaseTester;
import org.dbunit.dataset.IDataSet;
import org.dbunit.dataset.ITable;
import org.dbunit.dataset.filter.DefaultColumnFilter;
import org.dbunit.dataset.xml.FlatXmlDataSetBuilder;
import org.dbunit.operation.DatabaseOperation;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * [テストクラス]<br>
 * JUnitとDBUnitを使用してテストを行う。<br>
 * <br>
 */
public class TestMainTest {
  /** ロガー */
  private Logger logger = LoggerFactory.getLogger(TestMain.class);

  /** DBUnitのテスター */
  private static IDatabaseTester databaseTester;

  /**
   * [前処理]<br>
   * DBに事前データを準備する。<br>
   * <br>
   * @throws java.lang.Exception
   */
  @Before
  public void setUp() throws Exception {
    logger.info("前処理開始");

    // --------------------------------------
    // 事前準備データのINSERT
    // --------------------------------------
    // 事前準備データのINSERTにはスキーマも合わせて指定する
    databaseTester = new JdbcDatabaseTester("com.mysql.jdbc.Driver",
        "jdbc:mysql://localhost:3306/[データベース名]?serverTimezone=JST", "[ユーザー名]", "[パスワード]");

    // --------------------------------------
    // テストデータ投入
    // --------------------------------------
    IDataSet dataSet = new FlatXmlDataSetBuilder().build(new File("src/test/java/com/example/DBUnitDemo/data/Before.xml"));
    databaseTester.setDataSet(dataSet);
    // DELETE→INSERTで事前準備データを用意する
    databaseTester.setSetUpOperation(DatabaseOperation.CLEAN_INSERT);
        databaseTester.onSetup();

    logger.info("前処理終了");
  }

  /**
   * [後処理]<br>
   * テスト後の後処理を行う。<br>
   * DBUnitの後片付けを行う。<br>
   * <br>
   * @throws java.lang.Exception
   */
  @After
  public void tearDown() throws Exception {
    databaseTester.setTearDownOperation(DatabaseOperation.NONE);
    databaseTester.onTearDown();
  }

  /**
   * [テスト]<br>
   * DBUnitを使用して、DBの更新結果を検証する。<br>
   */
  @Test
  public void test() {
    logger.info("JUnit + DBUnitによるテスト開始。");

    TestMain.main(null);

    try {
      // ----------------------------------
      // DBUnitで更新後データチェック
      // ----------------------------------
      IDataSet expectedDataSet = new FlatXmlDataSetBuilder().build(new File("src/test/java/com/example/DBUnitDemo/data/After.xml"));
      ITable expectedTable = expectedDataSet.getTable("user");

      IDataSet databaseDataSet = databaseTester.getConnection().createDataSet();
      ITable actualTable = databaseDataSet.getTable("user");

      // 時間に対するAssertionはほぼ確実に失敗するので検証対象から除外する
      ITable filteredExpectedTable = DefaultColumnFilter.excludedColumnsTable(
          expectedTable, new String[]{"HIREDATE"});
      ITable filteredActualTable;
      filteredActualTable = DefaultColumnFilter.excludedColumnsTable(
          actualTable, new String[]{"HIREDATE"});

      // ---------------------------------------------------------------
      // 更新結果の検証はJUnitではなくDBUnitのAssertionを使用する
      // ---------------------------------------------------------------
      Assertion.assertEquals(filteredExpectedTable, filteredActualTable);
    } catch (Exception e) {
      logger.error("エラー", e);
      fail("予期しないエラーでテストが失敗しました。");
    }

    logger.info("JUnit + DBUnitによるテスト開始。");
  }
}
```

今回利用したDBUnitのメソッドは以下です。

|メソッド名|説明|
|:--|:--|
|JdbcDatabaseTester(String driverClass, String connectionUrl),JdbcDatabaseTester(String driverClass, String connectionUrl, String username, String password),JdbcDatabaseTester(String driverClass, String connectionUrl, String username, String password, String schema)|JDBCのドライバーマネージャーを使用して接続を作成するDatabaseTester。|
|FlatXmlDataSetBuilder().build(File  xmlInputFile)|FlatXmlDataSet構築するフラットXML入力ソースを設定します。|
|IDatabaseTester.setDataSet(IDataSet dataSet)|使用するテストデータセットを設定します。|
|IDatabaseTester.setSetUpOperation()|テストの開始時に呼び出すDatabaseOperationを取得します。|
|setTearDownOperation(DatabaseOperation tearDownOperation)|テストの終了時に呼び出すようにDatabaseOperationを設定します。|
|IDataSet.getTable(String tableName)|データセットから指定されたテーブルを返します。|
|IDatabaseTester.getConnection()|テストデータベース接続を返します。|
|IDatabaseConnection.createDataSet() |データベースから指定されたテーブルのみを含むデータセットを作成します。|
|DefaultColumnFilter.excludedColumnsTable(ITable table, Column[] columns)|指定された列が除外されているテーブルを返します。|
|Assertion.assertEquals(IDataSet expectedDataSet, IDataSet actualDataSet) |期待されるITableと実際のITableの比較を行う|


## 参照
今回はDBUnitの導入、そしてこの記事は以下の記事を参照に作成しています。
よりくわしく書かれていますので、もしさらに知りたい方は参照してみてください。
[【超初心者向け】DBUnit超入門](https://qiita.com/tarosa0001/items/70a1efa9edac2d83ba1a)
[DBUnit](https://www.techscore.com/tech/Java/Others/DBUnit/1/)
[DBUnit を使って Excel からデータを DB にロードする with Spring Boot](https://www.shookuro.com/entry/2019/01/20/165930)