かんがるーさんの日記

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

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

概要

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

  • 今回の手順で確認できるのは以下の内容です。
    • 貸出申請結果確認画面の作成
      • テストの作成

参照したサイト・書籍

  1. GroovyでJUnitなテストを書くときの注意点……なんて無かった
    http://irof.hateblo.jp/entry/20121213/p1

    • Spock で @RunWith(Enclosed.class) を使用してテストを書く方法を調査した時に参照しました。

目次

  1. テスト作成対象のクラスを決める
  2. printClassWhatNotMakeTest タスクのチェック対象外のパッケージを設定する
  3. ConfirmresultController クラスのテストを Spock で書くとどうなるのか?
  4. ConfirmresultController クラスのテストの作成
  5. 全てのテストが成功するか確認する
  6. commit、Push、Pull Request、マージ
  7. 次回は。。。
  8. メモ書き

手順

テスト作成対象のクラスを決める

  1. Gradle projects View から printClassWhatNotMakeTest タスクを実行します。

    f:id:ksby:20160207081541p:plain

    なぜかテストクラスを作成したはずの ValuesHelper.java が出力されています?

  2. 作成したはずの ValuesHelper.java のテストクラスがなくなっている原因を調査します。Spock で作成したはずなので GitHub で src/test/groovy/ksbysample/webapp/lending の下を見てみると記録されている履歴は2つ。

    f:id:ksby:20160207084215p:plain

    履歴の中を見てみると「#53 貸出承認画面のテストを作成しました」の方でなぜか削除していました。。。 気付いていませんでした。。。

    f:id:ksby:20160207085048p:plain

    f:id:ksby:20160207085441p:plain

  3. 削除してしまったファイルを元に戻します。コマンドプロンプトを起動して git-cmd.exe を実行した後、以下のコマンドを実行します。

    > cd /c/project-springboot/ksbysample-webapp-lending
    > git checkout 744e0ae src/test/groovy/ksbysample/webapp/lending/values/ValuesHelperTest.groovy

    再度 Gradle projects View から printClassWhatNotMakeTest タスクを実行します。今度は ValuesHelper.java は出力されていません。

    f:id:ksby:20160207090845p:plain

  4. 出力されたクラスに対して以下の対応を行います。

    • 以下のクラス、インターフェースは printClassWhatNotMakeTest タスクのチェック対象外にします。
      • src/main/java/ksbysample/webapp/lending/helper/download/booklistcsv /BookListCsvData.java
      • src/main/java/ksbysample/webapp/lending/helper/download/booklistcsv /BookListCsvDataConverter.java
      • src/main/java/ksbysample/webapp/lending/helper/download/booklistcsv /BookListCsvDownloadHelper.java
      • src/main/java/ksbysample/webapp/lending/helper/download /DataDownloadHelper.java
      • src/main/java/ksbysample/webapp/lending/view/BookListCsvView.java
    • 以下のクラスは Junit でテストを作成します。
      • src/main/java/ksbysample/webapp/lending/web/confirmresult /ConfirmresultController.java

printClassWhatNotMakeTest タスクのチェック対象外のパッケージを設定する

  1. build.gradle を リンク先の内容 に変更します。

ConfirmresultController クラスのテストを Spock で書くとどうなるのか?

なんか動かない気がしていたので Controller クラスのテストは JUnit で作成していたのですが、書いて試したことはなかったので試してみたいと思います。

src/main/java/ksbysample/webapp/lending/web/confirmresult の下の ConfirmresultController.java で「Create Test」ダイアログを表示し、テストクラスを作成します。

f:id:ksby:20160207105952p:plain

src/test/groovy/ksbysample/webapp/lending/web/confirmresult の下に ConfirmresultControllerTest.groovy が作成されますので、以下の内容に変更します。

package ksbysample.webapp.lending.web.confirmresult

