Spring + MyBatis + DbSetupでテストコードを書いてみる


手っ取り早くサンプルコードを見たい方はこちらをどうぞ。

* https://github.com/otsecbsol/sample-java-dbsetup

## 背景
Javaでデータベース周りのユニットテストを行う場面では、テストデータ投入手段として[DbUnit](http://dbunit.sourceforge.net/)が広く使われてきました。XMLやExcelでテストデータを定義できたり、データのクリーンアップやテストメソッド単位でのデータ作成ができたりとさまざまな仕組みが用意されており、私も便利に使わせてもらっています。

ただ、テスト対象のシステムがある程度の規模になると、データとテストコードの紐付けが難しくなったり、スローテスト問題が顕著になってきたりと課題もあることから、代替ライブラリを探しているところです。


## DbSetup
* http://dbsetup.ninja-squad.com/

データアクセス層のユニットテストを対象としたテストライブラリです。

1. Javaコード中にテストデータを定義することでコードとデータの見通しが良くなる
2. テストを高速に実行できる

を主なウリにしているようで、これを今回試してみようと思います。

## サンプル
### 「操作」の定義
例えば以下のような「会社」を表す `company`テーブルがあるとします。

|No.|Column|Type|PK|
|--:|:---------------|:-----------------------|:---:|
|1  | id                 |bigint auto increment|○|
|2  | company_cd |varchar|-|
|3  | name           |varchar|-|
|4  | remarks       |varchar|-|
|5  | created_at   |datetime|-|
|6  | updated_at  |datetime|-|


DbSetupを使ったテストの場合、このテーブルに投入するデータとデータへの「操作」を`Operation`クラスのインスタンスとして定義していくことになります。

#### 固定値を指定してテストデータを投入する操作
```java
import static com.ninja_squad.dbsetup.Operations.*;

    public static Operation INSERT_COMPANY =
            insertInto("company")
                .columns("id", "company_cd", "name", "remarks", "created_at", "updated_at")
                .values(1L, "AC", "A建設", null, "2016-07-05", "2016-07-05")
                .values(2L, "MB", "M商事", null, "2016-01-20", "2016-06-30")
            .build();
```

#### 連続した値のテストデータを投入する操作
```java
import static com.ninja_squad.dbsetup.Operations.*;

    public static Operation INSERT_COMPANY_FOR_TEST_FIND =
            insertInto("company")
                // 列 id は1001から連番を格納
                .withGeneratedValue("id",
                        ValueGenerators.seequence().startingAt(1001L))
                // 列 company_cd に "code-0001", "code-0002" といった連続した値を格納
                .withGeneratedValue("company_cd",
                        ValueGenerators.stringSequence("code-").startingAt(1L).withLeftPadding(4))
                // 列 name も company_cd と同様
                .withGeneratedValue("name",
                        ValueGenerators.stringSequence("name-").startingAt(1L).withLeftPadding(4))
                // 列 remarks, created_at, updated_at には固定値を格納
                .columns("remarks", "created_at", "updated_at")
                .repeatingValues("TEST-FIND", "2016-01-01", "2016-01-01")
                // 繰り返し回数の指定. この場合、100件分のレコードが作成されることになる
                .times(100)
            .build();
```
#### テーブルのレコードを削除する操作
```java
import static com.ninja_squad.dbsetup.Operations.*;

    /// 全レコード削除
    public static Operation DELETE_COMPANY =
            deleteAllFrom("company");
    /// 複数のテーブルを指定して順番に全レコード削除
    public static Operation DELETE_ALL =
            deleteAllFrom("section_employee", "employee", "section", "company");
    /// 任意のSQLでレコード削除
    public static Operation DELETE_COMPANY_FOR_TEST_FIND =
            sql("DELETE FROM company WHERE id > 1000");
```

`Operations`クラスに定義されたstaticメソッドを使って「データを登録する操作」「データを削除する操作」などを組み立てています。DSL的に書けるので直感的に理解し易そうですし、バルクデータ作成の助けとなるAPIも用意されているようですね。

### 実行
* テストの`setup`で、予め定義した`Operation`オブジェクトを組み合わせたひとつの`Operation`オブジェクトを生成、実行します。これで各テストメソッド実行時にテストデータが整った状態となります。
  * `Operations.sequenceOf(Operation... o)`
  * `DbSetupTracker#launchIfNecessary(DbSetup)`
* 更新系の処理についてはいつもどおりテストを書きます。
* 検索系の処理など、データを更新しないテストケースでは、テスト後のクリーンアップや後続のテストのためのデータ準備が必要ありません。(テストが実行される前に既にデータは作成されているので)
  * ですので、`DbSetupTracker#skipNextLaunch()`を呼び出して「後続のテストでの`setup`をスキップする」よう指定します。

```java
import static com.ninja_squad.dbsetup.Operations.*;
import com.ninja_squad.dbsetup.DbSetup;
import com.ninja_squad.dbsetup.DbSetupTracker;
import com.ninja_squad.dbsetup.destination.DataSourceDestination;
  
// @RunWith等のテスト関連アノテーションは省略
public class CompanyRepositoryTest {
    private static DbSetupTracker TRACKER = new DbSetupTracker();
    private DataSource dataSource = ...
    
    /** テスト対象 */
    @Autowired
    private CompanyRepository repository = ...
    /**
     * 各テストの事前処理.
     */
    @Before
    public void before() {
        DbSetup setup = new DbSetup(DataSourceDestination.with(dataSource),
                    sequenceOf(DELETE_ALL,
                                INSERT_COMPANY,
                                INSERT_COMPANY_FOR_TEST_FIND));
        TRACKER.launchIfNecessary(setup);
    }
    // 個別のテスト
    @Test
    public void testCreate() {
        ....
    }
    @Test
    public void testUpdate() {
        ....
    }
    @Test
    public void testFindWithCompanyCd() {
        // データを更新しないテストケースでは DbSetupTracker#skipNextLaunch(); を呼び出しておく
        // これにより、次の(＝別メソッドの)テストの事前処理で DbSetupTracker#launchIfNecessary() が
        // 呼び出されてもテストデータ再作成処理が実行されなくなる
        TRACKER.skipNextLaunch();
        ....
    }
}
```
要するに`setup`を効率化し「必要なタイミングで必要なテストデータを作る」ことで、「テストの高速な実行」を実現するということです。
そのためにはテストクラス・テストメソッド毎に任意の`Operataion`を組み合わせられるようなるべく小さな粒度で定義しておくことが求められ、そうすることで結果としてテストデータの再利用性が高まるかもしれません。(一番頭を悩ますところでもあるとは思いますが)

## 補足
Githubにgradleプロジェクトの形式で[サンプルコード](https://github.com/otsecbsol/sample-java-dbsetup)を置いています。MyBatisやDBマイグレーションツールである[liquibase](http://www.liquibase.org)とSpringの連携の部分も、あまり馴染みのない方にとっては参考になるかもしれません。

今回はSpring+MyBatisのプログラムをDbSetupを使ってテストしましたが、テストコードの部分に関しては、他のライブラリの組み合わせでもそれほど違いはないでしょう。