Spring Boot で DB アクセスまわりのテスト

Spring Boot に限った話ではないんだけど、今回 Spring Boot で書いたので、そんなタイトルに。

必要とされるシーンが多いわりに、意外と情報が少なかったので、まとめておきます。
DbSetup を使用するものと、DBUnit を使用するもの、それぞれを紹介します。
なお、今回はテストに Groovy Spock を使用します。

検証に使用したものを GitHub に公開しておいたので、こちらを確認しながら追いかけると、より理解しやすいかもしれません。
https://github.com/yo1000/com.yo1000.edu.boot.dbtest

# 環境

- Java 1.8.0_91
- Maven 3.3.9 (Maven wrapper)
- Spring Boot 1.4.0.RELEASE
- Groovy 2.4.7
- Spock 1.1-groovy-2.4-rc-1
- DbSetup 2.1.0
- DBUnit 2.5.3

```
$ ./mvnw -version
Apache Maven 3.3.9 (bb52d8502b132ec0a5a3f4c09453c07478323dc5; 2015-11-11T01:41:47+09:00)
Maven home: /Users/yo1000/.m2/wrapper/dists/apache-maven-3.3.9-bin/2609u9g41na2l7ogackmif6fj2/apache-maven-3.3.9
Java version: 1.8.0_91, vendor: Oracle Corporation
Java home: /Library/Java/JavaVirtualMachines/jdk1.8.0_91.jdk/Contents/Home/jre
Default locale: ja_JP, platform encoding: UTF-8
OS name: "mac os x", version: "10.11.5", arch: "x86_64", family: "mac"
```

# Groovy Spock の準備

テストを書く前に Spock、各種 DB テスティングフレームワークを使用して、テストを書けるように、諸々の準備を済ませておきます。

## 依存関係の追加

pom.xml にいろいろ書いていきます。
まずは、依存モジュールの定義。

```xml:pom.xml&nbsp;&lt;dependencies&gt;
<!-- Groovy Spock -->
<dependency>
  <groupId>org.codehaus.groovy</groupId>
  <artifactId>groovy-all</artifactId>
  <version>2.4.7</version>
  <scope>test</scope>
</dependency>
<dependency>
  <groupId>org.spockframework</groupId>
  <artifactId>spock-core</artifactId>
  <version>1.1-groovy-2.4-rc-1</version>
  <scope>test</scope>
</dependency>
<dependency>
  <groupId>org.spockframework</groupId>
  <artifactId>spock-spring</artifactId>
  <version>1.1-groovy-2.4-rc-1</version>
  <scope>test</scope>
</dependency>

<!-- DbSetup -->
<dependency>
  <groupId>com.ninja-squad</groupId>
  <artifactId>DbSetup</artifactId>
  <version>2.1.0</version>
  <scope>test</scope>
</dependency>

<!-- DBUnit -->
<dependency>
  <groupId>org.dbunit</groupId>
  <artifactId>dbunit</artifactId>
  <version>2.5.3</version>
  <scope>test</scope>
</dependency>
```

続いて、ビルドプラグイン。

```xml:pom.xml&nbsp;&lt;build&gt;&lt;plugins&gt;
<plugin>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
  <groupId>org.codehaus.gmavenplus</groupId>
  <artifactId>gmavenplus-plugin</artifactId>
  <version>1.5</version>
  <executions>
    <execution>
      <goals>
        <goal>addTestSources</goal>
        <goal>testCompile</goal>
      </goals>
    </execution>
  </executions>
  <configuration>
    <sources>
      <fileset>
        <directory>${pom.basedir}/src/test/groovy</directory>
        <includes>
          <include>**/*.groovy</include>
        </includes>
      </fileset>
    </sources>
  </configuration>
</plugin>
<plugin>
  <groupId>org.sonarsource.scanner.maven</groupId>
  <artifactId>sonar-maven-plugin</artifactId>
  <version>3.1.1</version>
</plugin>
<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-surefire-plugin</artifactId>
  <version>2.19.1</version>
  <configuration>
    <includes>
      <include>**/*Test.java</include>
      <include>**/*Spec.java</include>
      <include>**/*Tests.java</include>
      <include>**/*Specs.java</include>
    </includes>
  </configuration>
</plugin>
```

## テストデータベースの用意