import ksbysample.common.test.rule.db.TestData
import ksbysample.common.test.rule.db.TestDataResource
import ksbysample.common.test.rule.mockmvc.SecurityMockMvcResource
import ksbysample.webapp.lending.Application
import org.junit.Rule
import org.junit.experimental.runners.Enclosed
import org.junit.runner.RunWith
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.SpringApplicationContextLoader
import org.springframework.test.context.ContextConfiguration
import org.springframework.test.context.web.WebAppConfiguration
import spock.lang.Specification

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*

@RunWith(Enclosed)
class ConfirmresultControllerTest {

    @ContextConfiguration(loader = SpringApplicationContextLoader.class, classes = Application.class)
    @WebAppConfiguration
    static class 貸出申請結果確認画面の初期表示のテスト_正常処理 extends Specification {

        @Autowired
        @Rule
        public TestDataResource testDataResource;

        @Autowired
        @Rule
        public SecurityMockMvcResource mvc;

        @TestData("src/test/resources/ksbysample/webapp/lending/web/confirmresult/testdata/001")
        def "貸出申請結果確認画面が表示される"() {
            expect:
            mvc.authTanakaTaro.perform(get("/confirmresult?lendingAppId=105"))
                    .andExpect(status().isOk())
                    .andExpect(content().contentType("text/html;charset=UTF-8"))
                    .andExpect(view().name("confirmresult/confirmresult"))
                    .andExpect(model().hasNoErrors())
        }

    }

}

src/test/resources/ksbysample/webapp/lending/web の下に confirmresult/testdata/001 ディレクトリを作成し、現在の DB のデータを CSV ファイルとして保存します。以下の3ファイルを作成します。

■lending_app.csv

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

■lending_book.csv

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

■table-ordering.txt

lending_app
lending_book

テストを実行します。「貸出申請結果確認画面の初期表示のテスト_正常処理」テストクラスのクラス名の左側に表示されているアイコンをクリックしてコンテキストメニューを表示後「Run 'ConfirmresultControllerTest$算出...' with Coverage」を選択します。

NullPointerException が発生してテストが失敗しました。

f:id:ksby:20160207193013p:plain

NullPointerException が発生した SecurityMockMvcResource.java:41 を見ると以下の実装でした。

f:id:ksby:20160207193535p:plain

調べてみると テストクラスで @Rule + @Autowired を組み合わせて SecurityMockMvcResource を宣言した時に @Autowired の方が効いていないようです。Debug モードでテストを実行すると @Autowired アノテーションを付けて宣言しているフィールドが全て null になっていることが確認できます ( 変数の値が簡単に見られて IntelliJ IDEA の debug モードは便利ですね )。

f:id:ksby:20160207222156p:plain

個人的には場合に応じて Spock, JUnit を使い分ければよいと考えているので、ConfirmresultController クラスのテストは JUnit で作成します。作成した src/test/groovy/ksbysample/webapp/lending/web/confirmresult/ConfirmresultControllerTest.groovy は削除します。

