Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る ( その54 )( TestDataResource クラスの機能追加 )
概要
Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る ( その53 )( 貸出申請結果確認画面の作成5 ) の続きです。
- 今回の手順で確認できるのは以下の内容です。
参照したサイト・書籍
Repeating Annotations
http://docs.oracle.com/javase/tutorial/java/annotations/repeating.html(o1, o2) -> o1 - o2 なんて呪文はもうやめて! - Java8でのComparatorの使い方
http://qiita.com/tag1216/items/50ecf6a7bc10218ee889How to get a class instance of generics type T
http://stackoverflow.com/questions/3437897/how-to-get-a-class-instance-of-generics-type-t
目次
- feature/88-issue ブランチの作成
- TestDataResource クラスでバックアップ&ロード&リストアするテストデータをアノテーションで指定できるようにする
- SQL をアノテーションで指定してベースデータのバックアップ&ロード後に実行されるようにする
- @TestData アノテーションで指定されたデータのロード後にアノテーションで指定された SQL が実行されるようにする
- commit、Push、Pull Request、マージ
- 続く。。。
手順
feature/88-issue ブランチの作成
- feature/88-issue ブランチを作成します。
TestDataResource クラスでバックアップ&ロード&リストアするテストデータをアノテーションで指定できるようにする
実装仕様
以下の仕様で実装します。
- @BaseTestData アノテーションをクラス、TestDataResource クラスのフィールドあるいはメソッドに指定することで、バックアップ&ロード&リストアするテストデータを指定できるようにします。
- @BaseTestData アノテーションによる指定がない場合には、これまで通り TestDataResource クラス内の定数 TESTDATA_BASE_DIR に指定されたテストデータを使用します。
- @BaseTestData アノテーションはクラス、TestDataResource クラスのフィールドあるいはメソッドにそれぞれ1つだけ付加することができます。
- 優先順位はメソッド>TestDataResource クラスのフィールド>クラスの順とし、優先順位の高い @BaseTestData アノテーションのみ使用することとします。例えばメソッドとクラスに @BaseTestData アノテーションが付加されている場合にはメソッドに付加された @BaseTestData アノテーションのみ使用します。
バックアップ&ロード&リストアするテストデータを指定できる仕組みを実装する
src/test/java/ksbysample/common/test/rule/db の下に BaseTestData.java を作成します。作成後、リンク先の内容 に変更します。
src/test/java/ksbysample/common/test/rule/db の下の TestDataResource.java を リンク先のその1の内容 に変更します。
動作確認
動作確認します。現在 lending_app, lending_book テーブルには以下のデータが登録されている状況です。
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 'テストメソッド()'」を選択してテストを実行します。
予想通りテストはエラーになりました。
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 'テストメソッド()'」を選択してテストを実行します。テストが成功することが確認できます。
今度はフィールドに @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 'テストメソッド()'」を選択してテストを実行します。テストが成功することが確認できます。
今度はテストメソッドに @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 'テストメソッド()'」を選択してテストを実行します。テストが成功することが確認できます。
最後に @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 'テストメソッド()'」を選択してテストを実行します。テストが成功することが確認できます。
一旦 commit します。
SQL をアノテーションで指定してベースデータのバックアップ&ロード後に実行されるようにする
実装仕様
以下の仕様で実装します。
- @BaseTestSql アノテーションをクラス、TestDataResource クラスのフィールドあるいはメソッドに付加できるようにします。
- @BaseTestSql アノテーションは複数付加できるようにします。
- @BaseTestSql アノテーションで SQL 文と実行順序を指定し、ベースのテストデータのバックアップ&ロード後に指定された実行順序で SQL が実行されるようにします。
- 実行順序は省略可能とし、指定がない場合には 1 とみなします。
- クラス → TestDataResource クラスのフィールド → メソッドの順に付加されている @BaseTestSql アノテーションの SQL を実行します。
SQL を指定・実行する仕組みを実装する
src/test/java/ksbysample/common/test/rule/db の下に BaseTestSql.java を作成します。作成後、リンク先の内容 に変更します。
src/test/java/ksbysample/common/test/rule/db の下に BaseTestSqlList.java を作成します。作成後、リンク先の内容 に変更します。
src/test/java/ksbysample/common/test/rule/db の下に TestSqlExecutor.java を作成します。作成後、リンク先の内容 に変更します。
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 'テストメソッド()'」を選択してテストを実行します。テストが成功することが確認できます。
src/test/resources/testdata/assertdata の下の user_role.csv を以下の内容にします。role_id = 6, 9 の role を ROLE_USER
→ ROLE_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 'テストメソッド()'」を選択してテストを実行します。テストが成功することが確認できます。
src/test/resources/testdata/assertdata の下の user_info.csv を以下の内容にします。user_id = 1 の username を tanaka taro
→ tanaka 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 'テストメソッド()'」を選択してテストを実行します。テストが成功することが確認できます。
一旦 commit します。
@TestData アノテーションで指定されたデータのロード後にアノテーションで指定された SQL が実行されるようにする
実装仕様
以下の仕様で実装します。
- @TestSql アノテーションをメソッドに付加できるようにします。
- @TestSql アノテーションは複数付加できるようにします。
- @TestSql アノテーションで SQL 文と実行順序を指定し、@TestData アノテーションで指定されたテストデータのロード後に指定された実行順序で SQL が実行されるようにします。
- 実行順序は省略可能とし、指定がない場合には 1 とみなします。
SQL を指定・実行する仕組みを実装する
src/test/java/ksbysample/common/test/rule/db の下に TestSql.java を作成します。作成後、リンク先の内容 に変更します。
src/test/java/ksbysample/common/test/rule/db の下に TestSqlList.java を作成します。作成後、リンク先の内容 に変更します。
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 'テストメソッド()'」を選択してテストを実行します。が、なぜかエラーになりました。。。
原因を調査した結果、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 'テストメソッド()'」を選択してテストを実行します。今度はテストが成功することが確認できます。
一旦 commit します。
commit、Push、Pull Request、マージ
- 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_DIR
→testDataBaseDir
へ変更します。
- 第2引数に
- starting メソッドの以下の点を変更します。
String testDataBaseDir = getBaseTestDir(description);
を追加します。testDataLoader.load(...)
の引数をTESTDATA_BASE_DIR
→testDataBaseDir
へ変更します。
■その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) { } } }
■その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) { } } }
■今回の修正後の全体ソース
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
初版発行。