今回のデモプロジェクトでは、H2 データベースをメモリモードで使用していますが、基本的にはどのようなデータベースでも構いません。

以下のようなテーブルを用意しました。

| Column      | Type         |
|:------------|:-------------|
| GROUP_ID    | VARCHAR(80)  |
| ARTIFACT_ID | VARCHAR(80)  |
| DESC        | VARCHAR(200) |

```sql
CREATE TABLE TEST_FRAMEWORK (
  GROUP_ID    VARCHAR(80),
  ARTIFACT_ID VARCHAR(80),
  DESC        VARCHAR(200)
);
```

# DbSetup によるテスト

DbSetup によるテストは、テストデータもコード内に収まるため、非常に理解しやすく、簡単です。
とくに内容にひねりもないので、そのままコードを載せておきます。

```groovy:TestFrameworkRepositoryDbSetupSpec.groovy
package com.yo1000.edu.boot.dbtest

import com.ninja_squad.dbsetup.DbSetup
import com.ninja_squad.dbsetup.Operations
import com.ninja_squad.dbsetup.destination.DataSourceDestination
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import spock.lang.Specification
import spock.lang.Unroll

import javax.sql.DataSource

@SpringBootTest
@Unroll
class TestFrameworkRepositoryDbSetupSpec extends Specification {
    @Autowired
    Application.TestFrameworkRepository frameworkRepository

    @Autowired
    DataSource dataSource

    def setup() {
        def destination = new DataSourceDestination(dataSource)

        def insertItems = Operations.insertInto('TEST_FRAMEWORK')
                .columns('GROUP_ID'         , 'ARTIFACT_ID' , 'DESC')
                .values('com.ninja-squad'   , 'DbSetup'     , '小さいデータ向き。コードとデータを一緒に管理したい場合にオススメ。')
                .values('org.dbunit'        , 'dbunit'      , '大きいデータ向き。大量データの集計などをテストしたい場合にオススメ。')
                .values('Null Example'      , 'Null Example', null)
                .build()

        new DbSetup(destination,
                Operations.sequenceOf(
                        Operations.truncate('TEST_FRAMEWORK'),
                        insertItems
                )
        ).launch()
    }

    def "dbsetup demo"() {
        expect:

        def items = frameworkRepository.findAll()

        items.each {
            println 'groupId: ' + it.get('groupId')
            println 'artifactId: ' + it.get('artifactId')
            println 'desc: ' + it.get('desc')
            println 'desc is null?: ' + (it.get('desc') == null)
        }

        assert items.size() > 0
    }
}
```

# DBUnit によるテスト

DBUnit によるテストでは、テストデータは外部ファイルに分かれます。
そのため、DbSetup に比べると、レビューコストは少々膨らんでしまいます。

一方、テストデータファイルの形式は、今回使用する DBUnit 2.5.3 では、XML、CSV、Excel がサポートされているため、集計処理のテストなどで、大量のテストデータが必要になる場合には、Excel などのスプレッドシートエディタでデータの量産がしやすいといった利点もあります。

わたしのオススメは、GitHub などでレビューを行いやすいプレーンテキストであり、インデントを意識すれば表形式に近い表現のできる CSV です。
そのため、今回は CSV を使用したサンプルを紹介します。

## 必要なファイルセット

DBUnit でのテストに必要なファイルは、テストコードと、テストデータだけではありません。
以下のファイルセットが必要になります。

- テストコード
- テストデータ
- `table-ordering.txt`

なお、テストデータファイルと、`table-ordering.txt` は、同一のディレクトリに配置されている必要があります。

## テストデータ

テストデータファイルは、ファイル名を `テーブル名.拡張子` という形で用意します。
また、先の説明で触れているとおり、XML、CSV、Excel いずれかの形式でデータを表現できます。

今回は、`TEST_FRAMEWORK` というテーブルを使用し、CSV 形式のテストデータファイルを用意するので、`TEST_FRAMEWORK.csv` となります。
また、テストデータの表現については、以下のような制約があります。

カラム表現に関する制約

- カラム名は、1行目に列挙する
- カラム名は、ダブルクオート (`"`) で囲わないと、スペースも含めてカラム名として扱われる

データ表現に関する制約

- 文字データは、ダブルクオートで囲って表現する
- 日付データは、ダブルクオートで囲って表現する
- 数値データは、ダブルクオートをつけず表現する
- NULL 値は、ダブルクオートをつけず、小文字 (`null`) で表現する