ConfirmresultController クラスのテストの作成

  1. src/main/java/ksbysample/webapp/lending/web/confirmresult の下の ConfirmresultController.java で「Create Test」ダイアログを表示し、テストクラスを作成します。

    f:id:ksby:20160207223835p:plain

    src/test/java/ksbysample/webapp/lending/web/confirmresult の下に ConfirmresultControllerTest.java が作成されます。

  2. 最初にテストの構成を決めます。src/test/java/ksbysample/webapp/lending/web/confirmresult の下の ConfirmresultControllerTest.javaリンク先のその1の内容 に変更します。

  3. テストを実装します。src/test/java/ksbysample/webapp/lending/web/confirmresult の下の ConfirmresultControllerTest.javaリンク先のその2の内容 に変更します。

  4. テスト作成中に気付きましたが、以前 Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る ( その51 )( 貸出申請結果確認画面の作成3 ) で DatabaseConnection クラスのインスタンスを生成する際に dbUnit の allowEmptyFields の機能を有効にしましたが、それを TestDataLoader クラスにも実装するのを忘れていましたので反映します。

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

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

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

  8. ブラウザからファイルをダウンロードした時には何もエラーは出なかったのですが、テストクラスから実行するとエラーが発生することも判明したので修正します。

  9. src/main/java/ksbysample/webapp/lending/helper/download/booklistcsv の下の BookListCsvDownloadHelper.javaリンク先の内容 に変更します。

  10. src/main/java/ksbysample/webapp/lending/view の下の BookListCsvView.javaリンク先のその1の内容 に変更します。

  11. テストを実行します。ConfirmresultControllerTest クラスのクラス名の左側に表示されているアイコンをクリックしてコンテキストメニューを表示後「Run 'ConfirmresultControllerTest' with Coverage」を選択します。

    テストが成功することが確認できます。

    f:id:ksby:20160210231158p:plain

  12. 今回のテスト結果を見て以下の点に気付きました。

    • "CSVダウンロードAbstractViewをクリックした場合()" テストメソッドは 1s 990ms かかりましたが、次の "CSVダウンロードHttpServletResponseをクリックした場合()" テストメソッドは 251ms で終了しました。サブクラスの最初のテストメソッドは実行完了まで時間がかかっていますが、ほとんど同じ処理である次のテストメソッドは時間がかかっていません。
    • 次に実行された "貸出申請結果確認画面の初期表示のテスト_正常処理" サブクラスの最初のテストメソッドは 1s 444ms と再び 1秒台に戻りました。

    テストをサブクラスに分けると分かりやすい構成にはなりますが、その分テストに時間がかかるようです。テストの実行速度を上げたい場合には、サブクラスの数は増やさないようにした方がよいみたいですね。

  13. 一旦 commit します。

全てのテストが成功するか確認する

  1. 最後に全てのテストが成功するか確認します。Project View のルートでコンテキストメニューを表示して「Run 'All Tests' with Coverage」を選択します。

    テストが実行され、全て成功することが確認できます。

    f:id:ksby:20160211000557p:plain

  2. clean タスクの実行→「Rebuild Project」メニューの実行→build タスクの実行を行います。

    "BUILD SUCCESSFUL" のメッセージは出力されましたが、無検査キャストの警告も出ました。

    f:id:ksby:20160211001323p:plain

  3. 警告の出たソースを修正します。src/main/java/ksbysample/webapp/lending/view の下の BookListCsvView.javaリンク先のその2の内容 に変更します。

  4. 再度 clean タスクの実行→「Rebuild Project」メニューの実行→build タスクの実行を行います。

    今度は警告が出ずに "BUILD SUCCESSFUL" のメッセージが出力されることが確認できます。

    f:id:ksby:20160211002629p:plain

  5. 一旦 commit します。

commit、Push、Pull Request、マージ

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

次回は。。。

  • 以下のソフトウェアがバージョアンアップしているので、バージョンアップを実施します。
  • 次に何点か改善したいと思ったことを Issue に書いたので、それらに対応します。
  • その後はやり残したことがないかこれまでの記事を見直し、ないようならば Windows で本番稼働させるためのディレクトリ作成、jar ファイル配置、bat ファイル作成、サービス登録、動作確認へと進む予定です。

メモ書き

  • ここ最近は 1.0.x ブランチにマージする前に rebase でコミットをまとめないようにしてみたのですが、blog の記事毎の変更内容を確認したい時にはまとめない方が分かりやすくてよいのですが、実際の開発ではまとめておかないと revert で取り消したい場合等にかなり不便かなと思っています。

ソースコード

build.gradle

