かんがるーさんの日記

最近自分が興味をもったものを調べた時の手順等を書いています。今は Spring Boot をいじっています。

Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る ( その54 )( TestDataResource クラスの機能追加 )

概要

Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る ( その53 )( 貸出申請結果確認画面の作成5 ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • TestDataResource クラスの機能追加・変更
      • バックアップ&ロード&リストアするテストデータは TestDataResource クラス内の定数に記述しているが、アノテーションで指定できるようにする。
      • SQLアノテーションで指定してベースデータのバックアップ&ロード後に実行されるようにする。
      • 同じく SQLアノテーションで指定して、@TestData アノテーションで指定されたデータのロード後に実行されるようにする。

参照したサイト・書籍

  1. Repeating Annotations
    http://docs.oracle.com/javase/tutorial/java/annotations/repeating.html

  2. (o1, o2) -> o1 - o2 なんて呪文はもうやめて! - Java8でのComparatorの使い方
    http://qiita.com/tag1216/items/50ecf6a7bc10218ee889

    • Stream API の sorted メソッドでソート順を指定する方法を調べた時に参照しました。
  3. How to get a class instance of generics type T
    http://stackoverflow.com/questions/3437897/how-to-get-a-class-instance-of-generics-type-t

目次

  1. feature/88-issue ブランチの作成
  2. TestDataResource クラスでバックアップ&ロード&リストアするテストデータをアノテーションで指定できるようにする
    1. 実装仕様
    2. バックアップ&ロード&リストアするテストデータを指定できる仕組みを実装する
    3. 動作確認
  3. SQL をアノテーションで指定してベースデータのバックアップ&ロード後に実行されるようにする
    1. 実装仕様
    2. SQL を指定・実行する仕組みを実装する
    3. 動作確認
  4. @TestData アノテーションで指定されたデータのロード後にアノテーションで指定された SQL が実行されるようにする
    1. 実装仕様
    2. SQL を指定・実行する仕組みを実装する
    3. 動作確認
  5. commit、Push、Pull Request、マージ
  6. 続く。。。

手順

feature/88-issue ブランチの作成

  1. feature/88-issue ブランチを作成します。

TestDataResource クラスでバックアップ&ロード&リストアするテストデータをアノテーションで指定できるようにする

実装仕様

以下の仕様で実装します。

  • @BaseTestData アノテーションをクラス、TestDataResource クラスのフィールドあるいはメソッドに指定することで、バックアップ&ロード&リストアするテストデータを指定できるようにします。
  • @BaseTestData アノテーションによる指定がない場合には、これまで通り TestDataResource クラス内の定数 TESTDATA_BASE_DIR に指定されたテストデータを使用します。
  • @BaseTestData アノテーションはクラス、TestDataResource クラスのフィールドあるいはメソッドにそれぞれ1つだけ付加することができます。
  • 優先順位はメソッド>TestDataResource クラスのフィールド>クラスの順とし、優先順位の高い @BaseTestData アノテーションのみ使用することとします。例えばメソッドとクラスに @BaseTestData アノテーションが付加されている場合にはメソッドに付加された @BaseTestData アノテーションのみ使用します。

バックアップ&ロード&リストアするテストデータを指定できる仕組みを実装する

  1. src/test/java/ksbysample/common/test/rule/db の下に BaseTestData.java を作成します。作成後、リンク先の内容 に変更します。

  2. src/test/java/ksbysample/common/test/rule/db の下の TestDataResource.javaリンク先のその1の内容 に変更します。

動作確認

動作確認します。現在 lending_app, lending_book テーブルには以下のデータが登録されている状況です。

f:id:ksby:20160213190909p:plain

src/test/java/ksbysample/common/test/rule/db の下の TestDataResource.java の TESTDATA_BASE_DIR 定数の値を存在しないパスに変更します。

@Component
public class TestDataResource extends TestWatcher {

    private static final String TESTDATA_BASE_DIR = "src/test/resources/testdata/base/001";
    private static final String BACKUP_FILE_NAME = "ksbylending_backup";

src/test/java/ksbysample/webapp/lending の下に TestDataResourceTest.java を作成し、以下の内容に変更します。

package ksbysample.webapp.lending;

import ksbysample.common.test.rule.db.AssertOptions;
import ksbysample.common.test.rule.db.BaseTestData;
import ksbysample.common.test.rule.db.TableDataAssert;
import ksbysample.common.test.rule.db.TestDataResource;
import ksbysample.common.test.rule.mockmvc.SecurityMockMvcResource;
import org.dbunit.dataset.IDataSet;
import org.dbunit.dataset.csv.CsvDataSet;
import org.junit.Rule;
import org.junit.Test;
import org.junit.experimental.runners.Enclosed;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;

import javax.sql.DataSource;
import java.io.File;

@RunWith(Enclosed.class)
public class TestDataResourceTest {

    @RunWith(SpringJUnit4ClassRunner.class)
    @SpringApplicationConfiguration(classes = Application.class)
    @WebAppConfiguration
    public static class テストクラス {

//        @Rule
//        @Autowired
//        public TestDataResource testDataResource;

        @Autowired
        private DataSource dataSource;

        @Test
        public void テストメソッド() throws Exception {
            IDataSet dataSet = new CsvDataSet(new File("src/test/resources/testdata/base"));
            TableDataAssert tableDataAssert = new TableDataAssert(dataSet, dataSource);
            tableDataAssert.assertEquals("lending_app", new String[]{}, AssertOptions.EXCLUDE_COLUM);
            tableDataAssert.assertEquals("lending_book", new String[]{}, AssertOptions.EXCLUDE_COLUM);
            tableDataAssert.assertEquals("library_forsearch", new String[]{}, AssertOptions.EXCLUDE_COLUM);
            tableDataAssert.assertEquals("user_info", new String[]{}, AssertOptions.EXCLUDE_COLUM);
            tableDataAssert.assertEquals("user_role", new String[]{}, AssertOptions.EXCLUDE_COLUM);
        }

    }

}

最初にテストデータがロードされなければテストが失敗することを確認します。テストメソッドの左側の矢印アイコンをクリックしてコンテキストメニューを表示し、「Run 'テストメソッド()'」を選択してテストを実行します。

予想通りテストはエラーになりました。

f:id:ksby:20160213192056p:plain

testDataResource フィールドのコメントアウトを解除し、テストクラスに @BaseTestData("src/test/resources/testdata/base") を指定します。

@RunWith(Enclosed.class)
public class TestDataResourceTest {

    @RunWith(SpringJUnit4ClassRunner.class)
    @SpringApplicationConfiguration(classes = Application.class)
    @WebAppConfiguration
    @BaseTestData("src/test/resources/testdata/base")
    public static class テストクラス {

        @Rule
        @Autowired
        public TestDataResource testDataResource;

        ..........

    }

}

「Run 'テストメソッド()'」を選択してテストを実行します。テストが成功することが確認できます。

f:id:ksby:20160213192816p:plain

今度はフィールドに @BaseTestData("src/test/resources/testdata/base") を指定します。

@RunWith(Enclosed.class)
public class TestDataResourceTest {

    @RunWith(SpringJUnit4ClassRunner.class)
    @SpringApplicationConfiguration(classes = Application.class)
    @WebAppConfiguration
    public static class テストクラス {

        @Rule
        @Autowired
        @BaseTestData("src/test/resources/testdata/base")
        public TestDataResource testDataResource;

        ..........

    }

}

「Run 'テストメソッド()'」を選択してテストを実行します。テストが成功することが確認できます。

f:id:ksby:20160213193137p:plain

今度はテストメソッド@BaseTestData("src/test/resources/testdata/base") を指定します。

@RunWith(Enclosed.class)
public class TestDataResourceTest {

    @RunWith(SpringJUnit4ClassRunner.class)
    @SpringApplicationConfiguration(classes = Application.class)
    @WebAppConfiguration
    public static class テストクラス {

        @Rule
        @Autowired
        public TestDataResource testDataResource;

        ..........

        @Test
        @BaseTestData("src/test/resources/testdata/base")
        public void テストメソッド() throws Exception {
            ..........
        }

    }

}

「Run 'テストメソッド()'」を選択してテストを実行します。テストが成功することが確認できます。

f:id:ksby:20160213194326p:plain

最後に @BaseTestData が指定されていない場合には TestDataResource クラスの定数が使用されることを確認します。テストクラス内では @BaseTestData を指定しないようにします。

@RunWith(Enclosed.class)
public class TestDataResourceTest {

    @RunWith(SpringJUnit4ClassRunner.class)
    @SpringApplicationConfiguration(classes = Application.class)
    @WebAppConfiguration
    public static class テストクラス {

        @Rule
        @Autowired
        public TestDataResource testDataResource;

        ..........

        @Test
        public void テストメソッド() throws Exception {
            ..........
        }

    }

}

src/test/java/ksbysample/common/test/rule/db の下の TestDataResource.java の TESTDATA_BASE_DIR 定数の値を元のパスに戻します。

@Component
public class TestDataResource extends TestWatcher {

    private static final String TESTDATA_BASE_DIR = "src/test/resources/testdata/base";
    private static final String BACKUP_FILE_NAME = "ksbylending_backup";

「Run 'テストメソッド()'」を選択してテストを実行します。テストが成功することが確認できます。

f:id:ksby:20160213195134p:plain

一旦 commit します。

SQLアノテーションで指定してベースデータのバックアップ&ロード後に実行されるようにする

実装仕様

以下の仕様で実装します。

  • @BaseTestSql アノテーションをクラス、TestDataResource クラスのフィールドあるいはメソッドに付加できるようにします。
  • @BaseTestSql アノテーション複数付加できるようにします。
  • @BaseTestSql アノテーションSQL 文と実行順序を指定し、ベースのテストデータのバックアップ&ロード後に指定された実行順序で SQL が実行されるようにします。
  • 実行順序は省略可能とし、指定がない場合には 1 とみなします。
  • クラス → TestDataResource クラスのフィールド → メソッドの順に付加されている @BaseTestSql アノテーションSQL を実行します。

SQL を指定・実行する仕組みを実装する

  1. src/test/java/ksbysample/common/test/rule/db の下に BaseTestSql.java を作成します。作成後、リンク先の内容 に変更します。

  2. src/test/java/ksbysample/common/test/rule/db の下に BaseTestSqlList.java を作成します。作成後、リンク先の内容 に変更します。

  3. src/test/java/ksbysample/common/test/rule/db の下に TestSqlExecutor.java を作成します。作成後、リンク先の内容 に変更します。

  4. src/test/java/ksbysample/common/test/rule/db の下の TestDataResource.javaリンク先のその2の内容 に変更します。

動作確認

動作確認します。src/test/resources/testdata の下に assertdata ディレクトリを作成し、作成した src/test/resources/testdata/assertdata の下に src/test/resources/testdata/base の下のファイルを全てコピーします。

src/test/resources/testdata/assertdata の下の library_forsearch.csv を以下の内容に変更します。

systemid,formal
Kanagawa_Sample,図書館サンプル

src/test/resources/ksbysample/webapp/lending の下の TestDataResourceTest.java を以下の内容に変更します。テストクラスに @BaseTestSql を付加します。

@RunWith(Enclosed.class)
public class TestDataResourceTest {

    @RunWith(SpringJUnit4ClassRunner.class)
    @SpringApplicationConfiguration(classes = Application.class)
    @WebAppConfiguration
    @BaseTestSql(order = 1, sql = "insert into library_forsearch values ('Kanagawa_Sample', null)")
    @BaseTestSql(order = 2, sql = "update library_forsearch set formal = '図書館サンプル' where systemid = 'Kanagawa_Sample'")
    public static class テストクラス {

        @Rule
        @Autowired
        public TestDataResource testDataResource;

        ..........

        @Test
        public void テストメソッド() throws Exception {
            IDataSet dataSet = new CsvDataSet(new File("src/test/resources/testdata/assertdata"));
            TableDataAssert tableDataAssert = new TableDataAssert(dataSet, dataSource);
            tableDataAssert.assertEquals("lending_app", new String[]{}, AssertOptions.EXCLUDE_COLUM);
            tableDataAssert.assertEquals("lending_book", new String[]{}, AssertOptions.EXCLUDE_COLUM);
            tableDataAssert.assertEquals("library_forsearch", new String[]{}, AssertOptions.EXCLUDE_COLUM);
            tableDataAssert.assertEquals("user_info", new String[]{}, AssertOptions.EXCLUDE_COLUM);
            tableDataAssert.assertEquals("user_role", new String[]{}, AssertOptions.EXCLUDE_COLUM);
        }

    }

}

「Run 'テストメソッド()'」を選択してテストを実行します。テストが成功することが確認できます。

f:id:ksby:20160214093543p:plain

src/test/resources/testdata/assertdata の下の user_role.csv を以下の内容にします。role_id = 6, 9 の role を ROLE_USERROLE_APPROVER に変更しています。

role_id,user_id,role
1,1,ROLE_USER
2,1,ROLE_ADMIN
3,1,ROLE_APPROVER
4,2,ROLE_USER
5,2,ROLE_APPROVER
6,3,ROLE_APPROVER
7,4,ROLE_USER
8,5,ROLE_USER
9,8,ROLE_APPROVER

src/test/resources/ksbysample/webapp/lending の下の TestDataResourceTest.java を以下の内容に変更します。テストクラスの @BaseTestSql はそのままで、今度はフィールドにも @BaseTestSql を付加します。

@RunWith(Enclosed.class)
public class TestDataResourceTest {

    @RunWith(SpringJUnit4ClassRunner.class)
    @SpringApplicationConfiguration(classes = Application.class)
    @WebAppConfiguration
    @BaseTestSql(order = 1, sql = "insert into library_forsearch values ('Kanagawa_Sample', null)")
    @BaseTestSql(order = 2, sql = "update library_forsearch set formal = '図書館サンプル' where systemid = 'Kanagawa_Sample'")
    public static class テストクラス {

        @Rule
        @Autowired
        @BaseTestSql(sql = "update user_role set role = 'ROLE_APPROVER' where role_id in (6, 9)")
        public TestDataResource testDataResource;

        ..........

        @Test
        public void テストメソッド() throws Exception {
            ..........
        }

    }

}

「Run 'テストメソッド()'」を選択してテストを実行します。テストが成功することが確認できます。

f:id:ksby:20160216060413p:plain

src/test/resources/testdata/assertdata の下の user_info.csv を以下の内容にします。user_id = 1 の username を tanaka tarotanaka jiro へ変更し、user_id = 4 ~ 8 のデータを削除しています。

user_id,username,password,mail_address,enabled,cnt_badcredentials,expired_account,expired_password
1,"tanaka jiro",$2a$10$LKKepbcPCiT82NxSIdzJr.9ph.786Mxvr.VoXFl4hNcaaAn9u7jje,tanaka.taro@sample.com,1,0,"9999-12-31 23:59:00.000000","9999-12-31 23:59:00.000000"
2,"suzuki hanako",$2a$10$.fiPEZ155Rl41/e.mdM3A.mG0iEQNPmhjFL/aIiV8dZnXsCd.oqji,suzuki.hanako@test.co.jp,1,0,"9999-12-31 23:59:00.000000","9999-12-31 23:59:00.000000"
3,"kimura masao",$2a$10$yP1dLPIq9j7WQVH6ruSwkepf8jIkPxTtncbSnYM0/jAGQ4HCQO8R.,kimura.masao@test.co.jp,0,0,"2015-12-31 22:30:54.425000","2015-10-15 22:31:03.316000"

src/test/resources/testdata/assertdata の下の user_role.csv を以下の内容にします。role_id = 7 ~ 9 のデータを削除しています。

role_id,user_id,role
1,1,ROLE_USER
2,1,ROLE_ADMIN
3,1,ROLE_APPROVER
4,2,ROLE_USER
5,2,ROLE_APPROVER
6,3,ROLE_APPROVER

src/test/resources/ksbysample/webapp/lending の下の TestDataResourceTest.java を以下の内容に変更します。テストクラスとフィールドの @BaseTestSql はそのままで、今度はテストメソッドにも @BaseTestSql を付加します。

@RunWith(Enclosed.class)
public class TestDataResourceTest {

    @RunWith(SpringJUnit4ClassRunner.class)
    @SpringApplicationConfiguration(classes = Application.class)
    @WebAppConfiguration
    @BaseTestSql(order = 1, sql = "insert into library_forsearch values ('Kanagawa_Sample', null)")
    @BaseTestSql(order = 2, sql = "update library_forsearch set formal = '図書館サンプル' where systemid = 'Kanagawa_Sample'")
    public static class テストクラス {

        @Rule
        @Autowired
        @BaseTestSql(sql = "update user_role set role = 'ROLE_APPROVER' where role_id in (6, 9)")
        public TestDataResource testDataResource;

        ..........

        @Test
        @BaseTestSql(sql = "delete from user_info where user_id in (4, 5, 6, 7, 8)")
        @BaseTestSql(sql = "update user_info set username = 'tanaka jiro' where user_id = 1")
        public void テストメソッド() throws Exception {
            ..........
        }

    }

}

「Run 'テストメソッド()'」を選択してテストを実行します。テストが成功することが確認できます。

f:id:ksby:20160216061428p:plain

一旦 commit します。

@TestData アノテーションで指定されたデータのロード後にアノテーションで指定された SQL が実行されるようにする

実装仕様

以下の仕様で実装します。

SQL を指定・実行する仕組みを実装する

  1. src/test/java/ksbysample/common/test/rule/db の下に TestSql.java を作成します。作成後、リンク先の内容 に変更します。

  2. src/test/java/ksbysample/common/test/rule/db の下に TestSqlList.java を作成します。作成後、リンク先の内容 に変更します。

  3. src/test/java/ksbysample/common/test/rule/db の下の TestDataResource.javaリンク先のその3の内容 に変更します。

動作確認

動作確認します。

src/test/resources/ksbysample/webapp/lending/web/confirmresult/testdata/001 の下の lending_app.csv, lending_book.csv を src/test/resources/testdata/assertdata の下にコピーします。

src/test/resources/testdata/assertdata の下の lending_app.csv を以下の内容にします。status の値を 4 → 3 へ変更しています。

lending_app_id,status,lending_user_id,approval_user_id,version
105,3,1,2,2

src/test/resources/ksbysample/webapp/lending の下の TestDataResourceTest.java を以下の内容に変更します。テストメソッドに @TestData と @TestSql を1つ付加します。

@RunWith(Enclosed.class)
public class TestDataResourceTest {

    @RunWith(SpringJUnit4ClassRunner.class)
    @SpringApplicationConfiguration(classes = Application.class)
    @WebAppConfiguration
    @BaseTestSql(order = 1, sql = "insert into library_forsearch values ('Kanagawa_Sample', null)")
    @BaseTestSql(order = 2, sql = "update library_forsearch set formal = '図書館サンプル' where systemid = 'Kanagawa_Sample'")
    public static class テストクラス {

        @Rule
        @Autowired
        @BaseTestSql(sql = "update user_role set role = 'ROLE_APPROVER' where role_id in (6, 9)")
        public TestDataResource testDataResource;

        ..........

        @Test
        @BaseTestSql(sql = "delete from user_info where user_id in (4, 5, 6, 7, 8)")
        @BaseTestSql(sql = "update user_info set username = 'tanaka jiro' where user_id = 1")
        @TestData("src/test/resources/ksbysample/webapp/lending/web/confirmresult/testdata/001")
        @TestSql(sql = "update lending_app set status = '3' where lending_app_id = 105")
        public void テストメソッド() throws Exception {
            ..........
        }

    }

}

「Run 'テストメソッド()'」を選択してテストを実行します。が、なぜかエラーになりました。。。

f:id:ksby:20160217092106p:plain

原因を調査した結果、src/test/resources/testdata/assertdata/lending_book.csv のデータが lending_book_id の昇順になっていなかったからでした。昇順になるよう修正します。

lending_book_id,lending_app_id,isbn,book_name,lending_state,lending_app_flg,lending_app_reason,approval_result,approval_reason,version
521,105,978-4-7741-6366-6,GitHub実践入門,蔵書なし,[null],[null],[null],[null],1
522,105,978-4-7741-5377-3,JUnit実践入門,蔵書あり,1,開発で使用する為,2,購入済です,2
523,105,978-4-7973-8014-9,Java最強リファレンス,蔵書あり,,[null],[null],[null],1
524,105,978-4-7973-4778-4,アジャイルソフトウェア開発の奥義,蔵書あり,1,勉強の為,1,,2
525,105,978-4-87311-704-1,Javaによる関数型プログラミング,蔵書あり,1,勉強会の調査の為,1,,2

再度「Run 'テストメソッド()'」を選択してテストを実行します。今度はテストが成功することが確認できます。

f:id:ksby:20160217093532p:plain

一旦 commit します。

commit、Push、Pull Request、マージ

  1. GitHub へ Push、feature/88-issue -> 1.0.x へ Pull Request、1.0.x でマージ、feature/88-issue ブランチを削除、をします。

続く。。。

TestDataResource クラスの機能追加・変更を続けます。以下の機能を実装する予定です。

  • 最後のテストが失敗した時の調査で dataSource.getConnection() で取得する Connection インターフェースのソースを見た時に AutoCloseable インターフェースを継承していることに気づきました。try-with-resources 構文で自動 close するように書いていませんね。忘れていました。。。 修正します。
  • 同名のアノテーション複数付加する方法が分かったので @TestData アノテーション複数付加できるようにします。
  • テストデータのパスが長いので、クラス内の定数及びアノテーションでルートパスを設定できるようにします。@BaseTestData, @TestData アノテーションではルートパスを除く部分のみ指定すればよくなります。

ソースコード

BaseTestData.java

package ksbysample.common.test.rule.db;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Target({ TYPE, FIELD, METHOD })
@Retention(RUNTIME)
@Documented
public @interface BaseTestData {

    String value();

}

TestDataResource.java

■その1

@Component
public class TestDataResource extends TestWatcher {

    ..........

    @Override
    protected void starting(Description description) {
        IDatabaseConnection conn = null;
        try {
            // @NouseTestDataResource アノテーションがテストメソッドに付加されていない場合には処理を実行する
            if (!hasNoUseTestDataResourceAnnotation(description)) {
                conn = DbUnitUtils.createDatabaseConnection(dataSource);

                // バックアップ&ロード&リストア対象のテストデータのパスを取得する
                String testDataBaseDir = getBaseTestDir(description);

                // バックアップを取得する
                backupDb(conn, testDataBaseDir);

                // テストデータをロードする
                testDataLoader.load(testDataBaseDir);

                // テストメソッドに @TestData アノテーションが付加されている場合には、
                // アノテーションで指定されたテストデータをロードする
                loadTestData(description);
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            try {
                if (conn != null) conn.close();
            } catch (Exception ignored) {
            }
        }
    }

    ..........

    private String getBaseTestDir(Description description) {
        // @BaseTestData アノテーションで指定されている場合にはそれを使用し、指定されていない場合には
        // TESTDATA_BASE_DIR 定数で指定されているものと使用する

        // テストメソッドに @BaseTestData アノテーションが付加されているかチェックする
        BaseTestData baseTestData = description.getAnnotation(BaseTestData.class);
        if (baseTestData != null) {
            return baseTestData.value();
        }

        // TestDataResource クラスのフィールドに @BaseTestData アノテーションが付加されているかチェックする
        Field[] fields = description.getTestClass().getDeclaredFields();
        for (Field field : fields) {
            if (field.getType().equals(TestDataResource.class)) {
                baseTestData = field.getAnnotation(BaseTestData.class);
                if (baseTestData != null) {
                    return baseTestData.value();
                }
            }
        }

        // テストクラスに @BaseTestData アノテーションが付加されているかチェックする
        Class<?> testClass = description.getTestClass();
        baseTestData = testClass.getAnnotation(BaseTestData.class);
        if (baseTestData != null) {
            return baseTestData.value();
        }

        return TESTDATA_BASE_DIR;
    }

    private void backupDb(IDatabaseConnection conn, String testDataBaseDir)
            throws DataSetException, IOException {
        QueryDataSet partialDataSet = new QueryDataSet(conn);

        // TESTDATA_BASE_DIR で指定されたディレクトリ内の table-ordering.txt に記述されたテーブル名一覧を取得し、
        // バックアップテーブルとしてセットする
        List<String> backupTableList = Files.readAllLines(Paths.get(testDataBaseDir, "table-ordering.txt"));
        for (String backupTable : backupTableList) {
            partialDataSet.addTable(backupTable);
        }

        ReplacementDataSet replacementDatasetBackup = new ReplacementDataSet(partialDataSet);
        replacementDatasetBackup.addReplacementObject(null, DbUnitUtils.NULL_STRING);
        this.backupFile = File.createTempFile(BACKUP_FILE_NAME, "xml");
        try (FileOutputStream fos = new FileOutputStream(this.backupFile)) {
            FlatXmlDataSet.write(replacementDatasetBackup, fos);
        }
    }

    ..........

}
  • private String getBaseTestDir(Description description) メソッドを追加します。
  • backupDb メソッドの以下の点を変更します。
    • 第2引数に String testDataBaseDir を追加します。
    • TESTDATA_BASE_DIRtestDataBaseDir へ変更します。
  • starting メソッドの以下の点を変更します。
    • String testDataBaseDir = getBaseTestDir(description); を追加します。
    • testDataLoader.load(...) の引数を TESTDATA_BASE_DIRtestDataBaseDir へ変更します。

■その2

    @Override
    protected void starting(Description description) {
        IDatabaseConnection conn = null;
        try {
            // @NouseTestDataResource アノテーションがテストメソッドに付加されていない場合には処理を実行する
            if (!hasNoUseTestDataResourceAnnotation(description)) {
                conn = DbUnitUtils.createDatabaseConnection(dataSource);

                // バックアップ&ロード&リストア対象のテストデータのパスを取得する
                String testDataBaseDir = getBaseTestDir(description);

                // バックアップを取得する
                backupDb(conn, testDataBaseDir);

                // テストデータをロードする
                testDataLoader.load(testDataBaseDir);

                // @BaseTestSql アノテーションで指定された SQL を実行する
                TestSqlExecutor<BaseTestSqlList, BaseTestSql> baseTestSqlExecutor
                        = new TestSqlExecutor<>(BaseTestSqlList.class, BaseTestSql.class);
                baseTestSqlExecutor.execute(dataSource, description);

                // テストメソッドに @TestData アノテーションが付加されている場合には、
                // アノテーションで指定されたテストデータをロードする
                loadTestData(description);
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            try {
                if (conn != null) conn.close();
            } catch (Exception ignored) {
            }
        }
    }
  • 「// @BaseTestSql アノテーションで指定された SQL を実行する」のコメントを付けている部分の処理を追加します。

■その3

    @Override
    protected void starting(Description description) {
        IDatabaseConnection conn = null;
        try {
            // @NouseTestDataResource アノテーションがテストメソッドに付加されていない場合には処理を実行する
            if (!hasNoUseTestDataResourceAnnotation(description)) {
                conn = DbUnitUtils.createDatabaseConnection(dataSource);

                // バックアップ&ロード&リストア対象のテストデータのパスを取得する
                String testDataBaseDir = getBaseTestDir(description);

                // バックアップを取得する
                backupDb(conn, testDataBaseDir);

                // テストデータをロードする
                testDataLoader.load(testDataBaseDir);

                // @BaseTestSql アノテーションで指定された SQL を実行する
                TestSqlExecutor<BaseTestSqlList, BaseTestSql> baseTestSqlExecutor
                        = new TestSqlExecutor<>(BaseTestSqlList.class, BaseTestSql.class);
                baseTestSqlExecutor.execute(dataSource, description);

                // テストメソッドに @TestData アノテーションが付加されている場合には、
                // アノテーションで指定されたテストデータをロードする
                loadTestData(description);

                // @TestSql アノテーションで指定された SQL を実行する
                TestSqlExecutor<TestSqlList, TestSql> testSqlExecutor
                        = new TestSqlExecutor<>(TestSqlList.class, TestSql.class);
                testSqlExecutor.execute(dataSource, description);
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            try {
                if (conn != null) conn.close();
            } catch (Exception ignored) {
            }
        }
    }
  • 「// @TestSql アノテーションで指定された SQL を実行する」のコメントを付けている部分の処理を追加します。

■今回の修正後の全体ソース

package ksbysample.common.test.rule.db;

import org.dbunit.DatabaseUnitException;
import org.dbunit.database.IDatabaseConnection;
import org.dbunit.database.QueryDataSet;
import org.dbunit.dataset.DataSetException;
import org.dbunit.dataset.IDataSet;
import org.dbunit.dataset.ReplacementDataSet;
import org.dbunit.dataset.xml.FlatXmlDataSet;
import org.dbunit.dataset.xml.FlatXmlDataSetBuilder;
import org.dbunit.operation.DatabaseOperation;
import org.junit.rules.TestWatcher;
import org.junit.runner.Description;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.sql.DataSource;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.net.MalformedURLException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.sql.SQLException;
import java.util.Collection;
import java.util.List;

@Component
public class TestDataResource extends TestWatcher {

    private static final String TESTDATA_BASE_DIR = "src/test/resources/testdata/base";
    private static final String BACKUP_FILE_NAME = "ksbylending_backup";

    @Autowired
    private DataSource dataSource;

    @Autowired
    private TestDataLoader testDataLoader;

    private File backupFile;

    @Override
    protected void starting(Description description) {
        IDatabaseConnection conn = null;
        try {
            // @NouseTestDataResource アノテーションがテストメソッドに付加されていない場合には処理を実行する
            if (!hasNoUseTestDataResourceAnnotation(description)) {
                conn = DbUnitUtils.createDatabaseConnection(dataSource);

                // バックアップ&ロード&リストア対象のテストデータのパスを取得する
                String testDataBaseDir = getBaseTestDir(description);

                // バックアップを取得する
                backupDb(conn, testDataBaseDir);

                // テストデータをロードする
                testDataLoader.load(testDataBaseDir);

                // @BaseTestSql アノテーションで指定された SQL を実行する
                TestSqlExecutor<BaseTestSqlList, BaseTestSql> baseTestSqlExecutor
                        = new TestSqlExecutor<>(BaseTestSqlList.class, BaseTestSql.class);
                baseTestSqlExecutor.execute(dataSource, description);

                // テストメソッドに @TestData アノテーションが付加されている場合には、
                // アノテーションで指定されたテストデータをロードする
                loadTestData(description);

                // @TestSql アノテーションで指定された SQL を実行する
                TestSqlExecutor<TestSqlList, TestSql> testSqlExecutor
                        = new TestSqlExecutor<>(TestSqlList.class, TestSql.class);
                testSqlExecutor.execute(dataSource, description);
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            try {
                if (conn != null) conn.close();
            } catch (Exception ignored) {
            }
        }
    }

    @Override
    protected void finished(Description description) {
        IDatabaseConnection conn = null;
        try {
            // @NouseTestDataResource アノテーションがテストメソッドに付加されていない場合には処理を実行する
            if (!hasNoUseTestDataResourceAnnotation(description)) {
                conn = DbUnitUtils.createDatabaseConnection(dataSource);

                // バックアップからリストアする
                restoreDb(conn);
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            try {
                if (conn != null) conn.close();
            } catch (Exception ignored) {
            }

            if (backupFile != null) {
                try {
                    Files.delete(backupFile.toPath());
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }
                backupFile = null;
            }
        }
    }

    private boolean hasNoUseTestDataResourceAnnotation(Description description) {
        Collection<Annotation> annotationList = description.getAnnotations();
        boolean result = annotationList.stream()
                .anyMatch(annotation -> annotation instanceof NoUseTestDataResource);
        return result;
    }

    private String getBaseTestDir(Description description) {
        // @BaseTestData アノテーションで指定されている場合にはそれを使用し、指定されていない場合には
        // TESTDATA_BASE_DIR 定数で指定されているものと使用する

        // テストメソッドに @BaseTestData アノテーションが付加されているかチェックする
        BaseTestData baseTestData = description.getAnnotation(BaseTestData.class);
        if (baseTestData != null) {
            return baseTestData.value();
        }

        // TestDataResource クラスのフィールドに @BaseTestData アノテーションが付加されているかチェックする
        Field[] fields = description.getTestClass().getDeclaredFields();
        for (Field field : fields) {
            if (field.getType().equals(TestDataResource.class)) {
                baseTestData = field.getAnnotation(BaseTestData.class);
                if (baseTestData != null) {
                    return baseTestData.value();
                }
            }
        }

        // テストクラスに @BaseTestData アノテーションが付加されているかチェックする
        Class<?> testClass = description.getTestClass();
        baseTestData = testClass.getAnnotation(BaseTestData.class);
        if (baseTestData != null) {
            return baseTestData.value();
        }

        return TESTDATA_BASE_DIR;
    }

    private void backupDb(IDatabaseConnection conn, String testDataBaseDir)
            throws DataSetException, IOException {
        QueryDataSet partialDataSet = new QueryDataSet(conn);

        // TESTDATA_BASE_DIR で指定されたディレクトリ内の table-ordering.txt に記述されたテーブル名一覧を取得し、
        // バックアップテーブルとしてセットする
        List<String> backupTableList = Files.readAllLines(Paths.get(testDataBaseDir, "table-ordering.txt"));
        for (String backupTable : backupTableList) {
            partialDataSet.addTable(backupTable);
        }

        ReplacementDataSet replacementDatasetBackup = new ReplacementDataSet(partialDataSet);
        replacementDatasetBackup.addReplacementObject(null, DbUnitUtils.NULL_STRING);
        this.backupFile = File.createTempFile(BACKUP_FILE_NAME, "xml");
        try (FileOutputStream fos = new FileOutputStream(this.backupFile)) {
            FlatXmlDataSet.write(replacementDatasetBackup, fos);
        }
    }

    private void restoreDb(IDatabaseConnection conn)
            throws MalformedURLException, DatabaseUnitException, SQLException {
        if (this.backupFile != null) {
            IDataSet dataSet = new FlatXmlDataSetBuilder().build(this.backupFile);
            ReplacementDataSet replacementDatasetRestore = new ReplacementDataSet(dataSet);
            replacementDatasetRestore.addReplacementObject(DbUnitUtils.NULL_STRING, null);
            DatabaseOperation.CLEAN_INSERT.execute(conn, replacementDatasetRestore);
        }
    }

    private void loadTestData(Description description) {
        description.getAnnotations().stream()
                .filter(annotation -> annotation instanceof TestData)
                .forEach(annotation -> {
                    TestData testData = (TestData) annotation;
                    testDataLoader.load(testData.value());
                });
    }

}

BaseTestSql.java

package ksbysample.common.test.rule.db;

import java.lang.annotation.Documented;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Target({ TYPE, FIELD, METHOD })
@Retention(RUNTIME)
@Documented
@Repeatable(BaseTestSqlList.class)
public @interface BaseTestSql {

    long order() default 1;

    String sql();

}

BaseTestSqlList.java

package ksbysample.common.test.rule.db;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Target({ TYPE, FIELD, METHOD })
@Retention(RUNTIME)
@Documented
public @interface BaseTestSqlList {

    BaseTestSql[] value();

}

TestSqlExecutor.java

package ksbysample.common.test.rule.db;

import org.junit.runner.Description;

import javax.sql.DataSource;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Arrays;

import static java.util.Comparator.comparing;

public class TestSqlExecutor<L extends Annotation, I extends Annotation> {

    private Class<L> testSqlListClass;

    private Class<I> testSqlClass;

    public TestSqlExecutor(Class<L> testSqlListClass, Class<I> testSqlClass) {
        this.testSqlListClass = testSqlListClass;
        this.testSqlClass = testSqlClass;
    }

    public void execute(DataSource dataSource, Description description) throws SQLException {
        try (Statement stmt = dataSource.getConnection().createStatement()) {
            // テストクラスに付加されている @BaseTestSql, @TestSql アノテーションの SQL を実行する
            Class<?> testClass = description.getTestClass();
            executeTestSqlListOrTestSql(stmt
                    , testClass.getAnnotation(this.testSqlListClass)
                    , testClass.getAnnotation(this.testSqlClass));

            // TestDataResource クラスのフィールドに付加されている @BaseTestSql, @TestSql アノテーションの SQL を実行する
            Field[] fields = description.getTestClass().getDeclaredFields();
            for (Field field : fields) {
                if (field.getType().equals(TestDataResource.class)) {
                    executeTestSqlListOrTestSql(stmt
                            , field.getAnnotation(this.testSqlListClass)
                            , field.getAnnotation(this.testSqlClass));
                }
            }

            // テストメソッドに付加されている @BaseTestSql, @TestSql アノテーションの SQL を実行する
            executeTestSqlListOrTestSql(stmt
                    , description.getAnnotation(this.testSqlListClass)
                    , description.getAnnotation(this.testSqlClass));
        }
    }

    private void executeTestSqlListOrTestSql(Statement stmt, L testSqlList, I testSql) {
        executeTestSqlList(stmt, testSqlList);
        executeTestSql(stmt, testSql);
    }

    private void executeTestSqlList(Statement stmt, L testSqlList) {
        if (testSqlList != null) {
            Arrays.asList(value(testSqlList)).stream()
                    .sorted(comparing(testSql -> order(testSql)))
                    .forEach(testSql -> {
                        executeTestSql(stmt, testSql);
                    });
        }
    }

    private void executeTestSql(Statement stmt, I testSql) {
        if (testSql != null) {
            try {
                stmt.execute(sql(testSql));
            } catch (SQLException e) {
                throw new RuntimeException(e);
            }
        }
    }

    @SuppressWarnings("unchecked")
    private I[] value(L testSqlList) {
        try {
            Method method = testSqlList.getClass().getMethod("value");
            return (I[]) method.invoke(testSqlList);
        } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }

    private long order(I testSql) {
        try {
            Method method = testSql.getClass().getMethod("order");
            return (long) method.invoke(testSql);
        } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }

    private String sql(I testSql) {
        try {
            Method method = testSql.getClass().getMethod("sql");
            return (String) method.invoke(testSql);
        } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }

}
  • executeTestSqlListOrTestSql メソッドでは BaseTestSqlList.class, BaseTestSql.class それぞれで getAnnotation で @BaseTestSql アノテーションで付加された SQL を取得して実行していますが、これは @BaseTestSql アノテーションが1つだけ付加されている場合には BaseTestSql.class の方で、2つ以上付加されている場合には BaseTestSqlList.class の方でのみ @BaseTestSql アノテーションで付加した SQL を取得できるからです。

    最初は1つだけの場合も BaseTestSqlList.class で getAnnotation で取得できると思っていて、SQL が実行されず悩みました。。。

TestSql.java

package ksbysample.common.test.rule.db;

import java.lang.annotation.Documented;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Target({ METHOD })
@Retention(RUNTIME)
@Documented
@Repeatable(TestSqlList.class)
public @interface TestSql {

    long order() default 1;

    String sql();

}

TestSqlList.java

package ksbysample.common.test.rule.db;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Target({ METHOD })
@Retention(RUNTIME)
@Documented
public @interface TestSqlList {

    TestSql[] value();

}

履歴

2016/02/17
初版発行。