LoginSignup
15

More than 5 years have passed since last update.

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

Posted at

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

背景

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

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

DbSetup

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

  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クラスのインスタンスとして定義していくことになります。

固定値を指定してテストデータを投入する操作

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();

連続した値のテストデータを投入する操作

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();

テーブルのレコードを削除する操作

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をスキップする」よう指定します。
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プロジェクトの形式でサンプルコードを置いています。MyBatisやDBマイグレーションツールであるliquibaseとSpringの連携の部分も、あまり馴染みのない方にとっては参考になるかもしれません。

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

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
15