task printClassWhatNotMakeTest << {
    def srcDir = new File("src/main/java");
    def excludePaths = [
            "src/main/java/ksbysample/webapp/lending/Application.java"
            , "src/main/java/ksbysample/webapp/lending/config"
            , "src/main/java/ksbysample/webapp/lending/cookie"
            , "src/main/java/ksbysample/webapp/lending/dao"
            , "src/main/java/ksbysample/webapp/lending/entity"
            , "src/main/java/ksbysample/webapp/lending/exception"
            , "src/main/java/ksbysample/webapp/lending/helper/download/booklistcsv"
            , "src/main/java/ksbysample/webapp/lending/helper/download/DataDownloadHelper.java"
            , "src/main/java/ksbysample/webapp/lending/helper/page/PagenationHelper.java"
            , "src/main/java/ksbysample/webapp/lending/security/LendingUser.java"
            , "src/main/java/ksbysample/webapp/lending/security/RoleAwareAuthenticationSuccessHandler.java"
            , "src/main/java/ksbysample/webapp/lending/service/calilapi/response"
            , "src/main/java/ksbysample/webapp/lending/service/file/BooklistCSVRecord.java"
            , "src/main/java/ksbysample/webapp/lending/service/openweathermapapi"
            , "src/main/java/ksbysample/webapp/lending/service/queue/InquiringStatusOfBookQueueMessage.java"
            , "src/main/java/ksbysample/webapp/lending/util/doma"
            , "src/main/java/ksbysample/webapp/lending/util/velocity/VelocityUtils.java"
            , "src/main/java/ksbysample/webapp/lending/values/validation/ValuesEnum.java"
            , "src/main/java/ksbysample/webapp/lending/view/BookListCsvView.java"
            , "src/main/java/ksbysample/webapp/lending/web/.+/.+Service.java"
            , "src/main/java/ksbysample/webapp/lending/webapi/common/CommonWebApiResponse.java"
            , "src/main/java/ksbysample/webapp/lending/webapi/weather"
    ];
    def excludeFileNamePatterns = [
            ".*EventListener.java"
            , ".*Dto.java"
            , ".*Form.java"
            , ".*Values.java"
    ];

    compareSrcAndTestDir(srcDir, excludePaths, excludeFileNamePatterns);
}
  • 以下の3行を配列 excludePaths に追加します。
    • , "src/main/java/ksbysample/webapp/lending/helper/download/booklistcsv"
    • , "src/main/java/ksbysample/webapp/lending/helper/download/DataDownloadHelper.java"
    • , "src/main/java/ksbysample/webapp/lending/view/BookListCsvView.java"

ConfirmresultControllerTest.java

■その1

package ksbysample.webapp.lending.web.confirmresult;

import ksbysample.common.test.rule.db.NoUseTestDataResource;
import ksbysample.webapp.lending.Application;
import org.junit.Test;
import org.junit.experimental.runners.Enclosed;
import org.junit.runner.RunWith;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;

@RunWith(Enclosed.class)
public class ConfirmresultControllerTest {

    @RunWith(SpringJUnit4ClassRunner.class)
    @SpringApplicationConfiguration(classes = Application.class)
    @WebAppConfiguration
    public static class 貸出申請結果確認画面の初期表示のテスト_エラー処理 {

        @Test
        @NoUseTestDataResource
        public void ログインしていなければ貸出申請結果確認画面は表示できない() throws Exception {
        }

        @Test
        @NoUseTestDataResource
        public void lendingAppIdパラメータがなければエラーになる() throws Exception {
        }

        @Test
        @NoUseTestDataResource
        public void lendingAppIdパラメータで指定された値が数値でなければエラーになる() throws Exception {
        }

        @Test
        public void lendingAppIdパラメータで指定されたデータが登録されていなければエラーになる() throws Exception {
        }

        @Test
        public void 申請者でなければ貸出申請結果確認画面を表示できない() throws Exception {
        }

    }

    @RunWith(SpringJUnit4ClassRunner.class)
    @SpringApplicationConfiguration(classes = Application.class)
    @WebAppConfiguration
    public static class 貸出申請結果確認画面の初期表示のテスト_正常処理 {

