30
31

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2014-04-10

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をテストコードに1行追加するだけで実現できるようにする。

最終的には

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

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

きもはInterceptorの使い方

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

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

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

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

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

Interceptorの実装

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

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

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

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

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

実装

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

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を呼び出します。
なければそのままテストメソッドを呼び出します。

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

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ってどこで代入されるの?と疑問に思われたかと思います。
次回のアノテーション導入編ではこちらの初期化処理についても説明します。

30
31
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
30
31

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?