Spockでデータベースが絡むテストを書くためにやるべきこと　その１


## Spockとは
SpockはJavaやGroovy向けのテストフレームワークです。
JUnitなどと比較して以下の点が使いやすいです。

- データドリブンなテストが書きやすい
- Mockが書きやすい
- テスト結果が見やすい(PowerAssert)

基本的な使い方はこちら方のブログにわかりやすく書かれています。
http://bufferings.hatenablog.com/entry/2014/01/17/234736


## やりたいこと
データベースにアクセスするテストコードを書く場合、テスト開始時にデータベースにテスト用のデータを流し、テストが終了したら元のデータを復元するということを行えると便利です。
しかし残念ながら、Spockにはその仕組みが用意されていないためDbUnitのようなデータベースを操作する仕組みが必要になります。
今回はSpockとDbUnitを連携させて、上記の仕組みを作っていきます。

まず、今回作る機能の仕様はこんな感じです。

1. テストの開始前に、データベースにテスト用のデータが流される。
2. テストが完了したら、テスト用のデータが削除されてテスト前のデータが復元される。
3. テストコード自体が肥大化しないようにテストデータは別ファイルで管理する（とりあえずxml。時間ができたらyamlにも対応したい。）
4. 1〜3をテストコードに１行追加するだけで実現できるようにする。

最終的には

```groovy:例 
@DbUnit(dataFile = "test_data.xml")
def "データベースアクセスのテスト" () {
//テストコード
}
```

という感じで、DbUnitアノテーションをテストメソッドにつけたらメソッドの開始前にtest_data.xmlに入っているテスト用のデータが反映され、メソッドの処理が終わったらデータが復元されるようになります。


## きもはInterceptorの使い方
Spockではユーザが独自のInterceptorを定義できるようになっています。
これをやるにはまずAbstractMethodInterceptor抽象クラスを実装したクラスを作成する必要があります。
そして抽象クラスに実装されているメソッドを上書きすることによって、テスト実行時の特定のタイミングに処理を割り込ませることができます。
この割り込みメソッドは全部で12個ありますが、今回使用するのは以下の3個です。

|メソッド名|割り込むタイミング|
|:--------|:---------------| 
|interceptSetupSpecMethod|全てのテストが始まる前に１回だけ|
|interceptCleanupSpecMethod|全てのテストが終わった後に１回だけ|
|interceptFeatureMethod|各テストメソッド実行時に毎回|

あとは、各Interceptor内にDbUnitを使ってデータベースにデータを流したり、復元したりする処理を書いていきます。

ちなみに、他によく使いそうな割り込みメソッドとしては以下のものがあります。

|メソッド名|割り込むタイミング|
|:--------|:---------------| 
|interceptSetupSpecMethod|各メソッドの実行前に毎回|
|interceptCleanupMethod|各メソッドの実行後に毎回|

## Interceptorの実装
まず実装内容を整理すると

1. テストメソッドの実行前にデータベースのバックアップを行います（処理が重くならない用にバックアップはテストで使用するテーブルのみ行います）
2. 対象テーブルのデータを削除し、テスト用データをinsertします
3. テストコードを実行します
4. テスト完了後、データベースの状態をテスト前の状態に戻します

そして、各処理と対応するInterceptorはこんな感じです。

|メソッド名|割り込むタイミング|処理|
|:--------|:---------------|:-----|
|interceptSetupSpecMethod|テストが始まる前に１回だけ|データベースとコネクションをはる|
|interceptCleanupSpecMethod|テストが終わった後に１回だけ|データベースとのコネクションを切る|
|interceptFeatureMethod|各メソッド実行時に毎回|1,2,3,4の処理|


では実装をはじめていきます。
データベースとのコネクションを確立したり、切断したりする処理では特別なっことをやっている分けではないので説明をとばして、interceptFeatureMethod部分を見ていきます。

### 実装

コードから各メソッドでどのように処理を行っているかは分かって頂けるかなと思います。
コードは見やすさを考慮してエラー処理部分を省いています。そのためそのまま使おうとしても動かないです。