        @Test
        public void lendingAppIdパラメータで指定されたデータが登録されており申請者ならば貸出申請結果確認画面が表示される()
                throws Exception {
        }

    }

    @RunWith(SpringJUnit4ClassRunner.class)
    @SpringApplicationConfiguration(classes = Application.class)
    @WebAppConfiguration
    public static class 貸出申請結果確認画面の正常処理時のテスト {

        @Test
        public void CSVダウンロード_HttpServletResponse_をクリックした場合() throws Exception {
        }

        @Test
        public void CSVダウンロード_AbstractView_をクリックした場合() throws Exception {
        }

    }

}

■その2

package ksbysample.webapp.lending.web.confirmresult;

import com.google.common.base.Charsets;
import ksbysample.common.test.helper.TestHelper;
import ksbysample.common.test.rule.db.NoUseTestDataResource;
import ksbysample.common.test.rule.db.TestData;
import ksbysample.common.test.rule.db.TestDataResource;
import ksbysample.common.test.rule.mockmvc.SecurityMockMvcResource;
import ksbysample.webapp.lending.Application;
import ksbysample.webapp.lending.helper.message.MessagesPropertiesHelper;
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 org.springframework.test.web.servlet.MvcResult;
import org.yaml.snakeyaml.Yaml;

import java.io.File;
import java.nio.charset.Charset;

import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.containsString;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@RunWith(Enclosed.class)
public class ConfirmresultControllerTest {

    @RunWith(SpringJUnit4ClassRunner.class)
    @SpringApplicationConfiguration(classes = Application.class)
    @WebAppConfiguration
    public static class 貸出申請結果確認画面の初期表示のテスト_エラー処理 {

        @Rule
        @Autowired
        public TestDataResource testDataResource;

        @Rule
        @Autowired
        public SecurityMockMvcResource mvc;

        @Autowired
        private MessagesPropertiesHelper messagesPropertiesHelper;

        @Test
        @NoUseTestDataResource
        public void ログインしていなければ貸出申請結果確認画面は表示できない() throws Exception {
            mvc.noauth.perform(get("/confirmresult?lendingAppId=105"))
                    .andExpect(status().isFound())
                    .andExpect(redirectedUrl("http://localhost/"));
        }

        @Test
        @NoUseTestDataResource
        public void lendingAppIdパラメータがなければエラーになる() throws Exception {
            mvc.authTanakaTaro.perform(get("/confirmresult"))
                    .andExpect(status().isOk())
                    .andExpect(content().contentType("text/html;charset=UTF-8"))
                    .andExpect(view().name("error"))
                    .andExpect(content().string(
                            containsString(messagesPropertiesHelper.getMessage("ConfirmresultParamForm.lendingAppId.emptyerr", null))));
        }

        @Test
        @NoUseTestDataResource
        public void lendingAppIdパラメータで指定された値が数値でなければエラーになる() throws Exception {
            mvc.authTanakaTaro.perform(get("/confirmresult?lendingAppId=a"))
                    .andExpect(status().isOk())
                    .andExpect(content().contentType("text/html;charset=UTF-8"))
                    .andExpect(view().name("error"))
                    .andExpect(content().string(
                            containsString(messagesPropertiesHelper.getMessage("ConfirmresultParamForm.lendingAppId.emptyerr", null))));
        }

        @Test
        @TestData("src/test/resources/ksbysample/webapp/lending/web/confirmresult/testdata/001")
        public void lendingAppIdパラメータで指定されたデータが登録されていなければエラーになる() throws Exception {
            mvc.authTanakaTaro.perform(get("/confirmresult?lendingAppId=1"))
                    .andExpect(status().isOk())
                    .andExpect(content().contentType("text/html;charset=UTF-8"))
                    .andExpect(view().name("confirmresult/confirmresult"))
                    .andExpect(xpath("//*[@id=\"confirmresultForm\"]/div/p")
                            .string(messagesPropertiesHelper.getMessage("ConfirmresultForm.lendingApp.nodataerr", null)));
        }

