日々常々

ふつうのプログラマがあたりまえにしたいこと。

DbUnitのためのRuleから、RuleChainとかその辺の話

JUnit実践入門の「第12章 データベースのテスト」でも取り上げられているDbUnitさんのRuleから派生して、RuleとかRuleChainとかその辺をちょっと書いておきます。

JUnit実践入門 ~体系的に学ぶユニットテストの技法 (WEB+DB PRESS plus)

JUnit実践入門 ~体系的に学ぶユニットテストの技法 (WEB+DB PRESS plus)

セットアップ

ざくっと使えるようにするための build.gradle はこんな感じ。Mavenでもだいたいわかりますよね。

apply plugin: 'java'

repositories.mavenCentral()
dependencies {
    testCompile 'com.h2database:h2:1.3.170'
    testCompile 'junit:junit:4.11'
    testCompile 'org.dbunit:dbunit:2.4.9'
    testCompile 'org.slf4j:slf4j-simple:1.7.2'
}

載ってるRuleさん(DbUnitTester)

'extends AbstractDatabaseTester' とやってますが、JDBCを使うなら特化されてる 'JdbcDatabaseTester' や 'PropertiesBasedJdbcDatabaseTester' を使っても良いと思います。*1
JdbcDatabaseTester を使用すると若干シンプルにかけます。が、コンストラクタが例外をスローしちゃうので @Rule を付けたフィールドでは使えません。こういうときは二通りの解決が出来ます。

// メソッドに Rule をつける
    @Rule
    public DbUnitTester tester() throws ClassNotFoundException {
        return new DbUnitTester("org.h2.Driver", "jdbc:h2:tcp://localhost/db;SCHEMA=ut", "sa", "", "ut") {
            // 略
        };
    }

// メソッドを作る
    @Rule
    public DbUnitTester tester = DbUnitTester.create();

別に「Ruleはフィールドに付与し、コンストラクタを呼ばなければならない」なんて制約はありませんので。


それっぽく書くとこんな感じになるます。

実際は 'UserDaoDbUNitTeser' のようなものを作るでしょうし、どうでも良いことになるでしょうけども、もし「コンストラクタが例外を投げるとRuleに出来ない!」なんてなると勿体ないので。
で、TableごとにDrop-CreateのようなRuleを作るなら、いちいちコンストラクタで渡すのは面倒だから 'PropertiesBasedJdbcDatabaseTester' を使うことになると思います。接続情報とか散らばるの良く無い。

Ruleが増えてきた場合(RuleChain)

Ruleを使うようになると、そのうちセットでRuleをつけていくようになるかもしれません。

public class HogeTest {

    @Rule
    public TestRuleA a = new TestRuleA();

    @Rule
    public TestRuleB a = new TestRuleB();

    @Rule
    public TestRuleC a = new TestRuleC();

// Tests...
}

Ruleを選択して付けたり剥がしたりできるのはRuleの魅力の一つですが、セットで付ける場合(かつそのRuleの参照を必要としない場合)には無駄に行をとっているように見えます。こういうときはRuleChainですね。(P.159)

public class HogeTest {

    @Rule
    public RuleChain chain = RuleChain.outerRule(new TestRuleC())
            .around(new TestRuleB()).around(new TestRuleA());

// Tests...
}

元の記述ではRuleフィールドの定義順のように思えて、実際の適用順序は決まってなかったりします。このあたりはJava7やJUnit4.11で突然テストの実行順序が変わって戸惑った方もいるかもしれませんが、アレと似たようなものです。*2
RuleChain の適用順が気になるかもしれませんが、大雑把に言うと「Ruleは各テストを包むもの」です。だから outerRule/around となっていて、 before/after などではないわけです。TestWatcher などが一つのRuleで前後処理ができることを思えば、前後だと謎ですよね。

RuleChainはまだうるさい

さて、RuleChain を使って1フィールドに収まったように見えますが、まだ問題は解決してなかったりします。うるさい(長い)し、「お決まりのRule」が変わって足したり引いたりするのは複数のテストにまたがるととても手間です。

てことで、こう。

public class HogeTest {

    @Rule
    public TestRule chain = HogeTests.hogeRules();

// Tests...
}

public class HogeTests {
    public static RuleChain hogeRules() {
        return RuleChain.outerRule(new TestRuleC())
                        .around(new TestRuleB()).around(new TestRuleA());
    }
}

適用するRuleのセットを選択してRuleChainを作るようなメソッドにする手もあります。ただしRuleのフィールドやメソッドを使いたい場合*3には、この手は素直には使えませんので、素直じゃないやりかたや、別の解決をしてください。


テストの基底クラスを作りたくなる誘惑との戦いは続きます(しみじみ)

*1:JUnit3スタイルの 'HogeHogeTest extends DBTestCase' で使用されるDBTestCaseでは PropertiesBasedJdbcDatabaseTester が使われてますね。

*2:JUnit4.11でのデフォルトのテスト実行順序は「hashCode→テストメソッド名で比較」とか、テストクラスを継承している場合はそれぞれのクラス内でしかソートしないとか、なかなか直感的でないものになってたりします。これに関しては色々あるので別途書こうと思ってます。

*3:TemporaryFolderさんとか。べんり。