```groovy:
public class DbUnitInterceptor extends AbstractMethodInterceptor {

    private String testDataFile; //テストデータファイル名
    private File backupDataFile;
    private static IDatabaseConnection connection; //interceptSetupSpecMethod中で初期化する


    /**
     * 各テストメソッド実行時に呼ばれる
     * @param invocation
     * @throws Throwable
     */
    public void interceptFeatureMethod(IMethodInvocation invocation) throws Throwable {
       //xml形式のテストデータファイルを読み込む
       ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
       FlatXmlDataSet flatXmlDataSet = new FlatXmlDataSet(contextClassLoader.getResourceAsStream(testDataFile));

        // データベースのバックアップをとる
        makeBackupData(flatXmlDataSet);

        // テストデータを投入
        ReplacementDataSet replacementDataSet = replaceDataSet(flatXmlDataSet);
        DatabaseOperation.CLEAN_INSERT.execute(connection, replacementDataSet);

        // テストメソッドを呼び出す
        invocation.proceed();

        //データベースを復元する
        restoreData();
    }

    /**
     * データベースのバックアップをとる
     * @param flatXmlDataSet
     */
    private void makeBackupData (FlatXmlDataSet flatXmlDataSet) {
        QueryDataSet queryDataSet = new QueryDataSet(connection);
        String[] tableNames = flatXmlDataSet.getTableNames();
        for (String tableName : tableNames) {
            queryDataSet.addTable(tableName);
        }
        backupDataFile = File.createTempFile("tmp", ".xml");
        FileOutputStream fileOutputStream = new FileOutputStream(backupDataFile);
        FlatXmlDataSet.write(queryDataSet, fileOutputStream);
        fileOutputStream.close();
    }

   /**
    * テストデータ中の特定の文字列を置換する
    * @param flatXmlDataSet
    * @return
    */
    private ReplacementDataSet replaceDataSet(FlatXmlDataSet flatXmlDataSet) {
        ReplacementDataSet replacementDataSet = new ReplacementDataSet(flatXmlDataSet);
        replacementDataSet.addReplacementObject("[NULL]", null);
        replacementDataSet.addReplacementObject("[NOW]", new Date());
        replacementDataSet.addReplacementObject("[TOMORROW]", new DateTime().plusDays(1).toDate());
        replacementDataSet.addReplacementObject("[YESTERDAY]", new DateTime().minusDays(1).toDate());
        return replacementDataSet;
    }


    /**
     * データベースをテスト前の状態に復元する
     */
    private void restoreData() {
        if (backupDataFile != null) {
            FlatXmlDataSet flatXmlDataSet = null;
            flatXmlDataSet = new FlatXmlDataSet(backupDataFile);
            DatabaseOperation.CLEAN_INSERT.execute(connection, flatXmlDataSet);
        }
    }
}
```

ここで気をつけて頂きたいのは `invocation.proceed()` の位置です。 
`invocation.proceed()` を呼び出すことでSpecクラスに記述したテストメソッドを実行するため、このコードはテスト用データの投入処理とデータ復元処理の間に入れておかなければいけません。
ちなみに、`invocation.proceed()` はテストメソッドの実行前にInterceptorを実行します。
そのためここで `invocation.proceed()` を実行した場合、次に登録されているInterceptorがあるかチェックして、あればInterceptorを呼び出します。
なければそのままテストメソッドを呼び出します。

今回使用しているテストデータのサンプルはこんな感じです。

```xml:test1.xml
<?xml version="1.0" encoding="UTF-8"?>

<dataset>
    <user user_id="1" user_name="test-00001" last_login_datetime="[NOW]" />
    <user_status user_id="1" power="1000" />
</dataset>
```

<テーブル名 カラム1="値1" カラム2="値2" />
という規則でデータを定義しています。

コードを読まれた方はtestDataFileってどこで代入されるの？と疑問に思われたかと思います。
次回のアノテーション導入編ではこちらの初期化処理についても説明します。