        @Test
        @TestData("src/test/resources/ksbysample/webapp/lending/web/confirmresult/testdata/001")
        public void 申請者でなければ貸出申請結果確認画面を表示できない() throws Exception {
            mvc.authSuzukiHanako.perform(get("/confirmresult?lendingAppId=105"))
                    .andExpect(status().isForbidden())
                    .andExpect(content().contentType("text/html;charset=UTF-8"))
                    .andExpect(view().name("error"))
                    .andExpect(content().string(
                            containsString(messagesPropertiesHelper.getMessage("Confirmresult.lendingUserId.notequalerr", null))));
        }

    }

    @RunWith(SpringJUnit4ClassRunner.class)
    @SpringApplicationConfiguration(classes = Application.class)
    @WebAppConfiguration
    public static class 貸出申請結果確認画面の初期表示のテスト_正常処理 {

        @Rule
        @Autowired
        public TestDataResource testDataResource;

        @Rule
        @Autowired
        public SecurityMockMvcResource mvc;

        @Test
        @TestData("src/test/resources/ksbysample/webapp/lending/web/confirmresult/testdata/001")
        public void lendingAppIdパラメータで指定されたデータが登録されており申請者ならば貸出申請結果確認画面が表示される()
                throws Exception {
            mvc.authTanakaTaro.perform(get("/confirmresult?lendingAppId=105"))
                    .andExpect(status().isOk())
                    .andExpect(content().contentType("text/html;charset=UTF-8"))
                    .andExpect(view().name("confirmresult/confirmresult"))
                    .andExpect(xpath("//*[@id=\"confirmresultForm\"]/div/div/div[1]/table/tr[2]/td").string("承認済"))
                    .andExpect(xpath("//*[@id=\"confirmresultForm\"]/div/div/div[1]/table/tr[3]/td").string("tanaka taro"))
                    .andExpect(xpath("//*[@id=\"confirmresultForm\"]/div/div/div[1]/table/tr[4]/td").string("suzuki hanako"))
                    .andExpect(xpath("//*[@id=\"confirmresultForm\"]/div/div/table/tbody/tr").nodeCount(3))
                    .andExpect(xpath("//*[@id=\"confirmresultForm\"]/div/div/table/tbody/tr[1]/td[2]").string("978-4-7741-5377-3"))
                    .andExpect(xpath("//*[@id=\"confirmresultForm\"]/div/div/table/tbody/tr[1]/td[3]").string("JUnit実践入門"))
                    .andExpect(xpath("//*[@id=\"confirmresultForm\"]/div/div/table/tbody/tr[1]/td[4]").string("開発で使用する為"))
                    .andExpect(xpath("//*[@id=\"confirmresultForm\"]/div/div/table/tbody/tr[1]/td[5]").string("却下"))
                    .andExpect(xpath("//*[@id=\"confirmresultForm\"]/div/div/table/tbody/tr[1]/td[6]").string("購入済です"))
                    .andExpect(xpath("//*[@id=\"confirmresultForm\"]/div/div/table/tbody/tr[2]/td[5]").string("承認"));
        }

    }

    @RunWith(SpringJUnit4ClassRunner.class)
    @SpringApplicationConfiguration(classes = Application.class)
    @WebAppConfiguration
    public static class 貸出申請結果確認画面の正常処理時のテスト {

        // テストデータ
        private ConfirmresultForm confirmresultForm_001
                = (ConfirmresultForm) new Yaml().load(getClass().getResourceAsStream("ConfirmresultForm_001.yaml"));

        @Rule
        @Autowired
        public TestDataResource testDataResource;

        @Rule
        @Autowired
        public SecurityMockMvcResource mvc;

