かんがるーさんの日記

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

Spring Boot + npm + Geb で入力フォームを作ってテストする ( その68 )( Spring Boot を 1.5.10 → 1.5.14 へバージョンアップする )

概要

記事一覧はこちらです。

Spring Boot + npm + Geb で入力フォームを作ってテストする ( その67 )( Prettier のインストール+Jest Each を試してみる ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • Java 側のライブラリ/モジュールをバージョンアップします。

参照したサイト・書籍

目次

  1. Spring Boot を 1.5.10 → 1.5.14 へバージョンアップする(他のライブラリもバージョンアップする)

手順

Spring Boot を 1.5.10 → 1.5.14 へバージョンアップする(他のライブラリもバージョンアップする)

build.gradle の以下の点を変更します。

buildscript {
    ext {
        group "ksbysample"
        version "1.0.1-RELEASE"
        springBootVersion = "1.5.14.RELEASE"
    }
}

plugins {
    id "java"
    id "eclipse"
    id "idea"
    // plugins {} block 内では ${springBootVersion} が使用できないので、バージョンを直接記述している
    id "org.springframework.boot" version "1.5.14.RELEASE"
    id "groovy"
    id "net.ltgt.errorprone" version "0.0.14"
    id "checkstyle"
    id "findbugs"
    id "pmd"
    id "com.moowork.node" version "1.2.0"
}

..........

dependencyManagement {
    imports {
        // mavenBom は以下の URL のものを使用する
        // https://repo.spring.io/release/org/springframework/boot/spring-boot-starter-parent/1.5.10.RELEASE/
        // bomProperty に指定可能な property は以下の URL の BOM に記述がある
        // https://repo.spring.io/release/org/springframework/boot/spring-boot-dependencies/1.5.10.RELEASE/spring-boot-dependencies-1.5.10.RELEASE.pom
        mavenBom("org.springframework.boot:spring-boot-starter-parent:${springBootVersion}") {
            bomProperty 'thymeleaf.version', '3.0.9.RELEASE'
            bomProperty 'thymeleaf-extras-springsecurity4.version', '3.0.2.RELEASE'
            bomProperty 'thymeleaf-layout-dialect.version', '2.2.2'
            bomProperty 'thymeleaf-extras-data-attribute.version', '2.0.1'
            bomProperty 'thymeleaf-extras-java8time.version', '3.0.1.RELEASE'
        }
    }
}

dependencies {
    def spockVersion = "1.1-groovy-2.4"
    def domaVersion = "2.19.2"
    def lombokVersion = "1.18.0"
    def errorproneVersion = "2.3.1"
    def powermockVersion = "1.7.4"
    def seleniumVersion = "3.13.0"

    // dependency-management-plugin によりバージョン番号が自動で設定されるもの
    // Appendix F. Dependency versions ( https://docs.spring.io/spring-boot/docs/1.5.10.RELEASE/reference/html/appendix-dependency-versions.html ) 参照
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("org.springframework.boot:spring-boot-starter-thymeleaf") {
        exclude group: "org.codehaus.groovy", module: "groovy"
    }
    implementation("org.springframework.boot:spring-boot-starter-data-jpa")
    implementation("org.springframework.boot:spring-boot-starter-freemarker")
    implementation("org.springframework.boot:spring-boot-starter-mail")
    implementation("org.springframework.boot:spring-boot-starter-security")
    implementation("org.springframework.boot:spring-boot-devtools")
    implementation("org.springframework.session:spring-session")
    implementation("org.codehaus.janino:janino")
    testImplementation("org.springframework.boot:spring-boot-starter-test")
    testImplementation("org.springframework.security:spring-security-test")
    testImplementation("org.yaml:snakeyaml")
    testImplementation("org.mockito:mockito-core")

    // dependency-management-plugin によりバージョン番号が自動で設定されないもの、あるいは最新バージョンを指定したいもの
    implementation("com.integralblue:log4jdbc-spring-boot-starter:1.0.2")
    implementation("org.flywaydb:flyway-core:5.1.4")
    implementation("com.h2database:h2:1.4.192")
    implementation("com.github.rozidan:modelmapper-spring-boot-starter:1.0.0")
    implementation("com.google.guava:guava:25.1-jre")
    implementation("org.apache.commons:commons-lang3:3.7")
    testImplementation("org.dbunit:dbunit:2.5.4")
    testImplementation("org.assertj:assertj-core:3.10.0")
    testImplementation("org.spockframework:spock-core:${spockVersion}")
    testImplementation("org.spockframework:spock-spring:${spockVersion}")
    testImplementation("com.google.code.findbugs:jsr305:3.0.2")
    testImplementation("org.jsoup:jsoup:1.11.3")
    testImplementation("com.icegreen:greenmail:1.5.7")

    // for lombok
    annotationProcessor("org.projectlombok:lombok:${lombokVersion}")
    compileOnly("org.projectlombok:lombok:${lombokVersion}")

    // for Doma
    annotationProcessor("org.seasar.doma:doma:${domaVersion}")
    implementation("org.seasar.doma:doma:${domaVersion}")
    domaGenRuntime("org.seasar.doma:doma-gen:${domaVersion}")
    domaGenRuntime("com.h2database:h2:1.4.192")

    // for Error Prone ( http://errorprone.info/ )
    errorprone("com.google.errorprone:error_prone_core:${errorproneVersion}")
    compileOnly("com.google.errorprone:error_prone_annotations:${errorproneVersion}")

    // PowerMock
    testImplementation("org.powermock:powermock-module-junit4:${powermockVersion}")
    testImplementation("org.powermock:powermock-api-mockito:${powermockVersion}")

    // for Geb + Spock
    testImplementation("org.gebish:geb-spock:2.1")
    testImplementation("org.seleniumhq.selenium:selenium-chrome-driver:${seleniumVersion}")
    testImplementation("org.seleniumhq.selenium:selenium-firefox-driver:${seleniumVersion}")
    testImplementation("org.seleniumhq.selenium:selenium-support:${seleniumVersion}")
    testImplementation("org.seleniumhq.selenium:selenium-api:${seleniumVersion}")
    testImplementation("org.seleniumhq.selenium:selenium-remote-driver:${seleniumVersion}")
}
  • buildscript の以下の点を変更します。
    • springBootVersion = "1.5.10.RELEASE"springBootVersion = "1.5.14.RELEASE" に変更します。
  • plugins の以下の点を変更します。
    • id "org.springframework.boot" version "1.5.10.RELEASE"id "org.springframework.boot" version "1.5.14.RELEASE" に変更します。
  • dependencies の以下の点を変更します。
    • def domaVersion = "2.19.1"def domaVersion = "2.19.2" に変更します。
    • def lombokVersion = "1.16.20"def lombokVersion = "1.18.0" に変更します。
    • def powermockVersion = "1.7.3"def powermockVersion = "1.7.4" に変更します。
    • def seleniumVersion = "3.9.1"def seleniumVersion = "3.13.0" に変更します。
    • implementation("org.flywaydb:flyway-core:5.0.7")implementation("org.flywaydb:flyway-core:5.1.4") に変更します。
    • implementation("com.google.guava:guava:22.0")implementation("com.google.guava:guava:25.1-jre") に変更します。
    • testImplementation("org.assertj:assertj-core:3.9.1")testImplementation("org.assertj:assertj-core:3.10.0") に変更します。
    • testImplementation("org.jsoup:jsoup:1.11.2")testImplementation("org.jsoup:jsoup:1.11.3") に変更します。
    • testImplementation("com.icegreen:greenmail:1.5.7") を2回記述していたので削除します。

変更後、Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。

clean タスク実行 → Rebuild Project 実行 → build タスクを実行すると "BUILD SUCCESSFUL" のメッセージが出力されました。

f:id:ksby:20180715172056p:plain

IntelliJ IDEA から Tomcat を起動して npm run springboot で browser-sync を起動して動作確認しても問題ありませんでした(画面キャプチャなし)。

Spring Boot を含む Java 側のライブラリ/モジュールのバージョンアップは何かあっさり終わりました。。。

履歴

2018/07/15
初版発行。

Spring Boot + npm + Geb で入力フォームを作ってテストする ( その67 )( Prettier のインストール+Jest Each を試してみる )

概要

記事一覧はこちらです。

Spring Boot + npm + Geb で入力フォームを作ってテストする ( その66 )( Node.js を 8.9.4 → 8.11.3 へ、npm を 5.6.0 → 6.2.0 へ+ Javascript のライブラリをバージョンアップする ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • Prettier をインストールします。Jest Each のデータテーブル定義を綺麗にフォーマットしたいのがインストールの動機でしたが、最近の Javascript は Prettier でフォーマットするのが当たり前のようなのと、IntelliJ IDEA でも Prettier によるフォーマットがサポートされたので、IDE 上でのフォーマットも Prettier で行えるようにします。
    • Jest 23 からの新機能 Jest Each (Spock みたいなデータテーブルを定義して Parameterized test ができる機能)を試してみます。

参照したサイト・書籍

目次

  1. Prettier をインストールする
    1. 方針
    2. prettier、eslint-plugin-prettier、eslint-config-prettier をインストールする
    3. .eslintrc.js を変更する
    4. eslint-config-airbnb-base をアンインストールする
    5. IntelliJ IDEA に Prettier プラグインをインストールする
    6. package.json を変更する
    7. 動作確認
  2. Jest Each を試してみる
  3. 最後に

手順

Prettier をインストールする

方針

  • コードフォーマットは Prettier に完全に任せる。
  • ESLint は airbnb-base/legacy のルールや独自に定義したルールは全て破棄し、prettier のルールに従うようにする。airbnb-base/legacy のルールを使用しなくなるので、eslint-config-airbnb-base はアンインストールする。
  • IntelliJ IDEA に Prettier プラグインをインストールして、IDE 上でフォーマットできるようにする。
  • npm test コマンド実行時に全ての js ファイルが自動でフォーマットされるようにする。
  • npm run build コマンドの前には npm test コマンドを実行して必ずテストする想定なので、npm run build コマンド実行時は何もしない。

prettier、eslint-plugin-prettier、eslint-config-prettier をインストールする

Install のページの記述に従い、npm install --save-dev --save-exact prettier を実行して prettier をインストールします。

f:id:ksby:20180715101417p:plain

Integrating with ESLint のページの記述に従い、npm install --save-dev eslint-plugin-prettier eslint-config-prettier を実行します。

f:id:ksby:20180715101638p:plain

.eslintrc.js を変更する

.eslintrc.js の記述を以下の内容に入れ替えます。

module.exports = {
    "extends": "plugin:prettier/recommended"
};

eslint-config-airbnb-base をアンインストールする

npm uninstall --save-dev eslint-config-airbnb-base を実行します。

f:id:ksby:20180715114222p:plain

IntelliJ IDEA に Prettier プラグインをインストールする

IntelliJ IDEA のメインメニューから「File」-「Settings...」メニューを選択し、「Settings」ダイアログを表示します。

ダイアログの左側のリストから「Plugins」を選択した後、画面右側の「Browse repositories...」ボタンをクリックして「Browse Repositories」ダイアログを表示します。

f:id:ksby:20180715102522p:plain

「Browse Repositories」ダイアログが表示されたら検索フィールドに "Prettier" と入力します。リストに "Prettier" プラグインが表示されますので、選択して画面右側の「Install」ボタンをクリックします。

f:id:ksby:20180715102836p:plain

プラグインがダウロードされた後「Install」ボタンが「Restart IntelliJ IDEA」に変わるのでクリックします。

「Browse Repositories」ダイアログに戻るので「OK」ボタンをクリックします。ダイアログが閉じた後「IDE and Plugin Updates」ダイアログが表示されるので「Restart」ボタンをクリックして IntelliJ IDEA を再起動します。

これで IDE 上で Prettier でフォーマットできるようになりました。試してみます。

src/main/assets/js/lib/util/delayExecutor.js は現在以下のフォーマットですが、

f:id:ksby:20180715103543p:plain

Reformat with Prettier (Ctrl+Alt+Shift+P) を押すと以下のようにフォーマットされます。

f:id:ksby:20180715103749p:plain

package.json を変更する

package.json の以下の点を変更します。

  "scripts": {
    "test": "run-s prettier:format prettier:format-test jest",
    "jest": "jest --config=jest.config.json --coverage",
    "postinstall": "run-s clean:static-dir copy:all",
    ..........
    "postcss:watch": "postcss src/main/assets/css/**/* -d src/main/resources/static/css -x .min.css -w --poll",
    "prettier:format": "prettier --write src/main/assets/js/**/*.js",
    "prettier:format-test": "prettier --write src/test/assets/__tests__/**/*.js",
    "webpack:build": "webpack --mode production",
    ..........
  },
  • 以下の3行を追加します。
    • "jest": "jest --config=jest.config.json --coverage",
    • "prettier:format": "prettier --write src/main/assets/js/**/*.js",
    • "prettier:format-test": "prettier --write src/test/assets/__tests__/**/*.js",
  • testjest --config=jest.config.json --coveragerun-s prettier:format prettier:format-test jest に変更します。

動作確認

確認前に先程 IDE 上でフォーマットした src/main/assets/js/lib/util/delayExecutor.js を元に戻します。

npm test コマンドを実行してみます。テスト実行前に js ファイルをフォーマットしていることが確認でき、テストも全て成功しました。

f:id:ksby:20180715113350p:plain f:id:ksby:20180715113447p:plain

src/main/assets/js/lib/util/delayExecutor.js を開いてみると、IDE 上でフォーマットした時と同じようにフォーマットされていました。

f:id:ksby:20180715113732p:plain

Jest Each を試してみる

Jest Each のデータテーブル定義が Spock みたいだな、と思っていたら、Blog の文章に Spock Data Tables とはっきり書かれていました。Spock を使っているのもデータテーブルによる Parameterized test が楽だからなので、やっぱりこの機能追加は嬉しいですね。

f:id:ksby:20180715123854p:plain

では試してみましょう。src/test/assets/tests/lib/util/converter.test.js で convertHiragana のテストを以下のように記述していますが、

  describe("convertHiragana のテスト", () => {
    beforeEach(() => {
      document.body.innerHTML = `
              <input type="text" name="sample" id="sample" value=""/>
            `;
    });

    test("全角カタカナはひらがなに変更される", () => {
      $("#sample").val("アイウエオ");
      converter.convertHiragana(["#sample"]);
      expect($("#sample").val()).toBe("あいうえお");
    });

    test("半角カタカナはひらがなに変更される", () => {
      $("#sample").val("アイウエオ");
      converter.convertHiragana(["#sample"]);
      expect($("#sample").val()).toBe("あいうえお");
    });
  });

これを Jest Each の機能で書き換えてみます。

  describe("convertHiragana のテスト(Jest Each版)", () => {
    beforeEach(() => {
      document.body.innerHTML = `
              <input type="text" name="sample" id="sample" value=""/>
            `;
    });

    test.each`
      str             | expected
      ${"アイウエオ"} | ${"あいうえお"}
      ${"アイウエオ"}      | ${"あいうえお"}
    `("$str --> $expected", ({ str, expected }) => {
      $("#sample").val(str);
      converter.convertHiragana(["#sample"]);
      expect($("#sample").val()).toBe(expected);
    });
  });

IDE からテストを実行すると無事成功しました。

f:id:ksby:20180715121710p:plain

わざと1件だけ失敗するようにデータテーブルを変更してみます。

  describe("convertHiragana のテスト(Jest Each版)", () => {
    beforeEach(() => {
      document.body.innerHTML = `
              <input type="text" name="sample" id="sample" value=""/>
            `;
    });

    test.each`
      str             | expected
      ${"アイウエオ"} | ${"あいうえお"}
      ${"アイウエオ"}      | ${"あいうえおx"}
    `("$str --> $expected", ({ str, expected }) => {
      $("#sample").val(str);
      converter.convertHiragana(["#sample"]);
      expect($("#sample").val()).toBe(expected);
    });
  });

IDE からテストを実行すると1件失敗しました。右側の青色でリンク表示されているパスをクリックすると、ソースでエラーになったところへジャンプします。

f:id:ksby:20180715121921p:plain

またコマンドラインnpm test コマンドで実行した場合には、失敗したテストは以下のように表示されます。以前より何かとても分かりやすくなっているような。。。 調べてみたら Jest 22 の Codeframe in test failures のようですね。分かりやすい表示になるよう変更されていました。

f:id:ksby:20180715122219p:plain

最後に

Javascript のフォーマットは Prettier に任せて独自定義はなしにする方が全然楽ですね。What is Prettier? を見ると json もフォーマットできるので、package.json や webpack.config.js 等の設定ファイルも Prettier でフォーマットし直しました。

Jest は Jest 22、Jest 23 の変更内容を見ましたが、Jest Each 以外にも便利そうな機能が追加されているようです。自分としては Javascript のテストは Jest 一択ですね。

履歴

2018/07/15
初版発行。

Spring Boot + npm + Geb で入力フォームを作ってテストする ( その66 )( Node.js を 8.9.4 → 8.11.3 へ、npm を 5.6.0 → 6.2.0 へ+ Javascript のライブラリをバージョンアップする )

概要

記事一覧はこちらです。

Spring Boot + npm + Geb で入力フォームを作ってテストする ( その65 )( Gradle を 4.6 → 4.8.1 へ、Checkstyle を 8.8 → 8.11 へ、PMD を 6.4.0 → 6.5.0 へ、error-prone を 2.2.0 → 2.3.1 へバージョンアップする ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • Javascript 側のライブラリ/モジュールをバージョンアップします。Node.js や npm も最新版にバージョンアップします。

参照したサイト・書籍

目次

  1. Node.js を 8.9.4 → 8.11.3 へ、npm を 5.5.1 → 6.2.0 へバージョンアップする
  2. モジュールを最新版にバージョンアップする
  3. eslint-config-airbnb-base を 12.1.0 → 13.0.0 へ、eslint を 4.18.1 → 4.19.1 へバージョンアップする
  4. windows-build-tools を 2.2.1 → 3.0.1 へ、jest を 22.4.2 → 23.4.1 へバージョンアップする

手順

Node.js を 8.9.4 → 8.11.3 へ、npm を 5.5.1 → 6.2.0 へバージョンアップする

https://nodejs.org/ja/ を見ると推奨版は 8.11.3 LTS でした。

f:id:ksby:20180714144844p:plain

nodist dist コマンドを実行して 8.11.3 がインストール可能か確認します。

f:id:ksby:20180714145207p:plain f:id:ksby:20180714145319p:plain

8.11.3 が表示されていますので、8.11.3 へバージョンアップします。

f:id:ksby:20180714145535p:plain

npm も最新版にバージョンアップします。npm の最新バージョンは https://docs.npmjs.com/ の一番下を見ると 6.2.0 とありましたので、

f:id:ksby:20180714145739p:plain

6.2.0 にバージョンアップします。。。と思いましたが、Cannot read property 'trim' of null. というメッセージが出てバージョンアップできませんでした。

f:id:ksby:20180714150155p:plain

調べると nodist では、まだ npm@6.0.0 がインストールできない(2018年5月4日からインストールできます) という記事を見つけました。なるほど、https://github.com/npm/npm/releases のページの文字列を指定しないといけないのか。。。 最新版だと 6.2.0-next.1 でした。

バージョン番号を 6.2.0-next.1 にすると、今度は無事バージョンアップできました。

f:id:ksby:20180714150806p:plain

モジュールを最新版にバージョンアップする

バージョンアップ可能なモジュールを確認します。IntelliJ IDEA のメインメニューから「File」-「Settings...」を選択して「Settings」ダイアログを開き、画面左側で「Language & Frameworks」-「Node.js and NPM」を選択します。

画面右側にモジュールの一覧と現行バージョン、最新バージョン一覧が表示されます。

f:id:ksby:20180714152111p:plain

今回モジュール一覧には以下のように表示されました。

f:id:ksby:20180714152221p:plain f:id:ksby:20180714152335p:plain

今回は以下の方針でバージョンアップします。

  • admin-lte、bootstrap、ionicons は現行のまま。
  • postcss-cli、xhr-mock は 以前 正常にバージョンアップできなかったので今回もバージョンしない。
  • mobx、mobx-utils は少し触ってみただけなのでバージョンアップしない。
  • バージョンアップは npm update ではなく npm install --save-dev autoprefixer@8.0.0(package.json の dependencies に記載されているものは --save、devDependencies に記載されているものは --save-dev にする) のようにバージョンを指定しながらバージョンアップする。
  • 1つずつ上からバージョンアップする。関連しそうなところを動作確認しながら進める。
  • eslint、jest + widows-build-tools は別に章を分けてバージョンアップする。

特に問題がでなければ画面キャプチャは撮りません。

  • npm install --save-dev autoprefixer@8.6.5
  • npm install --save-dev browser-sync@2.24.5
  • npm install --save computed-async-mobx@4.1.0
  • npm install --save-dev cssnano@4.0.2
  • npm install --save-dev http-proxy-middleware@0.18.0
  • npm install --save-dev jquery-mockjax@2.4.0
  • npm install --save-dev nock@9.4.2
  • npm install --save-dev npm-run-all@4.1.3
  • npm install --save-dev stylelint@9.3.0
  • npm install --save-dev stylelint-config-standard@18.2.0
  • npm install --save-dev uglifyjs-webpack-plugin@1.2.7
  • npm install --save-dev webpack@4.16.0
  • npm install --save-dev webpack-cli@3.0.8
  • npm install --save-dev xhr-mock@2.4.1

コマンドラインから npm test コマンドを実行すると、テストは全て成功しました。

f:id:ksby:20180714205249p:plain f:id:ksby:20180714205401p:plain

npm run build コマンドも特にエラーは出ずに終了しました。

f:id:ksby:20180714205841p:plain

IntelliJ IDEA から Tomcat を起動して npm run springboot で browser-sync を起動して動作確認しても問題ありませんでした(画面キャプチャなし)。

eslint-config-airbnb-base を 12.1.0 → 13.0.0 へ、eslint を 4.18.1 → 4.19.1 へバージョンアップする

以下のコマンドを実行します。

  • npm install --save-dev eslint-config-airbnb-base@13.0.0

f:id:ksby:20180714212045p:plain

eslint-config-airbnb-base@13.0.0eslint@^4.19.1eslint-plugin-import@^2.12.0 が必要のようです。

npm show eslint@* version コマンドでインストール可能なバージョン一覧を確認します。メジャーバージョン番号が 4 だと 4.19.1 が最新でした。

f:id:ksby:20180714221814p:plain f:id:ksby:20180714221916p:plain

npm show eslint-plugin-import@* version コマンドでインストール可能なバージョン一覧を確認します。メジャーバージョン番号が 2 だと 2.13.0 が最新でした。

f:id:ksby:20180714222345p:plain f:id:ksby:20180714222442p:plain

以下のコマンドを実行して eslint-config-airbnb-base に対応する eslint、eslint-plugin-import の最新バージョンにバージョンアップします。

  • npm install --save-dev eslint@4.19.1
  • npm install --save-dev eslint-plugin-import@2.13.0

npm run build コマンドを実行してみると、特にエラーは出ずに終了しました。

f:id:ksby:20180714222717p:plain

windows-build-tools を 2.2.1 → 3.0.1 へ、jest を 22.4.2 → 23.4.1 へバージョンアップする

コマンドプロンプトを「管理者として実行...」で起動した後、npm install --global --production windows-build-tools@3.0.1 を実行します。

f:id:ksby:20180715005425p:plain

All done! のメッセージが出た後、いつまで経っても終わらないので Ctrl+C を押して強制終了した後、タスクマネージャから BuildTools_Full のプロセスを落としました。windows-build-tools はインストールの動作がおかしかったのでいろいろ試したのですが、何をやってもコマンドプロンプトに戻ってこなくて、今回きちんとインストールできているのか良く分からないんですよね。。。 どうしようもなかったので先に進めることにします。

次に普通に起動したコマンドプロンプトnpm install --save-dev jest@23.4.1 を実行します。

f:id:ksby:20180715012606p:plain

npm install --save-dev jest-html-reporter@2.4.1 も実行します。

f:id:ksby:20180715013303p:plain

npm WARN jest-html-reporter@2.0.0 requires a peer of jest@19.x - 22.x but none is installed. のメッセージが出ていますが、実際には Jest 23 でも動作するので無視します。

npm test コマンドを実行するとテストは全て成功し、

f:id:ksby:20180715013601p:plain f:id:ksby:20180715013705p:plain

build/reports/jest/jest-html-reporter.html も問題なく生成されていました。

f:id:ksby:20180715013906p:plain

Jest 23: 🔥 Blazing Fast Delightful Testing を見ましたが、Spock みたいな Parameterized Test が書ける Jest Each という機能が追加されていました。データを記述するテーブルレイアウトが少し独特ですが、Prettier を入れれば綺麗にフォーマットしてくれるらしいです。

次は Java のモジュールのバージョンアップの予定でしたが、先に Jest Each を試してみます。欲しかった機能なんですよね。期待大です。

履歴

2018/07/15
初版発行。

Spring Boot + npm + Geb で入力フォームを作ってテストする ( その65 )( Gradle を 4.6 → 4.8.1 へ、Checkstyle を 8.8 → 8.11 へ、PMD を 6.4.0 → 6.5.0 へ、error-prone を 2.2.0 → 2.3.1 へバージョンアップする )

概要

記事一覧はこちらです。

Spring Boot + npm + Geb で入力フォームを作ってテストする ( その64 )( 入力画面3を作成する6、@SpringBootTest のテストは Spock+Groovy より JUnit4+Groovy の方が速い? ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • Gradle を 4.6 → 4.8.1 へバージョンアップします。また compile 等が非推奨の書き方になっていたので、build.gradle を変更します。
    • Checkstyle を 8.8 → 8.11、PMD を 6.4.0 → 6.5.0、error-prone を 2.2.0 → 2.3.1 へバージョンアップします。

参照したサイト・書籍

目次

  1. Gradle を 4.6 → 4.8.1 へバージョンアップする
  2. build.gradle で compileimplementation に変更する
  3. Checkstyle を 8.8 → 8.11、PMD を 6.4.0 → 6.5.0 へバージョンアップする
  4. error-prone を 2.2.0 → 2.3.1 へバージョンアップする

手順

Gradle を 4.6 → 4.8.1 へバージョンアップする

build.gradle の以下の点を変更します。

task wrapper(type: Wrapper) {
    gradleVersion = '4.8.1'
}
  • gradleVersion = '4.6'gradleVersion = '4.8.1' に変更します。

コマンドプロンプトを起動し、gradlew wrapper コマンドを実行します。

f:id:ksby:20180711125042p:plain

gradle/wrapper/gradle-wrapper.properties を開くと gradle-4.8.1-bin.zip に変更されています。

f:id:ksby:20180711125255p:plain

Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。

f:id:ksby:20180711130045p:plain

Deprecated Gradle features were used in this build, making it incompatible with Gradle 5.0. というメッセージが出たので、https://docs.gradle.org/4.8.1/userguide/command_line_interface.html#sec:command_line_warnings の記述を参考に gradlew --warning-mode=all コマンドを実行してみます。

f:id:ksby:20180711130455p:plain

1件メッセージが出ていました。wrapper という task はもうNGなので wrapper { ... } syntax を使え、ということらしいです。

Customizing the Gradle Wrapper を参考に build.gradle の以下の点を変更して、Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。

wrapper {
    gradleVersion = '4.8.1'
    distributionType = Wrapper.DistributionType.ALL
}
  • task wrapper(type: Wrapper)wrapper に変更します。
  • distributionType = Wrapper.DistributionType.ALL を追加します。

再度 gradlew --warning-mode=all コマンドを実行すると、今度は何も出ませんでした。

f:id:ksby:20180711132753p:plain

clean タスク実行 → Rebuild Project → build タスクを実行すると BUILD SUCCESSFUL のメッセージが出ましたが、Deprecated Gradle features were used in this build, making it incompatible with Gradle 5.0. のメッセージがまた出ました。。。

f:id:ksby:20180711234228p:plain

コマンドラインから gradlew -Dfile.encoding=UTF-8 --warning-mode=all build コマンドを実行してみます。

f:id:ksby:20180711235526p:plain

Doma 2 と Lombok の annotaion processor が compile classpath にあるのは Gradle 5.0 から非推奨になるので、Please add them to the annotation processor path instead. ということらしいです。何のことか分からなかったので調べてみたところ、以下のページを見つけました。annotaion processorannotationProcessor で指定することになったようです。あと compile(Deprecated) とかありますね。。。

compile の方は一旦無視して annotationProcessor の方から反映してみます。build.gradle の以下の点を変更します。

dependencies {
    ..........

    // for lombok
    annotationProcessor("org.projectlombok:lombok:${lombokVersion}")
    compileOnly("org.projectlombok:lombok:${lombokVersion}")

    // for Doma
    annotationProcessor("org.seasar.doma:doma:${domaVersion}")
    compile("org.seasar.doma:doma:${domaVersion}")
    domaGenRuntime("org.seasar.doma:doma-gen:${domaVersion}")
    domaGenRuntime("com.h2database:h2:1.4.192")
  • annotationProcessor("org.projectlombok:lombok:${lombokVersion}") を追加します。
  • testCompileOnly("org.projectlombok:lombok:${lombokVersion}") を削除します。
  • annotationProcessor("org.seasar.doma:doma:${domaVersion}") を追加します。

gradlew -Dfile.encoding=UTF-8 --warning-mode=all build コマンドを実行しますが、まだ警告が出たままでした。Lombok はメッセージに出なくなりましたが、Doma 2 の方が出たままですね。Doma 2 はさすがに compilecompileOnly に変更する訳にもいかないので、ここまでにします。

f:id:ksby:20180713003051p:plain

build.gradle で compileimplementation に変更する

Gradle の compile, api, implementation とかについて という記事を見つけました。やっぱり非推奨になったようです。

ライブラリではないので api を使用する必要はなさそうなので、build.gradle で以下のように変更します。

  • compileimplementation
  • runtimeruntimeOnly
  • testCompiletestImplementation
dependencies {
    def spockVersion = "1.1-groovy-2.4"
    def domaVersion = "2.19.1"
    def lombokVersion = "1.16.20"
    def errorproneVersion = "2.2.0"
    def powermockVersion = "1.7.3"
    def seleniumVersion = "3.9.1"

    // dependency-management-plugin によりバージョン番号が自動で設定されるもの
    // Appendix F. Dependency versions ( https://docs.spring.io/spring-boot/docs/1.5.10.RELEASE/reference/html/appendix-dependency-versions.html ) 参照
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("org.springframework.boot:spring-boot-starter-thymeleaf") {
        exclude group: "org.codehaus.groovy", module: "groovy"
    }
    implementation("org.springframework.boot:spring-boot-starter-data-jpa")
    implementation("org.springframework.boot:spring-boot-starter-freemarker")
    implementation("org.springframework.boot:spring-boot-starter-mail")
    implementation("org.springframework.boot:spring-boot-starter-security")
    implementation("org.springframework.boot:spring-boot-devtools")
    implementation("org.springframework.session:spring-session")
    implementation("org.codehaus.janino:janino")
    testImplementation("org.springframework.boot:spring-boot-starter-test")
    testImplementation("org.springframework.security:spring-security-test")
    testImplementation("org.yaml:snakeyaml")
    testImplementation("org.mockito:mockito-core")

    // dependency-management-plugin によりバージョン番号が自動で設定されないもの、あるいは最新バージョンを指定したいもの
    implementation("com.integralblue:log4jdbc-spring-boot-starter:1.0.2")
    implementation("org.flywaydb:flyway-core:5.0.7")
    implementation("com.h2database:h2:1.4.192")
    implementation("com.github.rozidan:modelmapper-spring-boot-starter:1.0.0")
    implementation("com.google.guava:guava:22.0")
    implementation("org.apache.commons:commons-lang3:3.7")
    testImplementation("org.dbunit:dbunit:2.5.4")
    testImplementation("com.icegreen:greenmail:1.5.7")
    testImplementation("org.assertj:assertj-core:3.9.1")
    testImplementation("org.spockframework:spock-core:${spockVersion}")
    testImplementation("org.spockframework:spock-spring:${spockVersion}")
    testImplementation("com.google.code.findbugs:jsr305:3.0.2")
    testImplementation("org.jsoup:jsoup:1.11.2")
    testImplementation("com.icegreen:greenmail:1.5.7")

    // for lombok
    annotationProcessor("org.projectlombok:lombok:${lombokVersion}")
    compileOnly("org.projectlombok:lombok:${lombokVersion}")

    // for Doma
    annotationProcessor("org.seasar.doma:doma:${domaVersion}")
    implementation("org.seasar.doma:doma:${domaVersion}")
    domaGenRuntime("org.seasar.doma:doma-gen:${domaVersion}")
    domaGenRuntime("com.h2database:h2:1.4.192")

    // for Error Prone ( http://errorprone.info/ )
    errorprone("com.google.errorprone:error_prone_core:${errorproneVersion}")
    compileOnly("com.google.errorprone:error_prone_annotations:${errorproneVersion}")

    // PowerMock
    testImplementation("org.powermock:powermock-module-junit4:${powermockVersion}")
    testImplementation("org.powermock:powermock-api-mockito:${powermockVersion}")

    // for Geb + Spock
    testImplementation("org.gebish:geb-spock:2.1")
    testImplementation("org.seleniumhq.selenium:selenium-chrome-driver:${seleniumVersion}")
    testImplementation("org.seleniumhq.selenium:selenium-firefox-driver:${seleniumVersion}")
    testImplementation("org.seleniumhq.selenium:selenium-support:${seleniumVersion}")
    testImplementation("org.seleniumhq.selenium:selenium-api:${seleniumVersion}")
    testImplementation("org.seleniumhq.selenium:selenium-remote-driver:${seleniumVersion}")
}

Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新した後、clean タスク実行 → Rebuild Project → build タスクを実行して BUILD SUCCESSFUL のメッセージが出ることを確認します。

f:id:ksby:20180714081908p:plain

Checkstyle を 8.8 → 8.11、PMD を 6.4.0 → 6.5.0 へバージョンアップする

build.gradle の以下の点を変更します。

checkstyle {
    configFile = file("${rootProject.projectDir}/config/checkstyle/google_checks.xml")
    toolVersion = '8.11'
    sourceSets = [project.sourceSets.main]
}

..........

pmd {
    toolVersion = "6.5.0"
    sourceSets = [project.sourceSets.main]
    ignoreFailures = true
    consoleOutput = true
    ruleSetFiles = rootProject.files("/config/pmd/pmd-project-rulesets.xml")
    ruleSets = []
}
  • checkstyletoolVersion = '8.8'toolVersion = '8.11' に変更します。
  • pmd で toolVersion = "6.4.0"toolVersion = "6.5.0" に変更します。

Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新した後、clean タスク実行 → Rebuild Project → build タスクを実行して BUILD SUCCESSFUL のメッセージが出ることを確認します。

f:id:ksby:20180714084434p:plain f:id:ksby:20180714084545p:plain

error-prone を 2.2.0 → 2.3.1 へバージョンアップする

gradle-errorprone-plugin を見ると最新バージョンは 0.0.14、mvnrepository.com の error_prone_core のページ を見ると最新バージョンは 2.3.1 でした。

build.gradle の以下の点を変更します。

plugins {
    id "java"
    id "eclipse"
    id "idea"
    // plugins {} block 内では ${springBootVersion} が使用できないので、バージョンを直接記述している
    id "org.springframework.boot" version "1.5.10.RELEASE"
    id "groovy"
    id "net.ltgt.errorprone" version "0.0.14"
    id "checkstyle"
    id "findbugs"
    id "pmd"
    id "com.moowork.node" version "1.2.0"
}

..........

dependencies {
    def spockVersion = "1.1-groovy-2.4"
    def domaVersion = "2.19.1"
    def lombokVersion = "1.16.20"
    def errorproneVersion = "2.3.1"
    def powermockVersion = "1.7.3"
    def seleniumVersion = "3.9.1"

    ..........
  • id "net.ltgt.errorprone" version "0.0.13"id "net.ltgt.errorprone" version "0.0.14" に変更します。
  • def errorproneVersion = "2.2.0"def errorproneVersion = "2.3.1" に変更します。

Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新した後、clean タスク実行 → Rebuild Project → build タスクを実行して BUILD SUCCESSFUL のメッセージが出ることを確認します。今回はエラーは出ませんでした。

f:id:ksby:20180714094057p:plain f:id:ksby:20180714094213p:plain

また build.gradle で以下の定義を記述していますが、

compileJava.options.compilerArgs += [
        '-Xep:RemoveUnusedImports:WARN'
        , '-Xep:NestedInstanceOfConditions:OFF'
        , '-Xep:InstanceOfAndCastMatchWrongType:OFF'
        , '-Xep:ParameterName:OFF'
]

, '-Xep:NestedInstanceOfConditions:OFF', '-Xep:InstanceOfAndCastMatchWrongType:OFF'コメントアウトしてもエラーは出なかったので削除します。

履歴

2018/07/14
初版発行。

Spring Boot + npm + Geb で入力フォームを作ってテストする ( その64 )( 入力画面3を作成する6、@SpringBootTest のテストは Spock+Groovy より JUnit4+Groovy の方が速い? )

概要

記事一覧はこちらです。

Spring Boot + npm + Geb で入力フォームを作ってテストする ( その63 )( MockMvc#perform 呼び出し時に .with(csrf()) を付けていなくてもテストが成功していた理由とは? ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • InquiryInputControllerTest で1件だけ確認画面の実装待ちで未実装だったテストを実装します。
    • @SpringBootTest がクラスに付加されているテストは Spock+Groovy で書くより JUnit4+Groovy で書いた方が速いように見えたので、その検証をしてみます。

参照したサイト・書籍

目次

  1. InquiryInputControllerTest クラスで未実装のテストを実装する
  2. @SpringBootTest を付けるテストの場合 Spock+Groovy より JUnit4+Groovy の構成の方がテストの実行は速いのか?
  3. 次回は。。。

手順

InquiryInputControllerTest クラスで未実装のテストを実装する

src/test/groovy/ksbysample/webapp/bootnpmgeb/web/inquiry/InquiryInputControllerTest.groovy を以下のように変更します。

        @Test
        void "項目全てに入力して前の画面へ戻るボタンをクリックすると入力画面2へ戻り、次へ戻るボタンを押して入力画面3へ戻ると以前入力したデータがセットされて表示される"() {
            ..........
        }

        @Test
        void "項目全てに入力して次へボタンをクリックすると確認画面へ遷移し、修正するボタンを押して入力画面3へ戻ると以前入力したデータがセットされて表示される"() {
            // JUit4+Groovy でテストを必ず失敗させるには以下のように書く
            // expect:
            // assert false, "確認画面を実装してからテストを作成する"

            when: "入力画面1で項目全てに入力して「次へ」ボタンをクリックする"
            MvcResult result = mockMvc.perform(
                    TestHelper.postForm("/inquiry/input/01?move=next", inquiryInput01Form_001).with(csrf()))
                    .andExpect(status().isFound())
                    .andExpect(redirectedUrlPattern("**/inquiry/input/02"))
                    .andReturn()
            MockHttpSession session = result.getRequest().getSession()

            and: "入力画面2で項目全てに入力して「次へ」ボタンをクリックする"
            mockMvc.perform(TestHelper.postForm("/inquiry/input/02?move=next", inquiryInput02Form_001).with(csrf()).session(session))
                    .andExpect(status().isFound())
                    .andExpect(redirectedUrlPattern("**/inquiry/input/03"))

            and: "入力画面3で項目全てに入力して「次へ」ボタンをクリックする"
            mockMvc.perform(TestHelper.postForm("/inquiry/input/03?move=next", inquiryInput03Form_001).with(csrf()).session(session))
                    .andExpect(status().isFound())
                    .andExpect(redirectedUrlPattern("**/inquiry/confirm"))

            then: "確認画面で「修正する」ボタンをクリックすると、入力画面3に戻り以前入力したデータがセットされて表示される"
            mockMvc.perform(get("/inquiry/input/03").session(session))
                    .andExpect(status().isOk())
                    .andExpect(html("select[name='type1'] option[selected]").val(inquiryInput03Form_001.type1))
                    .andExpect(html("input[name='type2'][checked='checked']").count(3))
                    .andExpect(html("#inquiry").val(inquiryInput03Form_001.inquiry))
                    .andExpect(html("input[name='survey'][checked='checked']").count(8))
        }

        @Test
        void "入力チェックエラーのあるデータで「次へ」ボタンをクリックするとIllegalArgumentExceptionが発生する"() {
            ..........
        }
  • void "項目全てに入力して次へボタンをクリックすると確認画面へ遷移し、修正するボタンを押して入力画面3へ戻ると以前入力したデータがセットされて表示される"() { ... } のテストを実装します。

InquiryInputControllerTest クラスのテストを全て実行して成功することを確認します。

f:id:ksby:20180711073035p:plain

InquiryConfirmControllerTest クラスと比較するとテストの実行が速かったです。比較してみます。

InquiryInputControllerTest クラスのテストを3回実行すると以下の結果ですが、

回数 かかった時間
1回目 3s 991ms
2回目 4s 500ms
3回目 4s 142ms

InquiryConfirmControllerTest クラスのテストを3回実行すると以下の結果でした。

回数 かかった時間
1回目 23s 558ms
2回目 23s 543ms
3回目 23s 649ms

JUnit4+Groovy の方が速そうですね。

@SpringBootTest を付けるテストの場合 Spock+Groovy より JUnit4+Groovy の構成の方がテストの実行は速いのか?

SurveyOptionsHelperTest クラスのテストで検証してみます。SurveyOptionsHelperTest クラスのテストは、

  • Spock+Groovy で書いている。
  • @SpringBootTest が付いている。

という内容で、3回実行してみると以下の結果でした。

回数 かかった時間
1回目 20s 117ms
2回目 18s 941ms
3回目 19s 423ms

SurveyOptionsHelperTest クラスを JUnit4+Groovy で書き直します。

package ksbysample.webapp.bootnpmgeb.helper.db

import ksbysample.webapp.bootnpmgeb.entity.SurveyOptions
import org.junit.Test
import org.junit.runner.RunWith
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.test.context.junit4.SpringRunner

import static org.assertj.core.api.Assertions.catchThrowable

@RunWith(SpringRunner)
@SpringBootTest
class SurveyOptionsHelperTest {

    @Autowired
    private SurveyOptionsHelper soh

    @Test
    void "登録されているグループ名を指定してselectItemListメソッドを呼ぶとリストが取得できる"() {
        setup:
        List<SurveyOptions> surveyOptionsList = soh.selectItemList("survey")

        expect:
        assert surveyOptionsList.size() == 8
        assert surveyOptionsList[0].itemValue == "1"
        assert surveyOptionsList[0].itemName == "選択肢1だけ長くしてみる"
        assert surveyOptionsList[7].itemValue == "8"
        assert surveyOptionsList[7].itemName == "8"
    }

    @Test
    void "登録されていないグループ名を指定してselectItemListメソッドを呼ぶとIllegalArgumentExceptionがthrowされる"() {
        setup:
        Throwable thrown = catchThrowable({
            List<SurveyOptions> surveyOptionsList = soh.selectItemList("notexists")
        })

        expect:
        assert thrown instanceof IllegalArgumentException
        assert thrown.message == "指定されたグループ名のデータは登録されていません"
    }

}
  • クラスに @RunWith(SpringRunner) アノテーションを付加します。
  • extends Specification を削除します。
  • 各テストメソッドに @Test アノテーションを付加し、defvoid に変更します。
  • expect: の下で結果を検証している処理に assert を付けます。
  • void "登録されていないグループ名を指定してselectItemListメソッドを呼ぶとIllegalArgumentExceptionがthrowされる"() { ... } テストメソッド内で soh.selectItemList("notexists") を呼び出している部分を Throwable thrown = catchThrowable({ ... }) で囲みます。結果の検証方法も groovy の形式に書き直します。

変更後に3回実行してみると以下の結果でした。やっぱり速いですね。

回数 かかった時間
1回目 458ms
2回目 475ms
3回目 504ms

今度は Spock+Groovy に戻してから、@SpringBootTest は外して適宜 Stub を使う方法で書き直してみます。

package ksbysample.webapp.bootnpmgeb.helper.db

import ksbysample.webapp.bootnpmgeb.dao.SurveyOptionsDao
import ksbysample.webapp.bootnpmgeb.entity.SurveyOptions
import spock.lang.Specification

class SurveyOptionsHelperTest extends Specification {

    SurveyOptionsDao surveyOptionsDao

    SurveyOptionsHelper soh

    def setup() {
        surveyOptionsDao = Stub(SurveyOptionsDao) {
            selectByGroupName("survey") >> [
                    [groupName: "survey", itemValue: "1", itemName: "選択肢1だけ長くしてみる", itemOrder: 1],
                    [groupName: "survey", itemValue: "2", itemName: "選択肢2", itemOrder: 2],
                    [groupName: "survey", itemValue: "3", itemName: "選択肢3", itemOrder: 3],
                    [groupName: "survey", itemValue: "4", itemName: "選択肢4", itemOrder: 4],
                    [groupName: "survey", itemValue: "5", itemName: "選択肢5が少し長い", itemOrder: 5],
                    [groupName: "survey", itemValue: "6", itemName: "選択肢6", itemOrder: 6],
                    [groupName: "survey", itemValue: "7", itemName: "選択肢7", itemOrder: 7],
                    [groupName: "survey", itemValue: "8", itemName: "8", itemOrder: 8]
            ]
            selectByGroupName("notexists") >> []
        }
        soh = new SurveyOptionsHelper(surveyOptionsDao)
    }

    def "登録されているグループ名を指定してselectItemListメソッドを呼ぶとリストが取得できる"() {
        setup:
        List<SurveyOptions> surveyOptionsList = soh.selectItemList("survey")

        expect:
        surveyOptionsList.size() == 8
        surveyOptionsList[0].itemValue == "1"
        surveyOptionsList[0].itemName == "選択肢1だけ長くしてみる"
        surveyOptionsList[7].itemValue == "8"
        surveyOptionsList[7].itemName == "8"
    }

    def "登録されていないグループ名を指定してselectItemListメソッドを呼ぶとIllegalArgumentExceptionがthrowされる"() {
        when:
        List<SurveyOptions> surveyOptionsList = soh.selectItemList("notexists")

        then:
        def e = thrown(IllegalArgumentException)
        e.getMessage() == "指定されたグループ名のデータは登録されていません"
    }

}
  • クラスに付けている @SpringBootTest、及びフィールドに付けている @Autowired を削除します。
  • SurveyOptionsDao surveyOptionsDao を追加します。
  • def setup() { ... } を追加し、surveyOptionsDao の Stub を生成してから soh のインスタンスを生成する処理を記述します。

変更後に3回実行してみると以下の結果でした。JUnit4+Groovy より時間がかかっているように表示されましたが、実際にテストが終了する速度はこちらの方が速いです。IntelliJ IDEA は何の時間を表示しているのだろうという疑問が。。。

回数 かかった時間
1回目 768ms
2回目 840ms
3回目 970ms

次回は。。。

利用しているライブラリを一通りバージョンアップしてから Geb を利用したテストを作成します。

履歴

2018/07/11
初版発行。

Spring Boot + npm + Geb で入力フォームを作ってテストする ( その63 )( MockMvc#perform 呼び出し時に .with(csrf()) を付けていなくてもテストが成功していた理由とは? )

概要

記事一覧はこちらです。

Spring Boot + npm + Geb で入力フォームを作ってテストする ( その62 )( 確認画面を作成する5 ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • 確認画面のテストを作成し終えた時に、今回 Spring Security を入れて CSRF対策も入っているはずなのに MockMvc#perform 呼び出し時に .with(csrf()) を付けていないのにテストが成功しているというおかしな事態になっていることに気づいたので、その原因を調査します。

参照したサイト・書籍

目次

  1. 単に .apply(springSecurity()) を付け忘れいてただけでした。。。
  2. InquiryInputControllerTest クラスを修正する
  3. InquiryConfirmControllerTest クラスを修正する

手順

単に .apply(springSecurity()) を付け忘れいてただけでした。。。

Spring Security Reference - 12. Spring MVC Test Integration を見たら1発で原因が分かりました。単なる .apply(springSecurity()) の付け忘れでした。。。

ということで、入力画面・確認画面のテストで MockMvc のインスタンス生成時に .apply(springSecurity()) を付けるよう修正して、テストを見直すことにします。

InquiryInputControllerTest クラスを修正する

最初に MockMvc のインスタンス生成時に .apply(springSecurity()) を付けるよう修正します。

        @Before
        void setup() {
            mockMvc = MockMvcBuilders.webAppContextSetup(context)
                    .apply(springSecurity())
                    .build()
        }

全てのテストを実行すると、今度は 403 のエラーが出るようになりました。

f:id:ksby:20180708161507p:plain

テスト内で POST でリクエストを送信しているところを TestHelper.postForm(...)TestHelper.postForm(...).with(csrf()) のように修正します。

再度全てのテストを実行すると、テストを実装していない(必ず失敗する)1件を除き成功しました。

f:id:ksby:20180708162618p:plain

InquiryConfirmControllerTest クラスを修正する

こちらも MockMvc のインスタンス生成時に .apply(springSecurity()) を付けるよう修正します。意識していませんでしたが、InquiryInputControllerTest は JUnit4+Groovy で書いていますが、InquiryConfirmControllerTest は Spock+Groovy で書いていました。

        def setup() {
            mockMvc = MockMvcBuilders.webAppContextSetup(context)
                    .apply(springSecurity())
                    .build()
            sql = new Sql(dataSource)
            sql.execute("truncate table INQUIRY_DATA")
        }

全てのテストを実行すると、こちらも 403 のエラーが出るようになりました。

f:id:ksby:20180708163828p:plain

テスト内で POST でリクエストを送信しているところを TestHelper.postForm(...)TestHelper.postForm(...).with(csrf()) のように修正します。

再度全てのテストを実行すると、今度は成功しました。

f:id:ksby:20180708164229p:plain

最後に修正後のソースコードを載せておきます。

ソースコード

InquiryInputControllerTest.groovy

package ksbysample.webapp.bootnpmgeb.web.inquiry

import ksbysample.common.test.helper.TestHelper
import ksbysample.webapp.bootnpmgeb.values.Type1Values
import ksbysample.webapp.bootnpmgeb.values.Type2Values
import ksbysample.webapp.bootnpmgeb.web.inquiry.form.InquiryInput01Form
import ksbysample.webapp.bootnpmgeb.web.inquiry.form.InquiryInput02Form
import ksbysample.webapp.bootnpmgeb.web.inquiry.form.InquiryInput03Form
import org.junit.Before
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.context.SpringBootTest
import org.springframework.mock.web.MockHttpSession
import org.springframework.test.context.junit4.SpringRunner
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.MvcResult
import org.springframework.test.web.servlet.setup.MockMvcBuilders
import org.springframework.web.context.WebApplicationContext
import org.yaml.snakeyaml.Yaml

import static ksbysample.common.test.matcher.HtmlResultMatchers.html
import static org.assertj.core.api.Assertions.catchThrowable
import static org.assertj.core.api.AssertionsForClassTypes.assertThat
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrlPattern
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status

@RunWith(Enclosed)
class InquiryInputControllerTest {

    @RunWith(SpringRunner)
    @SpringBootTest
    static class 入力画面1のテスト {
        private InquiryInput01Form inquiryInput01Form_001 =
                (InquiryInput01Form) new Yaml().load(getClass().getResourceAsStream("InquiryInput01Form_001.yaml"))

        @Autowired
        private WebApplicationContext context

        MockMvc mockMvc

        @Before
        void setup() {
            mockMvc = MockMvcBuilders.webAppContextSetup(context)
                    .apply(springSecurity())
                    .build()
        }

        @Test
        void "初期表示時は画面の項目には何もセットされない"() {
            expect:
            mockMvc.perform(get("/inquiry/input/01"))
                    .andExpect(status().isOk())
                    .andExpect(html("#lastname").val(""))
                    .andExpect(html("#firstname").val(""))
                    .andExpect(html("#lastkana").val(""))
                    .andExpect(html("#firstkana").val(""))
                    .andExpect(html("input[name='sex'][checked='checked']").notExists())
                    .andExpect(html("#age").val(""))
                    .andExpect(html("select[name='job'] option[selected]").notExists())
        }

        @Test
        void "項目全てに入力して入力画面2へ遷移してから戻ると以前入力したデータがセットされて表示される"() {
            expect: "項目全てに入力して「次へ」ボタンをクリックする"
            MvcResult result = mockMvc.perform(
                    TestHelper.postForm("/inquiry/input/01?move=next", inquiryInput01Form_001).with(csrf()))
                    .andExpect(status().isFound())
                    .andExpect(redirectedUrlPattern("**/inquiry/input/02"))
                    .andReturn()
            MockHttpSession session = result.getRequest().getSession()

            and: "再び入力画面1を表示する"
            mockMvc.perform(get("/inquiry/input/01").session(session))
                    .andExpect(status().isOk())
                    .andExpect(html("#lastname").val(inquiryInput01Form_001.lastname))
                    .andExpect(html("#firstname").val(inquiryInput01Form_001.firstname))
                    .andExpect(html("#lastkana").val(inquiryInput01Form_001.lastkana))
                    .andExpect(html("#firstkana").val(inquiryInput01Form_001.firstkana))
                    .andExpect(html("input[name='sex'][checked='checked']").val(inquiryInput01Form_001.sex))
                    .andExpect(html("#age").val(inquiryInput01Form_001.age))
                    .andExpect(html("select[name='job'] option[selected]").val(inquiryInput01Form_001.job))
        }

        @Test
        void "入力チェックエラーのあるデータで入力画面2へ遷移しようとするとIllegalArgumentExceptionが発生する"() {
            setup: "入力チェックエラーになるデータを用意する"
            inquiryInput01Form_001.lastname = "x" * 21

            expect: "入力画面1の「次へ」ボタンをクリックする"
            Throwable thrown = catchThrowable({
                mockMvc.perform(
                        TestHelper.postForm("/inquiry/input/01?move=next", inquiryInput01Form_001).with(csrf()))
                        .andExpect(status().isOk())
            })
            assertThat(thrown.cause).isInstanceOf(IllegalArgumentException)
        }

    }

    @RunWith(SpringRunner)
    @SpringBootTest
    static class 入力画面2のテスト {
        private InquiryInput01Form inquiryInput01Form_001 =
                (InquiryInput01Form) new Yaml().load(getClass().getResourceAsStream("InquiryInput01Form_001.yaml"))
        private InquiryInput02Form inquiryInput02Form_001 = new InquiryInput02Form(
                zipcode1: "102"
                , zipcode2: "0072"
                , address: "東京都千代田区飯田橋1-1"
                , tel1: "03"
                , tel2: "1234"
                , tel3: "5678"
                , email: "taro.tanaka@sample.co.jp")

        @Autowired
        private WebApplicationContext context

        MockMvc mockMvc

        @Before
        void setup() {
            mockMvc = MockMvcBuilders.webAppContextSetup(context)
                    .build()
        }

        @Test
        void "初期表示時は画面の項目には何もセットされない"() {
            expect:
            mockMvc.perform(get("/inquiry/input/02"))
                    .andExpect(status().isOk())
                    .andExpect(html("#zipcode1").val(""))
                    .andExpect(html("#zipcode2").val(""))
                    .andExpect(html("#address").val(""))
                    .andExpect(html("#tel1").val(""))
                    .andExpect(html("#tel2").val(""))
                    .andExpect(html("#tel3").val(""))
                    .andExpect(html("#email").val(""))
        }

        @Test
        void "項目全てに入力して前の画面へ戻るボタンをクリックすると入力画面1へ戻り、次へ戻るボタンを押して入力画面2へ戻ると以前入力したデータがセットされて表示される"() {
            when: "入力画面1で項目全てに入力して「次へ」ボタンをクリックする"
            MvcResult result = mockMvc.perform(
                    TestHelper.postForm("/inquiry/input/01?move=next", inquiryInput01Form_001).with(csrf()))
                    .andExpect(status().isFound())
                    .andExpect(redirectedUrlPattern("**/inquiry/input/02"))
                    .andReturn()
            MockHttpSession session = result.getRequest().getSession()

            and: "入力画面2で項目全てに入力して「前の画面へ戻る」ボタンをクリックする"
            mockMvc.perform(TestHelper.postForm("/inquiry/input/02?move=back", inquiryInput02Form_001).with(csrf()).session(session))
                    .andExpect(status().isFound())
                    .andExpect(redirectedUrlPattern("**/inquiry/input/01"))

            and: "入力画面1で「次へ」ボタンをクリックする"
            mockMvc.perform(TestHelper.postForm("/inquiry/input/01?move=next", inquiryInput01Form_001).with(csrf()).session(session))
                    .andExpect(status().isFound())
                    .andExpect(redirectedUrlPattern("**/inquiry/input/02"))

            then: "入力画面2が以前入力したデータがセットされて表示される"
            mockMvc.perform(get("/inquiry/input/02").session(session))
                    .andExpect(status().isOk())
                    .andExpect(html("#zipcode1").val(inquiryInput02Form_001.zipcode1))
                    .andExpect(html("#zipcode2").val(inquiryInput02Form_001.zipcode2))
                    .andExpect(html("#address").val(inquiryInput02Form_001.address))
                    .andExpect(html("#tel1").val(inquiryInput02Form_001.tel1))
                    .andExpect(html("#tel2").val(inquiryInput02Form_001.tel2))
                    .andExpect(html("#tel3").val(inquiryInput02Form_001.tel3))
                    .andExpect(html("#email").val(inquiryInput02Form_001.email))
        }

        @Test
        void "項目全てに入力して次へボタンをクリックすると入力画面3へ遷移し、前の画面へ戻るボタンを押して入力画面2へ戻ると以前入力したデータがセットされて表示される"() {
            when: "入力画面2で項目全てに入力して「次へ」ボタンをクリックする"
            MvcResult result = mockMvc.perform(TestHelper.postForm("/inquiry/input/02?move=next", inquiryInput02Form_001).with(csrf()))
                    .andExpect(status().isFound())
                    .andExpect(redirectedUrlPattern("**/inquiry/input/03"))
                    .andReturn()
            MockHttpSession session = result.getRequest().getSession()

            and: "入力画面3で「前の画面へ戻る」ボタンをクリックする"
            mockMvc.perform(TestHelper.postForm("/inquiry/input/03?move=back", inquiryInput01Form_001).with(csrf()).session(session))
                    .andExpect(status().isFound())
                    .andExpect(redirectedUrlPattern("**/inquiry/input/02"))

            then: "入力画面2が以前入力したデータがセットされて表示される"
            mockMvc.perform(get("/inquiry/input/02").session(session))
                    .andExpect(status().isOk())
                    .andExpect(html("#zipcode1").val(inquiryInput02Form_001.zipcode1))
                    .andExpect(html("#zipcode2").val(inquiryInput02Form_001.zipcode2))
                    .andExpect(html("#address").val(inquiryInput02Form_001.address))
                    .andExpect(html("#tel1").val(inquiryInput02Form_001.tel1))
                    .andExpect(html("#tel2").val(inquiryInput02Form_001.tel2))
                    .andExpect(html("#tel3").val(inquiryInput02Form_001.tel3))
                    .andExpect(html("#email").val(inquiryInput02Form_001.email))
        }

        @Test
        void "入力チェックエラーのあるデータで「前の画面へ戻る」ボタンをクリックするとIllegalArgumentExceptionが発生する"() {
            setup: "入力チェックエラーになるデータを用意する"
            inquiryInput02Form_001.zipcode1 = "1"

            expect: "入力画面2の「前の画面へ戻る」ボタンをクリックする"
            Throwable thrown = catchThrowable({
                mockMvc.perform(
                        TestHelper.postForm("/inquiry/input/02?move=back", inquiryInput02Form_001).with(csrf()))
                        .andExpect(status().isOk())
            })
            assertThat(thrown.cause).isInstanceOf(IllegalArgumentException)
        }

        @Test
        void "入力チェックエラーのあるデータで「次へ」ボタンをクリックするとIllegalArgumentExceptionが発生する"() {
            setup: "入力チェックエラーになるデータを用意する"
            inquiryInput02Form_001.zipcode1 = "1"

            expect: "入力画面2の「次へ」ボタンをクリックする"
            Throwable thrown = catchThrowable({
                mockMvc.perform(
                        TestHelper.postForm("/inquiry/input/02?move=next", inquiryInput02Form_001).with(csrf()))
                        .andExpect(status().isOk())
            })
            assertThat(thrown.cause).isInstanceOf(IllegalArgumentException)
        }

    }

    @RunWith(SpringRunner)
    @SpringBootTest
    static class 入力画面3のテスト {
        private InquiryInput01Form inquiryInput01Form_001 =
                (InquiryInput01Form) new Yaml().load(getClass().getResourceAsStream("InquiryInput01Form_001.yaml"))
        private InquiryInput02Form inquiryInput02Form_001 = new InquiryInput02Form(
                zipcode1: "102"
                , zipcode2: "0072"
                , address: "東京都千代田区飯田橋1-1"
                , tel1: "03"
                , tel2: "1234"
                , tel3: "5678"
                , email: "taro.tanaka@sample.co.jp")
        private InquiryInput03Form inquiryInput03Form_001 = new InquiryInput03Form(
                type1: Type1Values.PRODUCT.value
                , type2: [Type2Values.ESTIMATE.value, Type2Values.CATALOGUE.value, Type2Values.OTHER.value]
                , inquiry: "これはテストです"
                , survey: ["1", "2", "3", "4", "5", "6", "7", "8"])

        @Autowired
        private WebApplicationContext context

        MockMvc mockMvc

        @Before
        void setup() {
            mockMvc = MockMvcBuilders.webAppContextSetup(context)
                    .build()
        }

        @Test
        void "初期表示時は画面の項目には何もセットされない"() {
            expect:
            mockMvc.perform(get("/inquiry/input/03"))
                    .andExpect(status().isOk())
                    .andExpect(html("select[name='type1'] option[selected]").notExists())
                    .andExpect(html("input[name='type2'][checked='checked']").notExists())
                    .andExpect(html("#inquiry").val(""))
                    .andExpect(html("input[name='survey'][checked='checked']").notExists())
        }

        @Test
        void "項目全てに入力して前の画面へ戻るボタンをクリックすると入力画面2へ戻り、次へ戻るボタンを押して入力画面3へ戻ると以前入力したデータがセットされて表示される"() {
            when: "入力画面1で項目全てに入力して「次へ」ボタンをクリックする"
            MvcResult result = mockMvc.perform(
                    TestHelper.postForm("/inquiry/input/01?move=next", inquiryInput01Form_001).with(csrf()))
                    .andExpect(status().isFound())
                    .andExpect(redirectedUrlPattern("**/inquiry/input/02"))
                    .andReturn()
            MockHttpSession session = result.getRequest().getSession()

            and: "入力画面2で項目全てに入力して「次へ」ボタンをクリックする"
            mockMvc.perform(TestHelper.postForm("/inquiry/input/02?move=next", inquiryInput02Form_001).with(csrf()).session(session))
                    .andExpect(status().isFound())
                    .andExpect(redirectedUrlPattern("**/inquiry/input/03"))

            and: "入力画面3で項目全てに入力して「前の画面へ戻る」ボタンをクリックする"
            mockMvc.perform(TestHelper.postForm("/inquiry/input/03?move=back", inquiryInput03Form_001).with(csrf()).session(session))
                    .andExpect(status().isFound())
                    .andExpect(redirectedUrlPattern("**/inquiry/input/02"))

            and: "入力画面2で「次へ」ボタンをクリックする"
            mockMvc.perform(TestHelper.postForm("/inquiry/input/02?move=next", inquiryInput02Form_001).with(csrf()).session(session))
                    .andExpect(status().isFound())
                    .andExpect(redirectedUrlPattern("**/inquiry/input/03"))

            then: "入力画面3が以前入力したデータがセットされて表示される"
            mockMvc.perform(get("/inquiry/input/03").session(session))
                    .andExpect(status().isOk())
                    .andExpect(html("select[name='type1'] option[selected]").val(inquiryInput03Form_001.type1))
                    .andExpect(html("input[name='type2'][checked='checked']").count(3))
                    .andExpect(html("#inquiry").val(inquiryInput03Form_001.inquiry))
                    .andExpect(html("input[name='survey'][checked='checked']").count(8))
        }

        @Test
        void "項目全てに入力して次へボタンをクリックすると確認画面へ遷移し、前の画面へ戻るボタンを押して入力画面3へ戻ると以前入力したデータがセットされて表示される"() {
            expect:
            assert false, "確認画面を実装してからテストを作成する"
        }

        @Test
        void "入力チェックエラーのあるデータで「次へ」ボタンをクリックするとIllegalArgumentExceptionが発生する"() {
            setup: "入力チェックエラーになるデータを用意する"
            inquiryInput03Form_001.type1 = ""

            expect: "入力画面3の「次へ」ボタンをクリックする"
            Throwable thrown = catchThrowable({
                mockMvc.perform(
                        TestHelper.postForm("/inquiry/input/03?move=next", inquiryInput03Form_001).with(csrf()))
                        .andExpect(status().isOk())
            })
            assertThat(thrown.cause).isInstanceOf(IllegalArgumentException)
        }

    }

}

InquiryConfirmControllerTest.groovy

package ksbysample.webapp.bootnpmgeb.web.inquiry

import groovy.sql.Sql
import ksbysample.common.test.helper.TestHelper
import ksbysample.common.test.rule.mail.MailServerResource
import ksbysample.webapp.bootnpmgeb.dao.InquiryDataDao
import ksbysample.webapp.bootnpmgeb.entity.SurveyOptions
import ksbysample.webapp.bootnpmgeb.helper.db.SurveyOptionsHelper
import ksbysample.webapp.bootnpmgeb.values.*
import ksbysample.webapp.bootnpmgeb.web.inquiry.form.InquiryInput01Form
import ksbysample.webapp.bootnpmgeb.web.inquiry.form.InquiryInput02Form
import ksbysample.webapp.bootnpmgeb.web.inquiry.form.InquiryInput03Form
import org.apache.commons.lang3.StringUtils
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.context.SpringBootTest
import org.springframework.core.io.ClassPathResource
import org.springframework.http.MediaType
import org.springframework.mock.web.MockHttpSession
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.MvcResult
import org.springframework.test.web.servlet.setup.MockMvcBuilders
import org.springframework.web.context.WebApplicationContext
import org.yaml.snakeyaml.Yaml
import spock.lang.Specification

import javax.mail.internet.MimeMessage
import javax.sql.DataSource
import java.util.stream.Collectors

import static ksbysample.common.test.matcher.HtmlResultMatchers.html
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrlPattern
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status

@RunWith(Enclosed)
class InquiryConfirmControllerTest {

    @SpringBootTest
    static class 確認画面のテスト extends Specification {

        InquiryInput01Form inquiryInput01Form_001 =
                (InquiryInput01Form) new Yaml().load(getClass().getResourceAsStream("InquiryInput01Form_001.yaml"))
        InquiryInput02Form inquiryInput02Form_001 = new InquiryInput02Form(
                zipcode1: "102"
                , zipcode2: "0072"
                , address: "東京都千代田区飯田橋1-1"
                , tel1: "03"
                , tel2: "1234"
                , tel3: "5678"
                , email: "taro.tanaka@sample.co.jp")
        InquiryInput03Form inquiryInput03Form_001 = new InquiryInput03Form(
                type1: Type1Values.PRODUCT.value
                , type2: [Type2Values.ESTIMATE.value, Type2Values.CATALOGUE.value, Type2Values.OTHER.value]
                , inquiry: "これはテストです"
                , survey: ["1", "2", "3", "4", "5", "6", "7", "8"])

        @Rule
        MailServerResource mailServerResource = new MailServerResource()

        @Autowired
        WebApplicationContext context

        @Autowired
        DataSource dataSource

        @Autowired
        ValuesHelper vh

        @Autowired
        SurveyOptionsHelper soh

        @Autowired
        InquiryDataDao inquiryDataDao

        MockMvc mockMvc

        Sql sql

        def setup() {
            mockMvc = MockMvcBuilders.webAppContextSetup(context)
                    .apply(springSecurity())
                    .build()
            sql = new Sql(dataSource)
            sql.execute("truncate table INQUIRY_DATA")
        }

        def teardown() {
            sql.close()
        }

        def "入力画面1~3で全ての項目に入力した場合のテスト"() {
            when: "入力画面1で項目全てに入力して「次へ」ボタンをクリックする"
            MvcResult result = mockMvc.perform(TestHelper.postForm("/inquiry/input/01?move=next", inquiryInput01Form_001).with(csrf()))
                    .andExpect(status().isFound())
                    .andExpect(redirectedUrlPattern("**/inquiry/input/02"))
                    .andReturn()
            MockHttpSession session = result.getRequest().getSession()

            and: "入力画面2で項目全てに入力して「次へ」ボタンをクリックする"
            mockMvc.perform(TestHelper.postForm("/inquiry/input/02?move=next", inquiryInput02Form_001).with(csrf()).session(session))
                    .andExpect(status().isFound())
                    .andExpect(redirectedUrlPattern("**/inquiry/input/03"))

            and: "入力画面3で項目全てに入力して「次へ」ボタンをクリックする"
            mockMvc.perform(TestHelper.postForm("/inquiry/input/03?move=next", inquiryInput03Form_001).with(csrf()).session(session))
                    .andExpect(status().isFound())
                    .andExpect(redirectedUrlPattern("**/inquiry/confirm"))

            then: "確認画面に入力画面1~3で入力したデータが表示される"
            mockMvc.perform(get("/inquiry/confirm").session(session))
                    .andExpect(status().isOk())
                    .andExpect(html("#name").text(inquiryInput01Form_001.lastname + " " + inquiryInput01Form_001.firstname))
                    .andExpect(html("#kana").text(inquiryInput01Form_001.lastkana + " " + inquiryInput01Form_001.firstkana))
                    .andExpect(html("#sex").text(vh.getText(SexValues, inquiryInput01Form_001.sex)))
                    .andExpect(html("#age").text(inquiryInput01Form_001.age + " 歳"))
                    .andExpect(html("#job").text(vh.getText(JobValues, inquiryInput01Form_001.job)))
                    .andExpect(html("#zipcode").text("〒 " + inquiryInput02Form_001.zipcode1 + "-" + inquiryInput02Form_001.zipcode2))
                    .andExpect(html("#address").text(inquiryInput02Form_001.address))
                    .andExpect(html("#tel").text(inquiryInput02Form_001.tel1 + "-" + inquiryInput02Form_001.tel2 + "-" + inquiryInput02Form_001.tel3))
                    .andExpect(html("#email").text(inquiryInput02Form_001.email))
                    .andExpect(html("#type1").text(vh.getText(Type1Values, inquiryInput03Form_001.type1)))
                    .andExpect(html("#type2").text([Type2Values.ESTIMATE.text
                                                    , Type2Values.CATALOGUE.text
                                                    , Type2Values.OTHER.text].stream().collect(Collectors.joining("、"))))
                    .andExpect(html("#inquiry").text(inquiryInput03Form_001.inquiry))
                    .andExpect(html("#survey > ul > li").count(8))
                    .andExpect(html("#survey > ul > li:nth-of-type(1)").text(
                    soh.selectItemList("survey").stream()
                            .filter({ SurveyOptions surveyOptions -> StringUtils.equals(surveyOptions.itemValue, "1") })
                            .map { SurveyOptions surveyOptions -> surveyOptions.itemName }
                            .findFirst().get()))

            and: "確認画面で「送信」ボタンをクリックする"
            mockMvc.perform(post("/inquiry/confirm/send").contentType(MediaType.APPLICATION_FORM_URLENCODED).with(csrf()).session(session))
                    .andExpect(status().isFound())
                    .andExpect(redirectedUrlPattern("**/inquiry/complete/"))

            then: "DBとメールを確認する"
            def rows = sql.rows("SELECT * FROM INQUIRY_DATA")
            rows.size() == 1
            rows[0]["lastname"] == "12345678901234567890"
            rows[0]["firstname"] == "12345678901234567890"
            rows[0]["lastkana"] == "あいうえおかきくけこさしすせそたちつてと"
            rows[0]["firstkana"] == "なにぬねのはひふへほまみむめもあいうえお"
            rows[0]["sex"] == "2"
            rows[0]["age"] == 999
            rows[0]["job"] == "3"
            rows[0]["zipcode1"] == "102"
            rows[0]["zipcode2"] == "0072"
            rows[0]["address"] == "東京都千代田区飯田橋1-1"
            rows[0]["tel1"] == "03"
            rows[0]["tel2"] == "1234"
            rows[0]["tel3"] == "5678"
            rows[0]["email"] == "taro.tanaka@sample.co.jp"
            rows[0]["type1"] == "1"
            rows[0]["type2"] == "1,2,3"
            rows[0]["inquiry"].asciiStream.text == inquiryInput03Form_001.inquiry
            rows[0]["survey"] == "1,2,3,4,5,6,7,8"

            mailServerResource.messagesCount == 1
            MimeMessage message = mailServerResource.firstMessage
            message.subject == "問い合わせフォームからお問い合わせがありました"
            message.content == new ClassPathResource("ksbysample/webapp/bootnpmgeb/web/inquiry/inquirymail-body_001.txt").inputStream.text
        }

        def "入力画面1~3で必須項目だけに入力した場合のテスト"() {
            when: "入力画面1で必須項目だけに入力して「次へ」ボタンをクリックする"
            inquiryInput01Form_001.job = StringUtils.EMPTY
            MvcResult result = mockMvc.perform(TestHelper.postForm("/inquiry/input/01?move=next", inquiryInput01Form_001).with(csrf()))
                    .andExpect(status().isFound())
                    .andExpect(redirectedUrlPattern("**/inquiry/input/02"))
                    .andReturn()
            MockHttpSession session = result.getRequest().getSession()

            and: "入力画面2で必須項目だけに入力して「次へ」ボタンをクリックする"
            inquiryInput02Form_001.tel1 = StringUtils.EMPTY
            inquiryInput02Form_001.tel2 = StringUtils.EMPTY
            inquiryInput02Form_001.tel3 = StringUtils.EMPTY
            mockMvc.perform(TestHelper.postForm("/inquiry/input/02?move=next", inquiryInput02Form_001).with(csrf()).session(session))
                    .andExpect(status().isFound())
                    .andExpect(redirectedUrlPattern("**/inquiry/input/03"))

            and: "入力画面3で必須項目だけに入力して「次へ」ボタンをクリックする"
            inquiryInput03Form_001.survey = []
            mockMvc.perform(TestHelper.postForm("/inquiry/input/03?move=next", inquiryInput03Form_001).with(csrf()).session(session))
                    .andExpect(status().isFound())
                    .andExpect(redirectedUrlPattern("**/inquiry/confirm"))

            then: "確認画面に入力画面1~3で入力したデータが表示される"
            mockMvc.perform(get("/inquiry/confirm").session(session))
                    .andExpect(status().isOk())
                    .andExpect(html("#name").text(inquiryInput01Form_001.lastname + " " + inquiryInput01Form_001.firstname))
                    .andExpect(html("#kana").text(inquiryInput01Form_001.lastkana + " " + inquiryInput01Form_001.firstkana))
                    .andExpect(html("#sex").text(vh.getText(SexValues, inquiryInput01Form_001.sex)))
                    .andExpect(html("#age").text(inquiryInput01Form_001.age + " 歳"))
                    .andExpect(html("#job").text(StringUtils.EMPTY))
                    .andExpect(html("#zipcode").text("〒 " + inquiryInput02Form_001.zipcode1 + "-" + inquiryInput02Form_001.zipcode2))
                    .andExpect(html("#address").text(inquiryInput02Form_001.address))
                    .andExpect(html("#tel").text(StringUtils.EMPTY))
                    .andExpect(html("#email").text(inquiryInput02Form_001.email))
                    .andExpect(html("#type1").text(vh.getText(Type1Values, inquiryInput03Form_001.type1)))
                    .andExpect(html("#type2").text([Type2Values.ESTIMATE.text
                                                    , Type2Values.CATALOGUE.text
                                                    , Type2Values.OTHER.text].stream().collect(Collectors.joining("、"))))
                    .andExpect(html("#inquiry").text(inquiryInput03Form_001.inquiry))
                    .andExpect(html("#survey > ul > li").count(0))

            and: "確認画面で「送信」ボタンをクリックする"
            mockMvc.perform(post("/inquiry/confirm/send").contentType(MediaType.APPLICATION_FORM_URLENCODED).with(csrf()).session(session))
                    .andExpect(status().isFound())
                    .andExpect(redirectedUrlPattern("**/inquiry/complete/"))

            then: "DBとメールを確認する"
            def rows = sql.rows("SELECT * FROM INQUIRY_DATA")
            rows.size() == 1
            rows[0]["lastname"] == "12345678901234567890"
            rows[0]["firstname"] == "12345678901234567890"
            rows[0]["lastkana"] == "あいうえおかきくけこさしすせそたちつてと"
            rows[0]["firstkana"] == "なにぬねのはひふへほまみむめもあいうえお"
            rows[0]["sex"] == "2"
            rows[0]["age"] == 999
            rows[0]["job"] == StringUtils.EMPTY
            rows[0]["zipcode1"] == "102"
            rows[0]["zipcode2"] == "0072"
            rows[0]["address"] == "東京都千代田区飯田橋1-1"
            rows[0]["tel1"] == StringUtils.EMPTY
            rows[0]["tel2"] == StringUtils.EMPTY
            rows[0]["tel3"] == StringUtils.EMPTY
            rows[0]["email"] == "taro.tanaka@sample.co.jp"
            rows[0]["type1"] == "1"
            rows[0]["type2"] == "1,2,3"
            rows[0]["inquiry"].asciiStream.text == inquiryInput03Form_001.inquiry
            rows[0]["survey"] == StringUtils.EMPTY

            mailServerResource.messagesCount == 1
            MimeMessage message = mailServerResource.firstMessage
            message.subject == "問い合わせフォームからお問い合わせがありました"
            message.content == new ClassPathResource("ksbysample/webapp/bootnpmgeb/web/inquiry/inquirymail-body_002.txt").inputStream.text
        }

    }

}

履歴

2018/07/08
初版発行。

Spring Boot + npm + Geb で入力フォームを作ってテストする ( その62 )( 確認画面を作成する5 )

概要

記事一覧はこちらです。

Spring Boot + npm + Geb で入力フォームを作ってテストする ( その61 )( 確認画面を作成する4 ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • 確認画面の作成
    • サーバ側のテストを作成します。

参照したサイト・書籍

目次

  1. GreenMail を導入する
  2. confirm.html の各項目に id 属性を付ける
  3. 送信したメールの本文を assert する時に使用するファイルを作成する
  4. InquiryConfirmController クラスのテストを作成する
  5. メモ書き+次回は。。。

手順

GreenMail を導入する

メール送信のテストをするために GreenMail を導入します。

build.gradle の以下の点を変更します。

dependencies {
    ..........

    // dependency-management-plugin によりバージョン番号が自動で設定されないもの、あるいは最新バージョンを指定したいもの
    ..........
    testCompile("com.icegreen:greenmail:1.5.7")

    // for lombok
  • testCompile("com.icegreen:greenmail:1.5.7") を追加します。

変更後、Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。

次にテスト時に GreenMail のサーバを起動/停止するためのクラスを ksbysample-webapp-lending からコピーします。

src/test/java/ksbysample/common/test の下に rule/mail パッケージを作成した後、MailServerResource.java ダウンロードして配置します。配置後、@Component アノテーションがあると Spock から使用できないので削除します。

confirm.html の各項目に id 属性を付ける

確認画面のテストをしやすくするために src/main/resources/templates/web/inquiry/confirm.html を以下のように変更します。

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="~{web/common/fragments :: common_header(~{::title}, ~{::link}, ~{::style})}">
  <title>入力フォーム - 確認画面</title>

  <style>
    /* セルの上部の罫線を表示しないようようにし、セル内の余白を詰める */
    .table > tbody > tr > th,
    .table > tbody > tr > td {
      border-top: none;
      padding: 5px;
    }
  </style>
</head>

<body class="skin-blue layout-top-nav">
<div class="wrapper">

  <!-- Content Wrapper. Contains page content -->
  <div class="content-wrapper">
    <!-- Content Header (Page header) -->
    <section class="content-header">
      <h1>
        確認画面
      </h1>
    </section>

    <!-- Main content -->
    <section class="content">
      <div class="row">
        <div class="col-xs-12">
          <!--/*@thymesVar id="confirmForm" type="ksbysample.webapp.bootnpmgeb.web.inquiry.form.ConfirmForm"*/-->
          <form id="confirmForm" method="post" action=""
                th:action="@{/inquiry/confirm/}"
                th:object="${confirmForm}">
            <table class="table">
              <colgroup>
                <col width="15%"/>
                <col width="85%"/>
              </colgroup>

              <!-- 入力画面1の項目 -->
              <tr>
                <th nowrap>お名前(漢字)</th>
                <td id="name" th:text="*{name}">田中 太郎</td>
              </tr>
              <tr>
                <th nowrap>お名前(かな)</th>
                <td id="kana" th:text="*{kana}">たなか たろう</td>
              </tr>
              <tr>
                <th nowrap>性別</th>
                <td id="sex" th:text="*{sex}">男性</td>
              </tr>
              <tr>
                <th nowrap>年齢</th>
                <td id="age">
                  <th:block th:text="*{age}">30</th:block></td>
              </tr>
              <tr>
                <th nowrap>職業</th>
                <td id="job" th:text="*{job}">会社員</td>
              </tr>
              <tr>
                <td colspan="2">
                  <button class="btn bg-blue js-btn-input01"><i class="fa fa-arrow-left"></i> 修正する</button>
                </td>
              </tr>

              <!-- 入力画面2の項目 -->
              <tr>
                <th nowrap>郵便番号</th>
                <td id="zipcode"><th:block th:text="*{zipcode}">102-0072</th:block>
                </td>
              </tr>
              <tr>
                <th nowrap>住所</th>
                <td id="address" th:text="*{address}">東京都千代田区飯田橋1-1</td>
              </tr>
              <tr>
                <th nowrap>電話番号</th>
                <td id="tel" th:text="*{tel}">03-1234-5678</td>
              </tr>
              <tr>
                <th nowrap>メールアドレス</th>
                <td id="email" th:text="*{email}">taro.tanaka@sample.co.jp</td>
              </tr>
              <tr>
                <td colspan="2">
                  <button class="btn bg-blue js-btn-input02"><i class="fa fa-arrow-left"></i> 修正する</button>
                </td>
              </tr>

              <!-- 入力画面3の項目 -->
              <tr>
                <th nowrap>お問い合わせの種類1</th>
                <td id="type1" th:text="*{type1}">製品に関するお問い合わせ</td>
              </tr>
              <tr>
                <th nowrap>お問い合わせの種類2</th>
                <td id="type2" th:text="*{type2}">見積が欲しい</td>
              </tr>
              <tr>
                <th nowrap>お問い合わせの内容</th>
                <td id="inquiry"
                    th:utext="*{inquiry} ? ${#strings.replace(#strings.escapeXml(confirmForm.inquiry), T(java.lang.System).getProperty('line.separator'), '&lt;br /&gt;')} : ''">
                  ここに、<br/>
                  入力されたお問い合わせの内容が表示されます。
                </td>
              </tr>
              <tr>
                <th nowrap>アンケート</th>
                <td id="survey">
                  <ul style="padding-left: 20px">
                    <li th:each="sv : *{survey}"
                        th:text="${sv}">選択肢1だけ長くしてみる
                    </li>
                  </ul>
                </td>
              </tr>
              <tr>
                <td colspan="2">
                  <button class="btn bg-blue js-btn-input03"><i class="fa fa-arrow-left"></i> 修正する</button>
                </td>
              </tr>
            </table>

            <div class="text-center">
              <button class="btn bg-green js-btn-send"><i class="fa fa-arrow-right"></i> 送信する</button>
            </div>
          </form>
        </div>
      </div>
    </section>
    <!-- /.content -->
  </div>
  <!-- /.content-wrapper -->
</div>
<!-- ./wrapper -->

<!-- REQUIRED JS SCRIPTS -->
<script src="/js/inquiry/confirm.js"></script>

</body>
</html>
  • 各項目に表示されているデータを取得しやすいようにするために td タグに id 属性を付加します。
  • 「アンケート」に記述している th:each="sv : *{survey}" を ul タグに書いていましたが、この位置だと li タグだけでなく ul タグも複数出力されていたので li タグへ移動します。

送信したメールの本文を assert する時に使用するファイルを作成する

src/test/resources/ksbysample/webapp/bootnpmgeb/web/inquiry の下に inquirymail-body_001.txt, inquirymail-body_002.txt を新規作成し、以下の内容を記述します。

■inquirymail-body_001.txt

問い合わせフォームから入力された内容は以下の通りです。

お名前(漢字):12345678901234567890 12345678901234567890
お名前(かな):あいうえおかきくけこさしすせそたちつてと なにぬねのはひふへほまみむめもあいうえお
性別:女性
年齢:999歳
職業:その他
郵便番号:〒102-0072
住所:東京都千代田区飯田橋1-1
電話番号:03-1234-5678
メールアドレス:taro.tanaka@sample.co.jp

お問い合わせの種類1:製品に関するお問い合わせ
お問い合わせの種類2:見積が欲しい、資料が欲しい、その他の問い合わせ
お問い合わせの内容:
これはテストです
アンケート:
・選択肢1だけ長くしてみる
・選択肢2
・選択肢3
・選択肢4
・選択肢5が少し長い
・選択肢6
・選択肢7
・8

■inquirymail-body_002.txt

問い合わせフォームから入力された内容は以下の通りです。

お名前(漢字):12345678901234567890 12345678901234567890
お名前(かな):あいうえおかきくけこさしすせそたちつてと なにぬねのはひふへほまみむめもあいうえお
性別:女性
年齢:999歳
職業:
郵便番号:〒102-0072
住所:東京都千代田区飯田橋1-1
電話番号:
メールアドレス:taro.tanaka@sample.co.jp

お問い合わせの種類1:製品に関するお問い合わせ
お問い合わせの種類2:見積が欲しい、資料が欲しい、その他の問い合わせ
お問い合わせの内容:
これはテストです
アンケート:

InquiryConfirmController クラスのテストを作成する

src/main/java/ksbysample/webapp/bootnpmgeb/web/inquiry/InquiryConfirmController.java で Ctrl+Shift+T を押して「Create Test」ダイアログを表示してから、以下の画像の値にした後「OK」ボタンをクリックします。

f:id:ksby:20180628005452p:plain

src/test/groovy/ksbysample/webapp/bootnpmgeb/web/inquiry/InquiryConfirmControllerTest.groovy が新規作成されるので、以下の内容を記述します。

package ksbysample.webapp.bootnpmgeb.web.inquiry

import groovy.sql.Sql
import ksbysample.common.test.helper.TestHelper
import ksbysample.common.test.rule.mail.MailServerResource
import ksbysample.webapp.bootnpmgeb.dao.InquiryDataDao
import ksbysample.webapp.bootnpmgeb.entity.SurveyOptions
import ksbysample.webapp.bootnpmgeb.helper.db.SurveyOptionsHelper
import ksbysample.webapp.bootnpmgeb.values.*
import ksbysample.webapp.bootnpmgeb.web.inquiry.form.InquiryInput01Form
import ksbysample.webapp.bootnpmgeb.web.inquiry.form.InquiryInput02Form
import ksbysample.webapp.bootnpmgeb.web.inquiry.form.InquiryInput03Form
import org.apache.commons.lang3.StringUtils
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.context.SpringBootTest
import org.springframework.core.io.ClassPathResource
import org.springframework.http.MediaType
import org.springframework.mock.web.MockHttpSession
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.MvcResult
import org.springframework.test.web.servlet.setup.MockMvcBuilders
import org.springframework.web.context.WebApplicationContext
import org.yaml.snakeyaml.Yaml
import spock.lang.Specification

import javax.mail.internet.MimeMessage
import javax.sql.DataSource
import java.util.stream.Collectors

import static ksbysample.common.test.matcher.HtmlResultMatchers.html
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.redirectedUrlPattern
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status

@RunWith(Enclosed)
class InquiryConfirmControllerTest {

    @SpringBootTest
    static class 確認画面のテスト extends Specification {

        private InquiryInput01Form inquiryInput01Form_001 =
                (InquiryInput01Form) new Yaml().load(getClass().getResourceAsStream("InquiryInput01Form_001.yaml"))
        private InquiryInput02Form inquiryInput02Form_001 = new InquiryInput02Form(
                zipcode1: "102"
                , zipcode2: "0072"
                , address: "東京都千代田区飯田橋1-1"
                , tel1: "03"
                , tel2: "1234"
                , tel3: "5678"
                , email: "taro.tanaka@sample.co.jp")
        private InquiryInput03Form inquiryInput03Form_001 = new InquiryInput03Form(
                type1: Type1Values.PRODUCT.value
                , type2: [Type2Values.ESTIMATE.value, Type2Values.CATALOGUE.value, Type2Values.OTHER.value]
                , inquiry: "これはテストです"
                , survey: ["1", "2", "3", "4", "5", "6", "7", "8"])

        @Rule
        public MailServerResource mailServerResource = new MailServerResource()

        @Autowired
        private WebApplicationContext context

        @Autowired
        private DataSource dataSource

        @Autowired
        private ValuesHelper vh

        @Autowired
        private SurveyOptionsHelper soh

        @Autowired
        private InquiryDataDao inquiryDataDao

        MockMvc mockMvc

        Sql sql

        void setup() {
            mockMvc = MockMvcBuilders.webAppContextSetup(context)
                    .build()
            sql = new Sql(dataSource)
            sql.execute("truncate table INQUIRY_DATA")
        }

        void teardown() {
            sql.close()
        }

        def "入力画面1~3で全ての項目に入力した場合のテスト"() {
            when: "入力画面1で項目全てに入力して「次へ」ボタンをクリックする"
            MvcResult result = mockMvc.perform(
                    TestHelper.postForm("/inquiry/input/01?move=next", inquiryInput01Form_001))
                    .andExpect(status().isFound())
                    .andExpect(redirectedUrlPattern("**/inquiry/input/02"))
                    .andReturn()
            MockHttpSession session = result.getRequest().getSession()

            and: "入力画面2で項目全てに入力して「次へ」ボタンをクリックする"
            mockMvc.perform(TestHelper.postForm("/inquiry/input/02?move=next", inquiryInput02Form_001)
                    .session(session))
                    .andExpect(status().isFound())
                    .andExpect(redirectedUrlPattern("**/inquiry/input/03"))

            and: "入力画面3で項目全てに入力して「次へ」ボタンをクリックする"
            mockMvc.perform(TestHelper.postForm("/inquiry/input/03?move=next", inquiryInput03Form_001)
                    .session(session))
                    .andExpect(status().isFound())
                    .andExpect(redirectedUrlPattern("**/inquiry/confirm"))

            then: "確認画面に入力画面1~3で入力したデータが表示される"
            mockMvc.perform(get("/inquiry/confirm").session(session))
                    .andExpect(status().isOk())
                    .andExpect(html("#name").text(inquiryInput01Form_001.lastname + " " + inquiryInput01Form_001.firstname))
                    .andExpect(html("#kana").text(inquiryInput01Form_001.lastkana + " " + inquiryInput01Form_001.firstkana))
                    .andExpect(html("#sex").text(vh.getText(SexValues, inquiryInput01Form_001.sex)))
                    .andExpect(html("#age").text(inquiryInput01Form_001.age + " 歳"))
                    .andExpect(html("#job").text(vh.getText(JobValues, inquiryInput01Form_001.job)))
                    .andExpect(html("#zipcode").text("〒 " + inquiryInput02Form_001.zipcode1 + "-" + inquiryInput02Form_001.zipcode2))
                    .andExpect(html("#address").text(inquiryInput02Form_001.address))
                    .andExpect(html("#tel").text(inquiryInput02Form_001.tel1 + "-" + inquiryInput02Form_001.tel2 + "-" + inquiryInput02Form_001.tel3))
                    .andExpect(html("#email").text(inquiryInput02Form_001.email))
                    .andExpect(html("#type1").text(vh.getText(Type1Values, inquiryInput03Form_001.type1)))
                    .andExpect(html("#type2").text([Type2Values.ESTIMATE.text
                                                    , Type2Values.CATALOGUE.text
                                                    , Type2Values.OTHER.text].stream().collect(Collectors.joining("、"))))
                    .andExpect(html("#inquiry").text(inquiryInput03Form_001.inquiry))
                    .andExpect(html("#survey > ul > li").count(8))
                    .andExpect(html("#survey > ul > li:nth-of-type(1)").text(
                    soh.selectItemList("survey").stream()
                            .filter({ SurveyOptions surveyOptions -> StringUtils.equals(surveyOptions.itemValue, "1") })
                            .map { SurveyOptions surveyOptions -> surveyOptions.itemName }
                            .findFirst().get()))

            and: "確認画面で「送信」ボタンをクリックする"
            mockMvc.perform(post("/inquiry/confirm/send").contentType(MediaType.APPLICATION_FORM_URLENCODED)
                    .session(session))
                    .andExpect(status().isFound())
                    .andExpect(redirectedUrlPattern("**/inquiry/complete/"))

            then: "DBとメールを確認する"
            def rows = sql.rows("SELECT * FROM INQUIRY_DATA")
            rows.size() == 1
            rows[0]["lastname"] == "12345678901234567890"
            rows[0]["firstname"] == "12345678901234567890"
            rows[0]["lastkana"] == "あいうえおかきくけこさしすせそたちつてと"
            rows[0]["firstkana"] == "なにぬねのはひふへほまみむめもあいうえお"
            rows[0]["sex"] == "2"
            rows[0]["age"] == 999
            rows[0]["job"] == "3"
            rows[0]["zipcode1"] == "102"
            rows[0]["zipcode2"] == "0072"
            rows[0]["address"] == "東京都千代田区飯田橋1-1"
            rows[0]["tel1"] == "03"
            rows[0]["tel2"] == "1234"
            rows[0]["tel3"] == "5678"
            rows[0]["email"] == "taro.tanaka@sample.co.jp"
            rows[0]["type1"] == "1"
            rows[0]["type2"] == "1,2,3"
            rows[0]["inquiry"].asciiStream.text == inquiryInput03Form_001.inquiry
            rows[0]["survey"] == "1,2,3,4,5,6,7,8"

            mailServerResource.messagesCount == 1
            MimeMessage message = mailServerResource.firstMessage
            message.subject == "問い合わせフォームからお問い合わせがありました"
            message.content == new ClassPathResource("ksbysample/webapp/bootnpmgeb/web/inquiry/inquirymail-body_001.txt").inputStream.text
        }

        def "入力画面1~3で必須項目だけに入力した場合のテスト"() {
            when: "入力画面1で必須項目だけに入力して「次へ」ボタンをクリックする"
            inquiryInput01Form_001.job = StringUtils.EMPTY
            MvcResult result = mockMvc.perform(
                    TestHelper.postForm("/inquiry/input/01?move=next", inquiryInput01Form_001))
                    .andExpect(status().isFound())
                    .andExpect(redirectedUrlPattern("**/inquiry/input/02"))
                    .andReturn()
            MockHttpSession session = result.getRequest().getSession()

            and: "入力画面2で必須項目だけに入力して「次へ」ボタンをクリックする"
            inquiryInput02Form_001.tel1 = StringUtils.EMPTY
            inquiryInput02Form_001.tel2 = StringUtils.EMPTY
            inquiryInput02Form_001.tel3 = StringUtils.EMPTY
            mockMvc.perform(TestHelper.postForm("/inquiry/input/02?move=next", inquiryInput02Form_001)
                    .session(session))
                    .andExpect(status().isFound())
                    .andExpect(redirectedUrlPattern("**/inquiry/input/03"))

            and: "入力画面3で必須項目だけに入力して「次へ」ボタンをクリックする"
            inquiryInput03Form_001.survey = []
            mockMvc.perform(TestHelper.postForm("/inquiry/input/03?move=next", inquiryInput03Form_001)
                    .session(session))
                    .andExpect(status().isFound())
                    .andExpect(redirectedUrlPattern("**/inquiry/confirm"))

            then: "確認画面に入力画面1~3で入力したデータが表示される"
            mockMvc.perform(get("/inquiry/confirm").session(session))
                    .andExpect(status().isOk())
                    .andExpect(html("#name").text(inquiryInput01Form_001.lastname + " " + inquiryInput01Form_001.firstname))
                    .andExpect(html("#kana").text(inquiryInput01Form_001.lastkana + " " + inquiryInput01Form_001.firstkana))
                    .andExpect(html("#sex").text(vh.getText(SexValues, inquiryInput01Form_001.sex)))
                    .andExpect(html("#age").text(inquiryInput01Form_001.age + " 歳"))
                    .andExpect(html("#job").text(StringUtils.EMPTY))
                    .andExpect(html("#zipcode").text("〒 " + inquiryInput02Form_001.zipcode1 + "-" + inquiryInput02Form_001.zipcode2))
                    .andExpect(html("#address").text(inquiryInput02Form_001.address))
                    .andExpect(html("#tel").text(StringUtils.EMPTY))
                    .andExpect(html("#email").text(inquiryInput02Form_001.email))
                    .andExpect(html("#type1").text(vh.getText(Type1Values, inquiryInput03Form_001.type1)))
                    .andExpect(html("#type2").text([Type2Values.ESTIMATE.text
                                                    , Type2Values.CATALOGUE.text
                                                    , Type2Values.OTHER.text].stream().collect(Collectors.joining("、"))))
                    .andExpect(html("#inquiry").text(inquiryInput03Form_001.inquiry))
                    .andExpect(html("#survey > ul > li").count(0))

            and: "確認画面で「送信」ボタンをクリックする"
            mockMvc.perform(post("/inquiry/confirm/send").contentType(MediaType.APPLICATION_FORM_URLENCODED)
                    .session(session))
                    .andExpect(status().isFound())
                    .andExpect(redirectedUrlPattern("**/inquiry/complete/"))

            then: "DBとメールを確認する"
            def rows = sql.rows("SELECT * FROM INQUIRY_DATA")
            rows.size() == 1
            rows[0]["lastname"] == "12345678901234567890"
            rows[0]["firstname"] == "12345678901234567890"
            rows[0]["lastkana"] == "あいうえおかきくけこさしすせそたちつてと"
            rows[0]["firstkana"] == "なにぬねのはひふへほまみむめもあいうえお"
            rows[0]["sex"] == "2"
            rows[0]["age"] == 999
            rows[0]["job"] == StringUtils.EMPTY
            rows[0]["zipcode1"] == "102"
            rows[0]["zipcode2"] == "0072"
            rows[0]["address"] == "東京都千代田区飯田橋1-1"
            rows[0]["tel1"] == StringUtils.EMPTY
            rows[0]["tel2"] == StringUtils.EMPTY
            rows[0]["tel3"] == StringUtils.EMPTY
            rows[0]["email"] == "taro.tanaka@sample.co.jp"
            rows[0]["type1"] == "1"
            rows[0]["type2"] == "1,2,3"
            rows[0]["inquiry"].asciiStream.text == inquiryInput03Form_001.inquiry
            rows[0]["survey"] == StringUtils.EMPTY

            mailServerResource.messagesCount == 1
            MimeMessage message = mailServerResource.firstMessage
            message.subject == "問い合わせフォームからお問い合わせがありました"
            message.content == new ClassPathResource("ksbysample/webapp/bootnpmgeb/web/inquiry/inquirymail-body_002.txt").inputStream.text
        }

    }

}

テストを実行すると1件エラーになりますね。。。

f:id:ksby:20180707060150p:plain

調べたところ、以下の原因でした。

  • INQUIRY_DATA テーブルの survey に NOT NULL が指定されていました。必須ではないので不要でしたね。。。
  • InquiryInput03Form クラスで type2 は private List<String> type2 = new ArrayList<>(); と実装しているのに、survey は private List<String> survey とだけ書いていて = new ArrayList<>(); を書いていなかったため、値がないと null となる場合があり、SessionData2ConfirmFormTypeMap クラス等で inquiryInput03Form.getSurvey().stream() を呼び出しているところでエラーになっていました。

まず src/main/resources/db/migration/V1__init.sql の以下の点を変更します。

CREATE TABLE INQUIRY_DATA
(
    id INT AUTO_INCREMENT PRIMARY KEY,
    lastname VARCHAR(20) NOT NULL,
    firstname VARCHAR(20) NOT NULL,
    lastkana VARCHAR(20) NOT NULL,
    firstkana VARCHAR(20) NOT NULL,
    sex VARCHAR(1) NOT NULL,
    age INT NOT NULL,
    job VARCHAR(1),
    zipcode1 VARCHAR(3) NOT NULL,
    zipcode2 VARCHAR(4) NOT NULL,
    address VARCHAR(256) NOT NULL,
    tel1 VARCHAR(5),
    tel2 VARCHAR(4),
    tel3 VARCHAR(4),
    email VARCHAR(256),
    type1 VARCHAR(1) NOT NULL,
    type2 VARCHAR(1),
    inquiry TEXT NOT NULL,
    survey VARCHAR(1),
    update_date DATETIME NOT NULL
);
  • survey から NOT NULL を削除します。

次に src/main/java/ksbysample/webapp/bootnpmgeb/web/inquiry/form/InquiryInput03Form.java の以下の点を変更します。

    private List<String> survey = new ArrayList<>();
  • private List<String> surveyprivate List<String> survey = new ArrayList<>(); に変更します。

再度テストを実行すると、今度は全て成功しました。

f:id:ksby:20180707065305p:plain

メモ書き+次回は。。。

久しぶりにメール送信のテストを書いたら時間かかってしまいました。また Groovy が相変わらず便利ですね。今回以下の点が良かったです。

  • Clob に格納されたテキストの比較をどうするかで、以前は org.springframework.util.StreamUtils#copyToString を利用しましたが、Groovy だと InputStream を返すメソッドに .text を付けるだけで文字列が取得できました。
  • Getter, Setter メソッドを Groovy から呼び出す場合、get, set を取り除いて先頭を小文字にしたプロパティとして呼び出せるので、記述が少しだけ完結にできます。例えば inquiryData.getInquiry().getAsciiStream().getText()inquiryData.inquiry.asciiStream.text のように書けます。

次回は、

  • CSRF対策が入っているはずなのですが、MockMvc#perform を呼び出す時に .with(csrf()) を付けていなくてもテストがなぜか成功していることにほとんど書き終えてから気づきました。気になったので調査してみます。
  • InquiryInputControllerTest クラスに未実装のテストがあるので実装します。

履歴

2018/07/07
初版発行。