以上を踏まえて、以下のようなファイルを用意します。

```csv:TEST_FRAMEWORK.csv
"GROUP_ID"          , "ARTIFACT_ID" , "DESC"
"com.ninja-squad"   , "DbSetup"     , "小さいデータ向き。コードとデータを一緒に管理したい場合にオススメ。"
"org.dbunit"        , "dbunit"      , "大きいデータ向き。大量データの集計などをテストしたい場合にオススメ。"
"Null Example"      , "Null Example", null
```

## table-ordering.txt

このファイルは、外部キー制約などに起因する、テーブル間の処理順序を定義するために使用します。
テストデータの流し込みを行いたい順番に、改行で区切って、テーブル名を列挙します。

```txt:table-ordering.txt
TEST_FRAMEWORK
```

## テストコード

ここまでできてしまえば、あとは DbSetup と大きな違いはありません。
`setup` でテストデータを用意して、`expect` などでテストを行うだけです。

```groovy:TestFrameworkRepositoryDbUnitSpec.groovy
package com.yo1000.edu.boot.dbtest

import com.yo1000.edu.boot.dbtest.Application.TestFrameworkRepository
import org.dbunit.DataSourceDatabaseTester
import org.dbunit.operation.DatabaseOperation
import org.dbunit.util.fileloader.CsvDataFileLoader
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.util.ResourceUtils
import spock.lang.Specification
import spock.lang.Unroll

import javax.sql.DataSource

@SpringBootTest
@Unroll
class TestFrameworkRepositoryDbUnitSpec extends Specification {
    @Autowired
    TestFrameworkRepository frameworkRepository

    @Autowired
    DataSource dataSource

    def setup() {
        def databaseTester = new DataSourceDatabaseTester(dataSource)
        databaseTester.setUpOperation = DatabaseOperation.CLEAN_INSERT

        def loader = new CsvDataFileLoader()
        databaseTester.dataSet = loader.loadDataSet(ResourceUtils.getURL("classpath:dbunit/"))   // Requires tail slash

        databaseTester.onSetup()
    }

    def "dbunit demo"() {
        expect:

        def items = frameworkRepository.findAll()

        items.each {
            println 'groupId: ' + it.get('groupId')
            println 'artifactId: ' + it.get('artifactId')
            println 'desc: ' + it.get('desc')
            println 'desc is null?: ' + (it.get('desc') == null)
        }

        assert items.size() > 0
    }
}
```

# まとめ

記述の違いこそあれ、いずれの方法であっても、同様のテストが可能なことは理解できたかと思います。
最後に個人的な所感に基づいた、それぞれのメリットと、使い分けのポイントなどを紹介しておわります。

|              | DbSetup     | DBUnit     
|:-------------|:------------|:-----------
| テストの保守性  | :+1:         | :cold_sweat: 
| レビューの容易さ | :+1:         | :cold_sweat: 
| 大量データ表現 | :cold_sweat: | :+1:  

DbSetup の使いどころ

- テストデータのパターンは必要だが、物量は必要ない場合に、オススメ
- テストデータも含めて、しっかりとレビューを行いたい場合にも効果的

DBUnit の使いどころ

- 集計処理のテストなど、テストデータの物量が必要になってくる場合に、オススメ

# 補足

2016-12-11 追記:
コメントでもご指摘を頂いておりますが、テストでトランザクションを使用して、挿入したテストデータをロールバックしようとした場合、DbSetup では、これが一部で上手く動作しません。少し手を加えれば、DbSetup でもテストデータのロールバックを扱うことができますが、提供されたデフォルトのテスト機能と方法を使用した場合、DbSetup ではこれが動作しない点には注意が必要となります。
詳しくは以下をご確認ください。
[Spring Boot, Spock, DBUnit でトランザクションを使ったテスト  #DbSetup でのトランザクション](http://qiita.com/yo1000/items/0ce7e8ab0800b49861f7#dbsetup-%E3%81%A7%E3%81%AE%E3%83%88%E3%83%A9%E3%83%B3%E3%82%B6%E3%82%AF%E3%82%B7%E3%83%A7%E3%83%B3)
[Spring Boot, Spock, DbSetup でトランザクションを使ったテスト](http://qiita.com/yo1000/items/a7c0825035b4fbcfa3b2)