        @Test
        @TestData("src/test/resources/ksbysample/webapp/lending/web/confirmresult/testdata/001")
        public void CSVダウンロード_HttpServletResponse_をクリックした場合() throws Exception {
            MvcResult result = mvc.authTanakaTaro.perform(TestHelper.postForm("/confirmresult/filedownloadByResponse", this.confirmresultForm_001).with(csrf()))
                    .andExpect(status().isOk())
                    .andExpect(header().string("Content-Disposition", "attachment; filename=\"booklist-105.csv\""))
                    .andExpect(content().encoding("UTF-8"))
                    .andReturn();
            String content = result.getResponse().getContentAsString();
            assertThat(content)
                    .isEqualTo(com.google.common.io.Files.toString(
                            new File("src/test/resources/ksbysample/webapp/lending/web/confirmresult/assertdata/001/booklist-105.utf-8.csv")
                            , Charsets.UTF_8));
        }

        @Test
        @TestData("src/test/resources/ksbysample/webapp/lending/web/confirmresult/testdata/001")
        public void CSVダウンロード_AbstractView_をクリックした場合() throws Exception {
            MvcResult result = mvc.authTanakaTaro.perform(TestHelper.postForm("/confirmresult/filedownloadByView", this.confirmresultForm_001).with(csrf()))
                    .andExpect(status().isOk())
                    .andExpect(header().string("Content-Disposition", "attachment; filename=\"booklist-105.csv\""))
                    .andExpect(content().encoding("MS932"))
                    .andReturn();
            String content = result.getResponse().getContentAsString();
            assertThat(content)
                    .isEqualTo(com.google.common.io.Files.toString(
                            new File("src/test/resources/ksbysample/webapp/lending/web/confirmresult/assertdata/002/booklist-105.ms932.csv")
                            , Charset.forName("MS932")));
        }

    }

}

DbUnitUtils.java

package ksbysample.common.test.rule.db;

import org.dbunit.DatabaseUnitException;
import org.dbunit.database.DatabaseConfig;
import org.dbunit.database.DatabaseConnection;
import org.dbunit.database.IDatabaseConnection;

import javax.sql.DataSource;
import java.sql.SQLException;

public class DbUnitUtils {

    public static final String NULL_STRING = "[null]";

    public static IDatabaseConnection createDatabaseConnection(DataSource dataSource) throws SQLException, DatabaseUnitException {
        IDatabaseConnection conn = new DatabaseConnection(dataSource.getConnection());
        DatabaseConfig databaseConfig = conn.getConfig();
        databaseConfig.setProperty(DatabaseConfig.FEATURE_ALLOW_EMPTY_FIELDS, true);
        return conn;
    }

}

TestDataResource.java

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

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

                // TESTDATA_BASE_DIR で指定されたディレクトリ内のテストデータをロードする
                testDataLoader.load(TESTDATA_BASE_DIR);

                // テストメソッドに @TestData アノテーションが付加されている場合には、
                // アノテーションで指定されたテストデータをロードする
                loadTestData(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 void backupDb(IDatabaseConnection conn)
            throws DataSetException, IOException {
        QueryDataSet partialDataSet = new QueryDataSet(conn);

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

}
  • starting メソッド、finished メソッドの IDatabaseConnection conn の実装クラスのインスタンスを生成する処理を new DatabaseConnection(dataSource.getConnection());DbUnitUtils.createDatabaseConnection(dataSource); へ変更します。
  • ついでに NULL_STRING 定数も DbUnitUtils クラスへ移動し、クラス内の記述を NULL_STRINGDbUnitUtils.NULL_STRING へ変更します。

TestDataLoader.java

package ksbysample.common.test.rule.db;

import org.dbunit.database.IDatabaseConnection;
import org.dbunit.dataset.IDataSet;
import org.dbunit.dataset.ReplacementDataSet;
import org.dbunit.dataset.csv.CsvDataSet;
import org.dbunit.operation.DatabaseOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

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

@Component
public class TestDataLoader {

    @Autowired
    private DataSource dataSource;

    public void load(String csvDir) {
        IDatabaseConnection conn = null;
        try {
            conn = DbUnitUtils.createDatabaseConnection(dataSource);

            IDataSet dataSet = new CsvDataSet(new File(csvDir));
            ReplacementDataSet replacementDataset = new ReplacementDataSet(dataSet);
            replacementDataset.addReplacementObject(DbUnitUtils.NULL_STRING, null);
            DatabaseOperation.CLEAN_INSERT.execute(conn, replacementDataset);
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            try {
                if (conn != null) conn.close();
            } catch (Exception ignored) {}
        }
    }

}
  • load メソッドの IDatabaseConnection conn の実装クラスのインスタンスを生成する処理を new DatabaseConnection(dataSource.getConnection());DbUnitUtils.createDatabaseConnection(dataSource); へ変更します。
  • NULL_STRING 定数を削除し、クラス内の記述を NULL_STRINGDbUnitUtils.NULL_STRING へ変更します。

BookListCsvDownloadHelper.java

    @Override
    public void writeDataToResponse(HttpServletResponse response) throws IOException {
        CsvWriterSettings settings = new CsvWriterSettings();
        settings.setHeaders(CSV_HEADER);
        BeanWriterProcessor<BookListCsvData> writerProcessor = new BeanWriterProcessor<>(BookListCsvData.class);
        settings.setRowWriterProcessor(writerProcessor);

        response.setCharacterEncoding("UTF-8");
        CsvWriter writer = new CsvWriter(response.getWriter(), settings);
        writer.writeHeaders();
        writer.processRecordsAndClose(bookListCsvDataList);
    }
  • writeDataToResponse メソッド内に response.setCharacterEncoding("UTF-8"); を追加し、response に出力するデータの文字コードを設定します。

BookListCsvView.java

■その1

package ksbysample.webapp.lending.view;

import com.univocity.parsers.common.processor.BeanWriterProcessor;
import com.univocity.parsers.csv.CsvWriter;
import com.univocity.parsers.csv.CsvWriterSettings;
import ksbysample.webapp.lending.helper.download.booklistcsv.BookListCsvData;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.view.AbstractView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
import java.util.Map;

@Component(value = "BookListCsvView")
public class BookListCsvView extends AbstractView {

    private static final String[] CSV_HEADER = new String[]{"ISBN", "書名", "申請理由", "承認/却下", "却下理由"};
    private static final String CSV_FILE_NAME_FORMAT = "booklist-%s.csv";

    @Override
    protected void renderMergedOutputModel(Map<String, Object> model
            , HttpServletRequest request, HttpServletResponse response) throws Exception {
        Long lendingAppId = (Long) model.get("lendingAppId");
        List<BookListCsvData> bookListCsvDataList = (List<BookListCsvData>) model.get("bookListCsvDataList");

        response.setContentType("application/octet-stream; charset=Windows-31J;");
        response.setHeader("Content-Disposition"
                , String.format("attachment; filename=\"%s\"", String.format(CSV_FILE_NAME_FORMAT, lendingAppId)));

        CsvWriterSettings settings = new CsvWriterSettings();
        settings.setHeaders(CSV_HEADER);
        BeanWriterProcessor<BookListCsvData> writerProcessor = new BeanWriterProcessor<>(BookListCsvData.class);
        settings.setRowWriterProcessor(writerProcessor);

        response.setCharacterEncoding("MS932");
        CsvWriter writer = new CsvWriter(response.getWriter(), settings);
        writer.writeHeaders();
        writer.processRecordsAndClose(bookListCsvDataList);
    }

}
  • response.setCharacterEncoding("MS932"); を追加します。

■その2

    @SuppressWarnings("unchecked")
    @Override
    protected void renderMergedOutputModel(Map<String, Object> model

履歴

2016/02/11
初版発行。