かんがるーさんの日記

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

Spring Boot + npm + Geb で入力フォームを作ってテストする ( その31 )( テスト対象のブラウザに Headless Chrome と HtmlUnit を追加する+Chrome, Firefox, HtmlUnit で連続テストする gradle タスクを作成する )

概要

記事一覧はこちらです。

Spring Boot + npm + Geb で入力フォームを作ってテストする ( その30 )( Geb を 2.0 へバージョンアップする+Firefox headless モードを使用する ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • テスト対象のブラウザに Headless ChromeHtmlUnit を追加します。
    • ChromeFirefox (どちらも Headless モード)と HtmlUnit で連続してテストする gradle のタスクを作成します。

参照したサイト・書籍

  1. ヘッドレス Chrome ことはじめ
    https://developers.google.com/web/updates/2017/04/headless-chrome?hl=ja

  2. Headless Chrome and Selenium on Windows?
    https://stackoverflow.com/questions/43880619/headless-chrome-and-selenium-on-windows

  3. SeleniumHQ/htmlunit-driver
    https://github.com/SeleniumHQ/htmlunit-driver

  4. Where to find 64 bit version of chromedriver.exe for Selenium WebDriver?
    https://stackoverflow.com/questions/23081507/where-to-find-64-bit-version-of-chromedriver-exe-for-selenium-webdriver

  5. 【入門】Geb+SpockではじめるWebテストクロスブラウザテスト編~ / Setting up and running of the cross-browser test
    http://yfj2.hateblo.jp/entry/2014/11/09/004011

  6. ChromeDriver - WebDriver for Chrome
    https://sites.google.com/a/chromium.org/chromedriver/

  7. geb/geb-example-gradle/build.gradle
    https://github.com/geb/geb-example-gradle/blob/master/build.gradle

目次

  1. Chrome と HtmlUnit 用の Selenium WebDriver をインストールする
  2. ChromeDriver をダウンロードして配置する
  3. GebConfig.groovy を修正する
  4. build.gradle にタスクを定義する
  5. 動作確認

手順

ChromeHtmlUnit 用の Selenium WebDriver をインストールする

build.gradle を以下のように変更します。

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

    // for Geb + Spock
    testCompile("org.gebish:geb-spock:2.0")
    testCompile("org.seleniumhq.selenium:selenium-chrome-driver:${seleniumVersion}")
    testCompile("org.seleniumhq.selenium:selenium-firefox-driver:${seleniumVersion}")
    testCompile("org.seleniumhq.selenium:htmlunit-driver:2.27")
    testCompile("org.seleniumhq.selenium:selenium-support:${seleniumVersion}")
    testCompile("org.seleniumhq.selenium:selenium-api:${seleniumVersion}")
    testCompile("org.seleniumhq.selenium:selenium-remote-driver:${seleniumVersion}")
}
  • 以下の行を追加します。
    • testCompile("org.seleniumhq.selenium:selenium-chrome-driver:${seleniumVersion}")
    • testCompile("org.seleniumhq.selenium:htmlunit-driver:2.27")
  • HtmlUnit 用の WebDriver として selenium-htmlunit-driver がありますが、https://mvnrepository.com/selenium-htmlunit-driver を見てみると最新バージョンが 2016/2 にリリースされた 2.52.0 で、3.x 系がリリースされていませんでした。さすがに 3.6.0 と組み合わせるのは無理だろうと思い Web で探してみると、WebDriver compatible を謳っている htmlunit-driver が見つかり、こちらは 2017/6 に 2.27 がリリースされていましたので、htmlunit-driver をインストールすることにします。

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

ChromeDriver をダウンロードして配置する

Chrome を操作するためには Firefox の geckodriver と同じように ChromeDriver が必要です。ダウンロードして配置します。

ChromeDrive の Downloads ページ の Latest Release の右側に表示されている「ChromeDriver 2.33」リンクをクリックして次のページへ進んだ後、「chromedriver_win32.zip」リンクをクリックして chromedriver_win32.zip をダウンロードします。64bit版は提供されていませんが、chromedriver_win32.zip のファイルで 64bit の Windows + Chrome でも問題なく動作します。

ダウンロード後、C:\chromedriver\2.33 ディレクトリを作成した後、chromedriver_win32.zip を解凍して出来る chromedriver.exe をその下に配置します。

f:id:ksby:20171029004812p:plain

GebConfig.groovy を修正する

src/test/resources/GebConfig.groovy を以下のように変更します。

import org.openqa.selenium.chrome.ChromeDriver
import org.openqa.selenium.chrome.ChromeOptions
import org.openqa.selenium.firefox.FirefoxDriver
import org.openqa.selenium.firefox.FirefoxOptions
import org.openqa.selenium.htmlunit.HtmlUnitDriver

System.setProperty("webdriver.gecko.driver", "C:/geckodriver/0.19.0/geckodriver.bat")
System.setProperty("webdriver.chrome.driver", "C:/chromedriver/2.33/chromedriver.exe")
driver = {
    FirefoxOptions firefoxOptions = new FirefoxOptions()
    firefoxOptions.setHeadless(true)
    new FirefoxDriver(firefoxOptions)
}
baseUrl = "http://localhost:8080"
waiting {
    timeout = 15
}

environments {

    chrome {
        driver = {
            ChromeOptions chromeOptions = new ChromeOptions()
            chromeOptions.setHeadless(true)
            new ChromeDriver(chromeOptions)
        }
    }

    firefox {
        driver = {
            FirefoxOptions firefoxOptions = new FirefoxOptions()
            firefoxOptions.setHeadless(true)
            new FirefoxDriver(firefoxOptions)
        }
    }

    htmlunit {
        new HtmlUnitDriver(true)
    }

}
  • System.setProperty("webdriver.gecko.driver", "C:/geckodriver/0.19.0/geckodriver.bat")driver の中に書いていたのを外に出します。
  • System.setProperty("webdriver.chrome.driver", "C:/chromedriver/2.33/chromedriver.exe") を追加します。
  • environments { ... } の定義を追加し、この中に chrome, firefox, htmlunit の driver の定義を記述します。

build.gradle にタスクを定義する

build.gradle に各ブラウザ毎のテスト用タスクと、全てのブラウザのテスト用タスクを連続実行するためのタスクを追加します。このタスクは geb/geb-example-gradle/build.gradle からコピーしました。

..........

test {
    // test タスクの jvmArgs は tasks.withType(Test) { ... } で定義している
    exclude "geb/**"
}

// for Geb + Spock Integration Test
def drivers = ["chrome", "firefox", "htmlunit"]
drivers.each { driver ->
    task "${driver}Test"(type: Test) {
        // 前回実行時以降に何も更新されていなくても必ず実行する
        outputs.upToDateWhen { false }
        systemProperty "geb.env", driver
        exclude "ksbysample/**"
    }
}
task gebTest {
    dependsOn drivers.collect { tasks["${it}Test"] }
    enabled = false
}

tasks.withType(Test) {
    jvmArgs = ['-Dspring.profiles.active=unittest']
}

// for Doma-Gen
task domaGen {
    ..........
  • test タスクから jvmArgs = ['-Dspring.profiles.active=unittest'] を削除します。
  • 記述していた task gebTest(type: Test) { ... } を削除します。
  • // for Geb + Spock Integration Test から下の部分を記述します。リストからタスクを動的に作成したり、動的に作成されたタスクを依存関係に指定する方法があるとは、初めて知りました。。。
  • tasks.withType(Test) { ... } を追加し、Test タイプのタスク共通の設定はこちらに記述します。今回は jvmArgs のみ記述します。

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

更新すると Gradle projects の other の下に chromeTest, firefoxTest, gebTest, htmlunitTest の4つのタスクが表示されます。

f:id:ksby:20171029085625p:plain

動作確認

最初に clean タスク → Rebuild Project → build タスクを実行して BUILD SUCCESSFUL のメッセージが出力されることを確認します。追加した chromeTest, firefoxTest, gebTest, htmlunitTest のタスクが実行されていないことも確認できます。

f:id:ksby:20171029090212p:plain

chromeTest タスクを実行します。ブラウザの画面が表示されず、テストは成功します。また ChromeDriver のメッセージが出力されており、Chrome がテストに使われていることが分かります。

f:id:ksby:20171029090534p:plain

firefoxTest タスクを実行します。こちらもブラウザの画面は表示されず、テストは成功します。

f:id:ksby:20171029090804p:plain

htmlunitTest タスクを実行します。HtmlUnit はブラウザではないので当然画面は表示されず、テストは成功します。

f:id:ksby:20171029091445p:plain

最後に gebTest タスクを実行します。chromeTest, firefoxTest, htmlunitTest の3つのタスクが実行されてそれぞれ成功し、gebTest タスクは build.gradle 内で enabled = false を記述しているので SKIP されることが確認できます。

f:id:ksby:20171029091741p:plain

ちなみに src/test/groovy/geb/gebspec/SimpleTestSpec.groovy をテストが失敗するように変更してから、

class SimpleTestSpec extends GebSpec {

    def "動作確認用"() {
        ..........

        then: "「お名前(漢字)」の必須チェックエラーのメッセージが表示される"
        $("#form-group-name .js-errmsg").displayed == true
//        $("#form-group-name .js-errmsg").text() == "お名前(漢字)を入力してください"
        $("#form-group-name .js-errmsg").text() == "エラーです"
    }

}

gebTest タスクを実行すると chromeTest タスクでエラーが出て先に進みません。

f:id:ksby:20171029092304p:plain

履歴

2017/10/29
初版発行。

Spring Boot + npm + Geb で入力フォームを作ってテストする ( その30 )( Geb を 2.0 へバージョンアップする+Firefox headless モードを使用する )

概要

記事一覧はこちらです。

Spring Boot + npm + Geb で入力フォームを作ってテストする ( その29 )( Geb をインストールする ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • Geb の 2.0 がリリースされたのでバージョンアップします。
    • geckodriver.exe の場所を jvm のオプション -Dwebdriver.gecko.driver= で指定するのではなく System.setProperty("webdriver.gecko.driver", "...") で指定するよう変更します。
    • Firefox に headless モードがあるようなので、headless モードを使用するよう設定を変更します。

参照したサイト・書籍

  1. Geb - Very Groovy Browser Automation
    http://www.gebish.org/

  2. Headless mode
    https://developer.mozilla.org/en-US/Firefox/Headless_mode

  3. Disable HttpClient logging
    https://stackoverflow.com/questions/4915414/disable-httpclient-logging

  4. How do I disable Firefox logging in Selenium using Geckodriver?
    https://stackoverflow.com/questions/41387794/how-do-i-disable-firefox-logging-in-selenium-using-geckodriver

目次

  1. Geb を 2.0-rc-1 → 2.0 へバージョンアップする
  2. geckodriver.exe の場所は System.setProperty("webdriver.gecko.driver", "...") で指定する
  3. Firefox headless モードを使用する
    1. GebConfig.groovy を変更する
    2. 動作確認
    3. 大量に出力される DEBUG ログ [Forwarding ... on session ... to remote] DEBUG org.apache.http を抑制する
    4. Marionette DEBUG のログを抑制する

手順

Geb を 2.0-rc-1 → 2.0 へバージョンアップする

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

dependencies {
    ..........
    def seleniumVersion = "3.6.0"

    ..........

    // for Geb + Spock
    testCompile("org.gebish:geb-spock:2.0")
    testCompile("org.seleniumhq.selenium:selenium-firefox-driver:${seleniumVersion}")
    testCompile("org.seleniumhq.selenium:selenium-support:${seleniumVersion}")
    testCompile("org.seleniumhq.selenium:selenium-api:${seleniumVersion}")
    testCompile("org.seleniumhq.selenium:selenium-remote-driver:${seleniumVersion}")
}
  • org.gebish:geb-spock のバージョン番号を 2.0-rc-12.0 に変更します。
  • def seleniumVersion = "3.6.0" を追加し、selenium のモジュールのバージョン番号を ${seleniumVersion} で指定するよう変更します。

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

Tomcat を起動した後、test, gebTest タスクを実行して問題ないことを確認します(画面キャプチャは省略します)。この後もテストを繰り返すので Tomcat は起動したままにします。

geckodriver.exe の場所は System.setProperty("webdriver.gecko.driver", "...") で指定する

前回の記事では geckordriver.exe を build.gradle の gebTest タスクと、IntelliJ IDEA の「Run」-「Edit Configurations...」メニューで開いた先の JUnit の設定のところに記述しましたが、GebConfig.groovy に System.setProperty("webdriver.gecko.driver", "...") で定義すれば利用可能になることが分かったので、設定を変更します。

src/test/resources/GebConfig.groovy を以下のように変更します。

import org.openqa.selenium.firefox.FirefoxDriver

driver = {
    System.setProperty("webdriver.gecko.driver", "C:/geckodriver/0.19.0/geckodriver.exe")
    new FirefoxDriver()
}
baseUrl = "http://localhost:8080"
waiting {
    timeout = 15
}

build.gradle の gebTest を以下のように変更します。

test {
    jvmArgs = ['-Dspring.profiles.active=unittest']
    exclude "geb/**"
}

task gebTest(type: Test) {
    jvmArgs = ['-Dspring.profiles.active=unittest']
    exclude "ksbysample/**"
}
  • jvmArgs の指定を '-Dwebdriver.gecko.driver=C:/geckodriver/0.19.0/geckodriver.exe' を削除して test タスクと同じにします。

IntelliJ IDEA のメインメニューから「Run」-「Edit Configurations...」を選択して「Run/Debug Configurations」ダイアログを表示した後、「JUnit」の「VM Options」から -Dwebdriver.gecko.driver=C:/geckodriver/0.19.0/geckodriver.exe を削除します。

f:id:ksby:20171028080101p:plain

動作確認します。

最初に clean タスク → Rebuild Project → build タスクを実行して BUILD SUCCESSFUL のメッセージが出力されることを確認します(画面キャプチャは省略します)。

次に gebTest タスクを実行すると Firefox が起動し、テストは成功します。

f:id:ksby:20171028080601p:plain

最後に src/test/groovy/geb/gebspec/SimpleTestSpec.groovy を開き、左側のアイコンから Run '動作確認用()' を選択します。

f:id:ksby:20171028081146p:plain

こちらも Firefox が起動し、テストは成功します。

f:id:ksby:20171028082241p:plain

Firefox headless モードを使用する

Firefox に headless モードが実装されているらしいので、そちらを使うように設定してみます。

GebConfig.groovy を変更する

src/test/resources/GebConfig.groovy を以下のように変更します。

import org.openqa.selenium.firefox.FirefoxDriver
import org.openqa.selenium.firefox.FirefoxOptions

driver = {
    System.setProperty("webdriver.gecko.driver", "C:/geckodriver/0.19.0/geckodriver.exe")
    FirefoxOptions firefoxOptions = new FirefoxOptions()
    firefoxOptions.setHeadless(true)
    new FirefoxDriver(firefoxOptions)
}
baseUrl = "http://localhost:8080"
waiting {
    timeout = 15
}
  • 以下の2行を追加します。
    • FirefoxOptions firefoxOptions = new FirefoxOptions()
    • firefoxOptions.setHeadless(true)
  • new FirefoxDriver() の引数に firefoxOptions を追加します。

動作確認

動作確認します。

最初に clean タスク → Rebuild Project → build タスクを実行して BUILD SUCCESSFUL のメッセージが出力されることを確認します(画面キャプチャは省略します)。

次に gebTest タスクを実行してみると Firefox の画面は表示されず、テストは成功します。ただし DEBUG ログが大量に出ます。。。 これは出来れば出ないようにしたいですね。

f:id:ksby:20171028124025p:plain

DEBUG メッセージに対応する前に、うまく動いているのかちょっと分かりづらいので、テストの内容を少し変更してみます。

src/test/groovy/geb/page/inquiry/InquiryInput01Page.groovy を以下のように変更します。

package geb.page.inquiry

import geb.Page

class InquiryInput01Page extends Page {

    static url = "/inquiry/input/01"
    static at = { title == "入力フォーム - 入力画面1" }
    static content = {
        btnNext { $(".js-btn-next") }
    }

}
  • static content { ... } を追加し、その中に btnNext { $(".js-btn-next") } を記述します。

src/test/groovy/geb/gebspec/SimpleTestSpec.groovy を以下のように変更し、最後でテストが失敗するようにします。

package geb.gebspec

import geb.page.inquiry.InquiryInput01Page
import geb.spock.GebSpec

class SimpleTestSpec extends GebSpec {

    def "動作確認用"() {
        given: "入力画面1へアクセスする"
        to InquiryInput01Page
        waitFor { at InquiryInput01Page }
        $("#form-group-name .js-errmsg").displayed == false

        when: "何も入力せずに「次へ」ボタンをクリックする"
        btnNext.click(InquiryInput01Page)

        then: "「お名前(漢字)」の必須チェックエラーのメッセージが表示される"
        $("#form-group-name .js-errmsg").displayed == true
//        $("#form-group-name .js-errmsg").text() == "お名前(漢字)を入力してください"
        $("#form-group-name .js-errmsg").text() == "エラーです"
    }

}

gebTest タスクを実行してみると今度はエラーになりました。Spock はエラーメッセージが分かりやすくていいですね。

f:id:ksby:20171028125357p:plain

src/test/groovy/geb/gebspec/SimpleTestSpec.groovy を以下のように変更し、今度は成功するようにします。

package geb.gebspec

import geb.page.inquiry.InquiryInput01Page
import geb.spock.GebSpec

class SimpleTestSpec extends GebSpec {

    def "動作確認用"() {
        given: "入力画面1へアクセスする"
        to InquiryInput01Page
        waitFor { at InquiryInput01Page }
        $("#form-group-name .js-errmsg").displayed == false

        when: "何も入力せずに「次へ」ボタンをクリックする"
        btnNext.click(InquiryInput01Page)

        then: "「お名前(漢字)」の必須チェックエラーのメッセージが表示される"
        $("#form-group-name .js-errmsg").displayed == true
        $("#form-group-name .js-errmsg").text() == "お名前(漢字)を入力してください"
//        $("#form-group-name .js-errmsg").text() == "エラーです"
    }

}

gebTest タスクを実行してみると今度は成功します。headless モードでも問題なく動いているようです。

f:id:ksby:20171028130043p:plain

大量に出力される DEBUG ログ [Forwarding ... on session ... to remote] DEBUG org.apache.http を抑制する

何か情報がないか Google で検索すると、stackoverflow の以下の QA を見つけました。

これを読むと logback.xml があれば抑制できるようです。

src/test/resources の下に logback.xml を新規作成し、以下の内容を記述します。色々試してみたところ <logger .../> の定義がなくても logback.xml があれば DEBUG ログを抑制できるようです。

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <!--
        Geb+Spock で Firefox を headless モードで動かした時に大量に以下の DEBUG ログ
        が出力されるのを抑制するために作成しているファイルである。ファイルがあればログを抑
        制できるので、特に logger の定義は記述していない。

        [Forwarding ... on session ... to remote] DEBUG org.apache.http
     -->
</configuration>

gebTest タスクを実行してみると [Forwarding ... on session ... to remote] DEBUG org.apache.http の DEBUG メッセージは出力されました。ただし、まだ Marionette DEBUG というログが出力されています。

f:id:ksby:20171028133928p:plain

Marionette DEBUG のログを抑制する

Google で検索していろいろ見たのですが、stackoverflow の以下の QA を見て解決しました。

geckodriver.exe を配置した C:\geckodriver\0.19.0 の下に geckodriver.bat を新規作成し、以下の内容を記述します。

@echo off
C:\geckodriver\0.19.0\geckodriver.exe --log info %* > NUL 2>&1

src/test/groovy/GebConfig.groovy を以下のように変更します。

import org.openqa.selenium.firefox.FirefoxDriver
import org.openqa.selenium.firefox.FirefoxOptions

driver = {
    System.setProperty("webdriver.gecko.driver", "C:/geckodriver/0.19.0/geckodriver.bat")
    FirefoxOptions firefoxOptions = new FirefoxOptions()
    firefoxOptions.setHeadless(true)
    new FirefoxDriver(firefoxOptions)
}
baseUrl = "http://localhost:8080"
waiting {
    timeout = 15
}
  • System.setProperty("webdriver.gecko.driver", "...") の第2引数に設定していたプログラム名を C:/geckodriver/0.19.0/geckodriver.exeC:/geckodriver/0.19.0/geckodriver.bat に変更します。

gebTest タスクを実行すると Marionette DEBUG のログは全く出力されなくなりました。

f:id:ksby:20171028143338p:plain

履歴

2017/10/28
初版発行。

Spring Boot + npm + Geb で入力フォームを作ってテストする ( その29 )( Geb をインストールする )

概要

記事一覧はこちらです。

Spring Boot + npm + Geb で入力フォームを作ってテストする ( その28 )( Spring Boot を 1.5.4 → 1.5.7 へ、error-prone を 2.0.15 → 2.1.1 へバージョンアップする ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • Geb + Spock でテストを作成するために、まずは Geb をインストールして使えるようにします。

参照したサイト・書籍

  1. Geb - Very Groovy Browser Automation
    http://www.gebish.org/

  2. Web画面自動テストフレームワークGeb」の紹介
    http://lab.astamuse.co.jp/entry/geb_test_01

  3. Geb with spock
    https://www.slideshare.net/MonikaGurram/geb-with-spock-24710923

  4. mozilla/geckodriver
    https://github.com/mozilla/geckodriver

  5. Gradle – How to exclude some tests
    https://www.mkyong.com/gradle/gradle-how-to-exclude-some-tests/

  6. Getting Started With Gradle: Integration Testing
    https://www.petrikainulainen.net/programming/gradle/getting-started-with-gradle-integration-testing/

  7. Spring Boot and Gradle: Separating tests
    https://moelholm.com/2016/10/22/spring-boot-separating-tests/

目次

  1. 方針
  2. Geb に必要なモジュールをインストールする
  3. GebConfig.groovy を作成する
  4. テスト用のパッケージを作成する
  5. 入力画面1の Page Objects を作成する
  6. 簡単なテストを作成する
  7. geckodriver をインストールする
  8. gebTest タスクを作成する
  9. 動作確認
  10. IntelliJ IDEA 上からテストを実行できるようにする

手順

方針

  • テストは Geb + Spock の組み合わせで作成します。geb-core ではなく geb-spock を指定してインストールします。
  • ブラウザは Firefox を利用します。今回テストを作成している時にインストールしている Firefox のバージョンは 56.0.2 (64ビット) です。
  • Geb は現時点で最新の 2.0-rc-1、selenium は 3.6.0 をインストールします。
  • Geb のテストは src/test/groovy/geb の下に作成します。src の下に main, test とは別にディレクトリは作成しません。
src/test/groovy
├ geb
│ ├ gebspec
│ └ page
└ ksbysample
   └ webapp
      └ bootnpmgeb
  • gradle の test タスクでは Geb のテストは実行されないようにし、Geb のテストだけを実行する gebTest タスクを作成します。

Geb に必要なモジュールをインストールする

Geb + Spock でテストを作成するために必要なモジュールをインストールします。1.5. Installation & usage を参考に、build.gradle の dependencies に以下の3行を追加します。

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

    // for Geb + Spock
    testCompile("org.gebish:geb-spock:2.0-rc-1")
    testCompile("org.seleniumhq.selenium:selenium-firefox-driver:3.6.0")
    testCompile("org.seleniumhq.selenium:selenium-support:3.6.0")
}

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

GebConfig.groovy を作成する

共通の設定を記述するための GebConfig.groovy を src/test/resources の下に作成します。ファイルを作成した後、7. Configuration を参考に以下のように記述します。

driver = "firefox"
baseUrl = "http://localhost:8080"
waiting {
    timeout = 15
}

テスト用のパッケージを作成する

src/test/groovy の下に geb パッケージを作成し、geb の下に gebspec, page パッケージを作成します。

f:id:ksby:20171027005122p:plain

入力画面1の Page Objects を作成する

src/test/groovy/geb/page の下に inquiry パッケージを作成した後、inquiry の下に InquiryInput01Page.groovy を作成して以下の内容を記述します。

package geb.page.inquiry

import geb.Page

class InquiryInput01Page extends Page {

    static url = "/inquiry/input/01"
    static at = { title == "入力フォーム - 入力画面1" }

}

簡単なテストを作成する

動作確認のために簡単なテストを作成します。src/test/groovy/geb/gebspec の下に SimpleTestSpec.groovy を作成して以下の内容を記述します。

package geb.gebspec

import geb.page.inquiry.InquiryInput01Page
import geb.spock.GebSpec

class SimpleTestSpec extends GebSpec {

    def "動作確認用"() {
        expect: "入力画面1へアクセスする"
        to InquiryInput01Page
        waitFor { at InquiryInput01Page }
    }

}

geckodriver をインストールする

Web画面自動テストフレームワーク「Geb」の紹介 の記事によると Firefox の 47.0以降 から geckodriver を gradle でのダウンロードとは別にインストールする必要があるとのことですので、ダウンロード&インストールします。

https://github.com/mozilla/geckodriver/releases を見ると最新バージョンは v0.19.0 です。下にある geckodriver-v0.19.0-win64.zip のリンクをクリックしてダウンロードします。

ダウンロードした geckodriver-v0.19.0-win64.zip を解凍すると geckodriver.exe が出来ますので、C:\geckodriver\0.19.0 の下に配置します。

f:id:ksby:20171027011630p:plain

gebTest タスクを作成する

build.gradle に gebTest タスクを追加します。また test タスクで ksbysample パッケージ配下のテストが実行されないようにします。

test {
    jvmArgs = ['-Dspring.profiles.active=unittest']
    exclude "geb/**"
}

task gebTest(type: Test) {
    jvmArgs = [
            '-Dspring.profiles.active=unittest'
            , '-Dwebdriver.gecko.driver=C:/geckodriver/0.19.0/geckodriver.exe'
    ]
    exclude "ksbysample/**"
}
  • test タスクに exclude "geb/**" を追加します。
  • gebTest タスクを追加します。

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

動作確認

Tomcat を起動した後、gebTest タスクを実行してみます。。。が、java.lang.ClassNotFoundException: org.openqa.selenium.MutableCapabilities というエラーメッセージが出て失敗しました。

f:id:ksby:20171027062307p:plain

モジュールを見てみると selenium 関連で4つ表示されているのですが、selenium-apiselenium-remote-driver が 2.53.1 と古いバージョンになっています。全て 3.6.0 になるよう調整します。

f:id:ksby:20171027062538p:plain

build.gradle を以下のように変更します。

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

    // for Geb + Spock
    testCompile("org.gebish:geb-spock:2.0-rc-1")
    testCompile("org.seleniumhq.selenium:selenium-firefox-driver:3.6.0")
    testCompile("org.seleniumhq.selenium:selenium-support:3.6.0")
    testCompile("org.seleniumhq.selenium:selenium-api:3.6.0")
    testCompile("org.seleniumhq.selenium:selenium-remote-driver:3.6.0")
}
  • 以下の2行を追加します。
    • testCompile("org.seleniumhq.selenium:selenium-api:3.6.0")
    • testCompile("org.seleniumhq.selenium:selenium-remote-driver:3.6.0")

変更後、Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新すると、今度は全て 3.6.0 になりました。

f:id:ksby:20171027063123p:plain

再度 getTest タスクを実行すると、今度は Firefox が起動して入力画面1にアクセスし、テストが成功しました。

f:id:ksby:20171027063323p:plain

Tomcat を停止した後、clean タスク → Rebuild Project → build タスクを実行すると BUILD SUCCESSFUL のメッセージが出力されました。test タスク中に Firefox は起動しなかったので、Geb で作成したテストは実行されませんでした。

f:id:ksby:20171027063830p:plain

IntelliJ IDEA 上からテストを実行できるようにする

今の設定だけでは IntelliJ IDEA のエディタの左側のメニューから Run 動作確認用() を選択してもテストを実行できません。

f:id:ksby:20171027065029p:plain

Caused by: java.lang.IllegalStateException: The path to the driver executable must be set by the webdriver.gecko.driver system property; for more information, see https://github.com/mozilla/geckodriver. The latest version can be downloaded from https://github.com/mozilla/geckodriver/releases というエラーメッセージが出力されます。

f:id:ksby:20171027065225p:plain

IntelliJ IDEA のメインメニューから「Run」-「Edit Configurations...」を選択して「Run/Debug Configurations」ダイアログを表示した後、「JUnit」の「VM Options」に -Dwebdriver.gecko.driver=C:/geckodriver/0.19.0/geckodriver.exe を追加します。

f:id:ksby:20171027065606p:plain

再度 Run 動作確認用() を選択してテストを実行すると Firefox が起動して入力画面1にアクセスし、テストが成功しました。

f:id:ksby:20171027065818p:plain

履歴

2017/10/27
初版発行。

Spring Boot + npm + Geb で入力フォームを作ってテストする ( 番外編 )( ModelMapper メモ書き )

概要

記事一覧はこちらです。

最近 POJO 間のデータコピーに ModelMapper を使用していますが、使っていて気づいた点やつまずいた点をメモしておきます。

尚、ModelMapper の利用には rozidan/modelmapper-spring-boot-starter を利用しています(利用しなくても ModelMapper を使うのは難しくありませんが少し便利になる感じです)。

参照したサイト・書籍

目次

  1. String --> int 変換は何も定義しなくてもやってくれる
  2. skip に指定したフィールドがプリミティブ型だと実行時に NullPointerException が発生する
  3. 部分的に特別な処理をしたい時には setPreConverter で定義する
  4. コピー元からコピー先へ通常のフィールドコピーが行われるフィールドに preConverter で特別な処理をする場合には、一緒に skip も指定して通常のフィールドコピーが行われないようにする
  5. sourceType, destinationType に指定するクラスが同じで変換ルールが異なる TypeMap を作りたい場合には name を設定する
  6. rozidan/modelmapper-spring-boot-starter を使わずに ModelMapper を使用するには?
  7. 最後に

本文

String --> int 変換は何も定義しなくてもやってくれる

String 型のフィールドを int 型のフィールドにコピーする場合、何か変換処理を入れないといけないのかと思っていましたが、何もしなくても変換してくれます。

@RunWith(SpringRunner.class)
@SpringBootTest
public class ModelMapperTest {

    @Autowired
    private ModelMapper modelMapper;

    @Data
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    static class SrcData {
        private String age;
    }

    @Data
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    static class DstData {
        private int age;
    }

    @Test
    public void string2IntTest() throws Exception {
        SrcData srcData = SrcData.builder()
                .age("25")
                .build();
        // String 型 --> int には何も用意しなくても自動で変換してくれる
        DstData dstData = modelMapper.map(srcData, DstData.class);
        assertThat(dstData.getAge()).isEqualTo(Integer.parseInt(srcData.getAge()));
    }

}

上のテストを実行すると成功します。

f:id:ksby:20171021145514p:plain

skip に指定したフィールドがプリミティブ型だと実行時に NullPointerException が発生する

テストクラス内でプリミティブ型のフィールドの setter を skip に指定しても正常に動作するのですが(assertThat まで実行されます)、

@RunWith(SpringRunner.class)
@SpringBootTest
public class ModelMapperTest {

    @Autowired
    private ModelMapper modelMapper;

    @Data
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    static class SrcData {
        private String age;
    }

    @Data
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    static class DstData {
        // コピー先をプリミティブ型(int)にする
        private int age;
    }

    @Component
    static class SrcData2DstDataTypeMap extends TypeMapConfigurer<SrcData, DstData> {
        @Override
        public void configure(TypeMap<SrcData, DstData> typeMap) {
            // プリミティブ型(int) の setter を skip に指定する
            typeMap.addMappings(mapping -> mapping.skip(DstData::setAge));
        }
    }

    @Test
    public void string2IntTest() throws Exception {
        SrcData srcData = SrcData.builder()
                .age("25")
                .build();
        DstData dstData = modelMapper.map(srcData, DstData.class);
        assertThat(dstData.getAge()).isEqualTo(Integer.parseInt(srcData.getAge()));
    }

}

f:id:ksby:20171021153543p:plain

以下のような Controller クラスを作成して、

@Controller
@RequestMapping("/sample")
public class SampleController {

    private final ModelMapper modelMapper;

    public SampleController(ModelMapper modelMapper) {
        this.modelMapper = modelMapper;
    }

    @Data
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    static class SrcData {
        private String age;
    }

    @Data
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    static class DstData {
        // コピー先をプリミティブ型(int)にする
        private int age;
    }

    @Component
    static class SrcData2DstDataTypeMap extends TypeMapConfigurer<SrcData, DstData> {
        @Override
        public void configure(TypeMap<SrcData, DstData> typeMap) {
            // プリミティブ型(int) の setter を skip に指定する
            typeMap.addMappings(mapping -> mapping.skip(DstData::setAge));
        }
    }

    @RequestMapping
    @ResponseBody
    public String index() {
        SrcData srcData = SrcData.builder()
                .age("25")
                .build();
        DstData dstData = modelMapper.map(srcData, DstData.class);
        return "sample";
    }

}

Tomcat を起動しようとすると NullPointerException が発生して起動しません。

f:id:ksby:20171021155408p:plain

これは skip に指定するフィールドがプリミティブ型であることが原因です。参照型に変更するとこのエラーは出ません。

    @Data
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    static class DstData {
        // コピー先をプリミティブ型(int)にする --> 参照型に変更する
        // private int age;
        private Integer age;
    }

int --> Integer に変更すると Tomcat は起動します。

f:id:ksby:20171021160945p:plain

部分的に特別な処理をしたい時には setPreConverter で定義する

コピー先にだけ存在するフィールドに、コピー元のフィールドの値を見て値をセットしたい場合、setPreConverter で定義します。

以下の処理を行うテストクラスを書いてみます。

  • DstData.name に SrcData.firstName + " " + SrcData.lastName をセットします。
  • DstData.name が空でない場合には DstData.isEmptyNameFlg に true を、そうでない場合には false をセットします。
  • setPreConverter で定義された処理の後に通常のフィールドの値のコピーは行われるので、firstName, lastName はそのまま SrcData --> DstData へコピーされます。
@RunWith(SpringRunner.class)
@SpringBootTest
public class ModelMapperTest {

    @Autowired
    private ModelMapper modelMapper;

    @Data
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    static class SrcData {
        private String firstName;
        private String lastName;
    }

    @Data
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    static class DstData {
        private String firstName;
        private String lastName;
        private String name;
        // コピー時に name に値がセットされれば true, 空なら false をセットする
        private boolean isEmptyNameFlg;
    }

    @Component
    static class GlobalConfiguration extends ConfigurationConfigurer {
        @Override
        public void configure(Configuration configuration) {
            // デフォルトの MatchingStrategies.STANDARD だと DstData.name のコピー元のフィールドとして
            // SrcData.firstName, SrcData.lastName の2つがあると判断されるため、MatchingStrategies.STRICT
            // に変更する
            configuration.setMatchingStrategy(MatchingStrategies.STRICT);
        }
    }

    @Component
    static class SrcData2DstDataTypeMap extends TypeMapConfigurer<SrcData, DstData> {
        @Override
        public void configure(TypeMap<SrcData, DstData> typeMap) {
            // setPreConverter に処理を定義して、コピー元に対応するフィールドがない
            // name, isEmptyNameFlg に値をセットする
            typeMap.setPreConverter(context -> {
                SrcData srcData = context.getSource();
                DstData dstData = context.getDestination();
                dstData.setName(String.format("%s %s"
                        , srcData.getFirstName(), srcData.getLastName()));
                dstData.setEmptyNameFlg(StringUtils.isNotEmpty(dstData.getName()));
                return context.getDestination();
            });
        }
    }

    @Test
    public void string2IntTest() throws Exception {
        SrcData srcData = SrcData.builder()
                .firstName("taro")
                .lastName("tanaka")
                .build();
        DstData dstData = modelMapper.map(srcData, DstData.class);
        assertThat(dstData.getFirstName()).isEqualTo(srcData.getFirstName());
        assertThat(dstData.getLastName()).isEqualTo(srcData.getLastName());
        assertThat(dstData.getName()).isEqualTo(
                String.format("%s %s", srcData.getFirstName(), srcData.getLastName()));
        assertThat(dstData.isEmptyNameFlg()).isTrue();
    }

}

上のテストを実行すると成功します。

f:id:ksby:20171021190710p:plain

コピー元からコピー先へ通常のフィールドコピーが行われるフィールドに preConverter で特別な処理をする場合には、一緒に skip も指定して通常のフィールドコピーが行われないようにする

コピー元とコピー先に同じフィールドが存在するがコピー時に setPreConverter に処理を定義して特殊な処理を行う場合、処理対象のフィールドが通常のフィールドのコピーの対象にならないよう skip で指定する必要があります。

@RunWith(SpringRunner.class)
@SpringBootTest
public class ModelMapperTest {

    @Autowired
    private ModelMapper modelMapper;

    @Data
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    static class SrcData {
        private String firstName;
        private String lastName;
    }

    @Data
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    static class DstData {
        private String firstName;
        private String lastName;
    }

    @Component
    static class SrcData2DstDataTypeMap extends TypeMapConfigurer<SrcData, DstData> {
        @Override
        public void configure(TypeMap<SrcData, DstData> typeMap) {
            typeMap.setPreConverter(context -> {
                SrcData srcData = context.getSource();
                DstData dstData = context.getDestination();
                // firstName はコピー時に先頭に文字数を追加する
                dstData.setFirstName(srcData.getFirstName().length() + ":" + srcData.getFirstName());
                return context.getDestination();
            });
            // setPreConverter で firstName のコピー処理をしているので、通常のフィールドコピーの対象外にする
            typeMap.addMappings(mapping -> mapping.skip(DstData::setFirstName));
        }
    }

    @Test
    public void string2IntTest() throws Exception {
        SrcData srcData = SrcData.builder()
                .firstName("taro")
                .lastName("tanaka")
                .build();
        DstData dstData = modelMapper.map(srcData, DstData.class);
        // firstName には "taro" の文字数が追加されて "4:taro" がセットされているはず
        assertThat(dstData.getFirstName()).isEqualTo("4:" + srcData.getFirstName());
        assertThat(dstData.getLastName()).isEqualTo(srcData.getLastName());
    }

}

上のテストを実行すると成功します。

f:id:ksby:20171021200144p:plain

ちなみに skip をコメントアウトすると

    @Component
    static class SrcData2DstDataTypeMap extends TypeMapConfigurer<SrcData, DstData> {
        @Override
        public void configure(TypeMap<SrcData, DstData> typeMap) {
            typeMap.setPreConverter(context -> {
                SrcData srcData = context.getSource();
                DstData dstData = context.getDestination();
                // firstName はコピー時に先頭に文字数を追加する
                dstData.setFirstName(srcData.getFirstName().length() + ":" + srcData.getFirstName());
                return context.getDestination();
            });
            // setPreConverter で firstName のコピー処理をしているので、通常のフィールドコピーの対象外にする
//            typeMap.addMappings(mapping -> mapping.skip(DstData::setFirstName));
        }
    }

フィールドのコピー処理が行われるのでテストは失敗します。

f:id:ksby:20171021200449p:plain

sourceType, destinationType に指定するクラスが同じで変換ルールが異なる TypeMap を作りたい場合には name を設定する

SrcData --> DstData へデータをコピーするのは同じですが、内部の処理が異なる TypeMap を2つ定義したいような場合には、TypeMap に名前を付けるようにします。そして ModelMapper#map メソッドを呼ぶ時に、第3引数に使用する TypeMap 名を指定します。

@RunWith(SpringRunner.class)
@SpringBootTest
public class ModelMapperTest {

    @Autowired
    private ModelMapper modelMapper;

    @Data
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    static class SrcData {
        private String firstName;
        private String lastName;
    }

    @Data
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    static class DstData {
        private String firstName;
        private String lastName;
    }

    /**
     * DstData.firstName に SrcData.firstName + " " + SrcData.lastName をセットする
     * DstData.lastName には何もコピーしない
     */
    @Component
    static class CopyFirstNameOnlyTypeMap extends TypeMapConfigurer<SrcData, DstData> {
        // ①外から参照できるよう public static final String の定数に TypeMap 名を定義する
        public static final String TYPEMAP_NAME = "CopyFirstNameOnlyTypeMap";

        // ②getTypeMapName メソッドをオーバーライドして、TypeMap 名を返す
        @Override
        public String getTypeMapName() {
            return TYPEMAP_NAME;
        }

        @Override
        public void configure(TypeMap<SrcData, DstData> typeMap) {
            typeMap.setPreConverter(context -> {
                SrcData srcData = context.getSource();
                DstData dstData = context.getDestination();
                dstData.setFirstName(String.format("%s %s", srcData.getFirstName(), srcData.getLastName()));
                return context.getDestination();
            });
            typeMap.addMappings(mapping -> mapping.skip(DstData::setFirstName));
            typeMap.addMappings(mapping -> mapping.skip(DstData::setLastName));
        }
    }

    /**
     * DstData.firstName には何もコピーしない
     * DstData.lastName に SrcData.firstName + " " + SrcData.lastName をセットする
     */
    @Component
    static class CopyLastNameOnlyTypeMap extends TypeMapConfigurer<SrcData, DstData> {
        public static final String TYPEMAP_NAME = "CopyLastNameOnlyTypeMap";

        @Override
        public String getTypeMapName() {
            return TYPEMAP_NAME;
        }

        @Override
        public void configure(TypeMap<SrcData, DstData> typeMap) {
            typeMap.setPreConverter(context -> {
                SrcData srcData = context.getSource();
                DstData dstData = context.getDestination();
                dstData.setLastName(String.format("%s %s", srcData.getFirstName(), srcData.getLastName()));
                return context.getDestination();
            });
            typeMap.addMappings(mapping -> mapping.skip(DstData::setFirstName));
            typeMap.addMappings(mapping -> mapping.skip(DstData::setLastName));
        }
    }

    @Test
    public void string2IntTest() throws Exception {
        SrcData srcData = SrcData.builder()
                .firstName("taro")
                .lastName("tanaka")
                .build();
        // ③map メソッドの第3引数に使用する TypeMap 名を指定する
        // ※TypeMap Bean をインジェクションして、getTypeMapName メソッドを呼んでもよい
        DstData dstDataFirst = modelMapper.map(srcData, DstData.class, CopyFirstNameOnlyTypeMap.TYPEMAP_NAME);
        assertThat(dstDataFirst.getFirstName()).isEqualTo(srcData.getFirstName() + " " + srcData.getLastName());
        assertThat(dstDataFirst.getLastName()).isNull();

        DstData dstDataLast = modelMapper.map(srcData, DstData.class, CopyLastNameOnlyTypeMap.TYPEMAP_NAME);
        assertThat(dstDataLast.getFirstName()).isNull();
        assertThat(dstDataLast.getLastName()).isEqualTo(srcData.getFirstName() + " " + srcData.getLastName());
    }

}

上のテストを実行すると成功します。

f:id:ksby:20171021212245p:plain

rozidan/modelmapper-spring-boot-starter を使わずに ModelMapper を使用するには?

Java Config で modelMapper Bean を定義し、

@Configuration
public class ModelMapperConfig {

    @Bean
    public ModelMapper modelMapper() {
        ModelMapper modelMapper = new ModelMapper();
        modelMapper.getConfiguration().setMatchingStrategy(MatchingStrategies.STRICT);
        return modelMapper;
    }

}

@Component アノテーションを付加したクラスのコンストラクタで ModelMapper#createTypeMap メソッドを呼び出して TypeMap を生成・登録します。あとは使用したい箇所で ModelMapper#map メソッドを呼び出します。

@RunWith(SpringRunner.class)
@SpringBootTest
public class ModelMapperTest {

    @Autowired
    private ModelMapper modelMapper;

    @Data
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    static class SrcData {
        private String firstName;
        private String lastName;
    }

    @Data
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    static class DstData {
        private String firstName;
        private String lastName;
    }

    @Component
    static class SrcData2DstDataTypeMap {
        private SrcData2DstDataTypeMap(ModelMapper modelMapper) {
            TypeMap<SrcData, DstData> typeMap
                    = modelMapper.createTypeMap(SrcData.class, DstData.class);

            // setPreConverter に処理を定義して、コピー元に対応するフィールドがない
            // name, isEmptyNameFlg に値をセットする
            typeMap.setPreConverter(context -> {
                SrcData srcData = context.getSource();
                DstData dstData = context.getDestination();
                // firstName はコピー時に先頭に文字数を追加する
                dstData.setFirstName(srcData.getFirstName().length() + ":" + srcData.getFirstName());
                return context.getDestination();
            });
            // setPreConverter で firstName のコピー処理をしているので、通常のフィールドコピーの対象外にする
            typeMap.addMappings(mapping -> mapping.skip(DstData::setFirstName));
        }
    }

    @Test
    public void string2IntTest() throws Exception {
        SrcData srcData = SrcData.builder()
                .firstName("taro")
                .lastName("tanaka")
                .build();
        DstData dstData = modelMapper.map(srcData, DstData.class);
        // firstName には "taro" の文字数が追加されて "4:taro" がセットされているはず
        assertThat(dstData.getFirstName()).isEqualTo("4:" + srcData.getFirstName());
        assertThat(dstData.getLastName()).isEqualTo(srcData.getLastName());
    }

}

rozidan/modelmapper-spring-boot-starter を入れると、modelMapper Bean を自動生成してくれるのと、ModelMapper#createTypeMap の呼び出しを自動でやってくれます。

最後に

  • ModelMapper はいろいろ機能がありますが、modelMapper.map(...) を呼び出してコピーするか、単純なコピーでない場合には TypeMap を作成すればとりあえず使えるという印象です。
  • Matching strategy は個人的には STRICT にしておいた方が問題がない気がするのですが、まだそんなに使い込んでいる訳ではないので何とも言えないですね。
  • POJO をコピーするのに Dozer もありますが、XML で定義しないといけないし、ModelMapper でもそんなに困らない気がしていて、個人的には ModelMapper でいいんじゃないかなと思っています。

履歴

2017/10/22
初版発行。

Spring Boot + npm + Geb で入力フォームを作ってテストする ( その28 )( Spring Boot を 1.5.4 → 1.5.7 へ、error-prone を 2.0.15 → 2.1.1 へバージョンアップする )

概要

記事一覧はこちらです。

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

  • 今回の手順で確認できるのは以下の内容です。
    • Spring Boot のバージョンを 1.5.4 → 1.5.7 へバージョンアップします。
    • 他にもバージョンアップしているライブラリを更新します。
    • 今回はこれまでエラーが出てバージョンアップできなかった error-prone もバージョンアップします。やっと lombok を使用している環境で error-prone の最新バージョンを使う方法が理解できました。

参照したサイト・書籍

目次

  1. Spring Boot を 1.5.4 → 1.5.7 へバージョンアップする(他のライブラリもバージョンアップする)
  2. error-prone を 2.0.15 → 2.1.1 へバージョンアップする

手順

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

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

group 'ksbysample'
version '1.0.0-RELEASE'

buildscript {
    ext {
        springBootVersion = '1.5.7.RELEASE'
    }
    repositories {
        mavenCentral()
        maven { url "https://plugins.gradle.org/m2/" }
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
        // for Error Prone ( http://errorprone.info/ )
        classpath("net.ltgt.gradle:gradle-errorprone-plugin:0.0.10")
    }
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'idea'
apply plugin: 'org.springframework.boot'
apply plugin: 'groovy'
apply plugin: 'net.ltgt.errorprone'
apply plugin: 'checkstyle'
apply plugin: 'findbugs'
apply plugin: 'pmd'

sourceCompatibility = 1.8
targetCompatibility = 1.8

task wrapper(type: Wrapper) {
    gradleVersion = '3.5'
}

[compileJava, compileTestGroovy, compileTestJava]*.options*.compilerArgs = ['-Xlint:all,-options,-processing,-path']
compileJava.options.compilerArgs += ['-Xep:RemoveUnusedImports:WARN']

// for Doma 2
// JavaクラスとSQLファイルの出力先ディレクトリを同じにする
processResources.destinationDir = compileJava.destinationDir
// コンパイルより前にSQLファイルを出力先ディレクトリにコピーするために依存関係を逆転する
compileJava.dependsOn processResources

idea {
    module {
        inheritOutputDirs = false
        outputDir = file("$buildDir/classes/main/")
    }
}

configurations {
    // for Doma 2
    domaGenRuntime
}

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

findbugs {
    toolVersion = '3.0.1'
    sourceSets = [project.sourceSets.main]
    ignoreFailures = true
    effort = "max"
    excludeFilter = file("${rootProject.projectDir}/config/findbugs/findbugs-exclude.xml")
}

tasks.withType(FindBugs) {
    // Gradle 3.3以降 + FindBugs Gradle Plugin を組み合わせると、"The following errors occurred during analysis:"
    // の後に "Cannot open codebase filesystem:..." というメッセージが大量に出力されるので、以下の doFirst { ... }
    // のコードを入れることで出力されないようにする
    doFirst {
        def fc = classes
        if (fc == null) {
            return
        }
        fc.exclude '**/*.properties'
        fc.exclude '**/*.sql'
        fc.exclude '**/*.xml'
        fc.exclude '**/META-INF/**'
        fc.exclude '**/static/**'
        fc.exclude '**/templates/**'
        classes = files(fc.files)
    }
    reports {
        xml.enabled = false
        html.enabled = true
    }
}

pmd {
    toolVersion = "5.8.1"
    sourceSets = [project.sourceSets.main]
    ignoreFailures = true
    consoleOutput = true
    ruleSetFiles = rootProject.files("/config/pmd/pmd-project-rulesets.xml")
    ruleSets = []
}

repositories {
    mavenCentral()
}

dependencyManagement {
    imports {
        // BOM は https://repo.spring.io/release/io/spring/platform/platform-bom/Brussels-SR5/
        // の下を見ること
        mavenBom("io.spring.platform:platform-bom:Brussels-SR5") {
            bomProperty 'guava.version', '22.0'
            bomProperty 'thymeleaf.version', '3.0.8.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'
        }
    }
}

bootRepackage {
    mainClass = 'ksbysample.webapp.lending.Application'
}

dependencies {
    def spockVersion = "1.1-groovy-2.4"
    def domaVersion = "2.16.1"
    def lombokVersion = "1.16.18"
    def errorproneVersion = "2.0.15"
    def powermockVersion = "1.7.3"

    // dependency-management-plugin によりバージョン番号が自動で設定されるもの
    // Appendix A. Dependency versions ( http://docs.spring.io/platform/docs/current/reference/htmlsingle/#appendix-dependency-versions ) 参照
    compile("org.springframework.boot:spring-boot-starter-web")
    compile("org.springframework.boot:spring-boot-starter-thymeleaf") {
        exclude group: "org.codehaus.groovy", module: "groovy"
    }
    compile("org.springframework.boot:spring-boot-starter-data-jpa")
    compile("org.springframework.boot:spring-boot-starter-freemarker")
    compile("org.springframework.boot:spring-boot-starter-mail")
    compile("org.springframework.boot:spring-boot-starter-security")
    compile("org.springframework.boot:spring-boot-devtools")
    compile("org.springframework.session:spring-session")
    compile("com.google.guava:guava")
    compile("org.apache.commons:commons-lang3")
    compile("org.codehaus.janino:janino")
    testCompile("org.springframework.boot:spring-boot-starter-test")
    testCompile("org.springframework.security:spring-security-test")
    testCompile("org.yaml:snakeyaml")
    testCompile("org.mockito:mockito-core")

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

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

    // for Doma
    compile("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
    testCompile("org.powermock:powermock-module-junit4:${powermockVersion}")
    testCompile("org.powermock:powermock-api-mockito:${powermockVersion}")
}
  • version '1.5.4-RELEASE'version '1.0.0-RELEASE' に変更します。ここは Webアプリケーションのバージョンとして 1.0.0 と書くべきところを、Spring Boot のバージョンである 1.5.4 を書いてしまったという勘違いでした。
  • buildscript の以下の点を変更します。
    • springBootVersion = '1.5.4.RELEASE'springBootVersion = '1.5.7.RELEASE' に変更します。
  • checkstyle の以下の点を変更します。
    • toolVersion = '8.0'toolVersion = '8.3' に変更します。
  • dependencyManagement の以下の点を変更します。
    • mavenBom("io.spring.platform:platform-bom:Brussels-SR3")mavenBom("io.spring.platform:platform-bom:Brussels-SR5") に変更します。
    • bomProperty 'thymeleaf.version', '3.0.7.RELEASE'bomProperty 'thymeleaf.version', '3.0.8.RELEASE' に変更します。
    • bomProperty 'thymeleaf-extras-java8time.version', '3.0.0.RELEASE'bomProperty 'thymeleaf-extras-java8time.version', '3.0.1.RELEASE' に変更します。
  • dependencies の以下の点を変更します。
    • def powermockVersion = "1.7.0"def powermockVersion = "1.7.3" に変更します。
    • compile("com.integralblue:log4jdbc-spring-boot-starter:1.0.1")compile("com.integralblue:log4jdbc-spring-boot-starter:1.0.2") に変更します。
    • testCompile("org.dbunit:dbunit:2.5.3")testCompile("org.dbunit:dbunit:2.5.4") に変更します。

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

clean タスク実行 → Rebuild Project 実行 → build タスクを実行してみますが、checkstyleProperty 'maxLineLength' in module LeftCurly does not exist, please check the documentation というエラーが出ました。

f:id:ksby:20171017010855p:plain

checkstyle のホームページ を見ると、確かに LeftCurly には maxLineLength は存在しませんでした。1行あたりの文字数のチェックは LineLength で行うようです(google_checks.xml にも定義が書いてありました)。

config/checkstyle/google_checks.xml の以下の点を変更します。

        <module name="NeedBraces"/>
        <module name="LeftCurly"/>
        <module name="RightCurly">
  • LeftCurly から <property name="maxLineLength" value="100"/> を削除し、<module name="LeftCurly"/> に変更します。

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

f:id:ksby:20171017012140p:plain

Project Tool Window で src/test を選択した後、コンテキストメニューを表示して「Run 'All Tests' with Coverage」を選択し、テストが全て成功することも確認できました。

f:id:ksby:20171017012928p:plain

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

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

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

buildscript {
    ext {
        springBootVersion = '1.5.7.RELEASE'
    }
    repositories {
        mavenCentral()
        maven { url "https://plugins.gradle.org/m2/" }
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
        // for Error Prone ( http://errorprone.info/ )
        classpath("net.ltgt.gradle:gradle-errorprone-plugin:0.0.13")
    }
}

..........

dependencies {
    def spockVersion = "1.1-groovy-2.4"
    def domaVersion = "2.16.1"
    def lombokVersion = "1.16.18"
    def errorproneVersion = "2.1.1"
    def powermockVersion = "1.7.3"

    ..........

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

    ..........
}
  • buildscript 内で classpath("net.ltgt.gradle:gradle-errorprone-plugin:0.0.10")classpath("net.ltgt.gradle:gradle-errorprone-plugin:0.0.13") に変更します。
  • dependencies 内で def errorproneVersion = "2.0.15"def errorproneVersion = "2.1.1" に変更します。

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

clean タスク実行 → Rebuild Project 実行 → build タスクを実行すると前とは違いますが、相変わらずエラーが出てコンパイルが通りません。An unhandled exception was thrown by the Error Prone static analysis plugin. というメッセージが出て @Data アノテーションで引っかかっているようです。

f:id:ksby:20171018014059p:plain

もう少しエラーが発生している詳細な原因が知りたいので、コマンドプロンプトから gradlew --stacktrace build コマンドを実行してみます。

f:id:ksby:20171018014639p:plain

結果として上の画像のメッセージが出力されますが、ここで注目するのは com.google.errorprone.bugpatterns のどのクラスで引っかかっているのか、です。今回は NestedInstanceOfConditions で引っかかっています。

NestedInstanceOfConditionsGoogle で検索すると error-prone の Issue で Lombok breaks NestedInstanceOfConditions and InstanceOfAndCastMatchWrongType がヒットします。この中で -Xep:NestedInstanceOfConditions:OFF オプションを追加して無効にするよう記述がありました。

build.gradle を修正し、-Xep:NestedInstanceOfConditions:OFF オプションを追加します。

[compileJava, compileTestGroovy, compileTestJava]*.options*.compilerArgs = ['-Xlint:all,-options,-processing,-path']
compileJava.options.compilerArgs += [
        '-Xep:RemoveUnusedImports:WARN'
        , '-Xep:NestedInstanceOfConditions:OFF'
]

再び clean タスク実行 → Rebuild Project 実行 → build タスクを実行しますが、やっぱりさっきと同じエラーが出ます。

gradlew --stacktrace build コマンドを実行すると今度は InstanceOfAndCastMatchWrongType で引っかかっています。

f:id:ksby:20171018015631p:plain

build.gradle を修正して , '-Xep:InstanceOfAndCastMatchWrongType:OFF' を追加します。

[compileJava, compileTestGroovy, compileTestJava]*.options*.compilerArgs = ['-Xlint:all,-options,-processing,-path']
compileJava.options.compilerArgs += [
        '-Xep:RemoveUnusedImports:WARN'
        , '-Xep:NestedInstanceOfConditions:OFF'
        , '-Xep:InstanceOfAndCastMatchWrongType:OFF'
]

再び clean タスク実行 → Rebuild Project 実行 → build タスクを実行すると、今度は "BUILD SUCCESSFUL" が出力されました。

f:id:ksby:20171018020245p:plain

Project Tool Window で src/test を選択した後、コンテキストメニューを表示して「Run 'All Tests' with Coverage」を選択し、テストが全て成功することも確認しておきます。

f:id:ksby:20171018020618p:plain

履歴

2017/10/18
初版発行。

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

概要

記事一覧はこちらです。

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

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

参照したサイト・書籍

目次

  1. InquiryInputController クラスのテストを変更する
  2. build タスクを実行したら PMD でエラーが出たので pmd-project-rulesets.xml と InquiryInput02FormValidator.java を修正する
  3. 次回は。。。

手順

InquiryInputController クラスのテストを変更する

src/test/groovy/ksbysample/webapp/bootnpmgeb/web/inquiry/InquiryInputControllerTest.groovy の以下の点を変更します。groovy だと SnakeYAML を使わなくてもテストデータを分かりやすく書けることに今更ながら気付いたので、今回は SnakeYAML は使用せずテストデータはテストクラス内に直接記述します。

@RunWith(Enclosed)
class InquiryInputControllerTest {

    ..........

    @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))
                    .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)
                    .session(session))
                    .andExpect(status().isFound())
                    .andExpect(redirectedUrlPattern("**/inquiry/input/01"))

            and: "入力画面1で「次へ」ボタンをクリックする"
            mockMvc.perform(TestHelper.postForm("/inquiry/input/01?move=next", inquiryInput01Form_001)
                    .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))
                    .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)
                    .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))
                        .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))
                        .andExpect(status().isOk())
            })
            assertThat(thrown.cause).isInstanceOf(IllegalArgumentException)
        }

    }

}
  • 入力画面2のテスト クラスを追加します。

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

f:id:ksby:20171015190914p:plain

build タスクを実行したら PMD でエラーが出たので pmd-project-rulesets.xml と InquiryInput02FormValidator.java を修正する

clean タスク実行 → Rebuild Project 実行 → build タスクを実行すると、PMD で何件かメッセージが出ました。

f:id:ksby:20171015194130p:plain

build/reports/pmd/main.xml を見ると以下の3種類の Rule が原因でした。

ただし https://pmd.github.io/pmd-5.8.1/pmd-java/rules/java/codesize.html#ModifiedCyclomaticComplexity を見ると Deprecated の表示がありますね。前に PMD の設定ファイルを作成していた時には気づきませんでした。Code Size の Rules にいくつか Deprecated が表示されていますが pmd-project-rulesets.xml で除外していなかったので、pmd-project-rulesets.xml を修正して除外するようにします。

config/pmd/pmd-project-rulesets.xml の以下の点を変更します。

    <rule ref="rulesets/java/codesize.xml">
        <exclude name="CyclomaticComplexity"/>
        <exclude name="StdCyclomaticComplexity"/>
        <exclude name="ModifiedCyclomaticComplexity"/>
        <exclude name="TooManyMethods"/>
    </rule>
  • 以下の行を追加します。
    • <exclude name="CyclomaticComplexity"/>
    • <exclude name="StdCyclomaticComplexity"/>
    • <exclude name="ModifiedCyclomaticComplexity"/>

また https://pmd.github.io/pmd-5.8.1/pmd-java/rules/java/basic.html#CollapsibleIfStatements の方は src/main/java/ksbysample.webapp.bootnpmgeb/web/inquiry/form/InquiryInput02FormValidator.java の以下の箇所でエラーが出ていたのですが(if 文をネストさせずに && でつなげて1行で書くようにという内容でした)、

            // メールアドレスが入力されていたら入力チェックする
            if (StringUtils.isNotEmpty(email)) {
                if (!EmailValidator.validate(email)) {
                    errors.reject("InquiryInput02Form.email.Invalid");
                }
            }

ここは今の実装のままにしたいので、@SuppressWarnings を指定してメッセージが出ないようにします。

    @SuppressWarnings({"PMD.CollapsibleIfStatements", "PMD.ConfusingTernary"})
    private void checkTelAndEmail(boolean ignoreCheckRequired, String tel1, String tel2, String tel3, String email
            , Errors errors) {
  • @SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.ConfusingTernary"})@SuppressWarnings({"PMD.CollapsibleIfStatements", "PMD.ConfusingTernary"}) に変更します。

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

f:id:ksby:20171015200528p:plain

次回は。。。

入力画面3の実装に進む前に1~2件書きたいことができたのでそれを書きます(たぶん)。

その後に入力画面1のテストを Geb で作成します。元々今回の記事を書いている理由が Geb のテストの書き方を知りたかったからなんですよね。。。(もうその27ですが)

その後で入力画面3の実装に進みます。

履歴

2017/10/15
初版発行。

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

概要

記事一覧はこちらです。

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

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

参照したサイト・書籍

  1. Verify Static Method Call using PowerMockito 1.6
    https://stackoverflow.com/questions/34323909/verify-static-method-call-using-powermockito-1-6

目次

  1. InquiryInput02Form クラスのテストを作成する
  2. InquiryInput02FormNotEmptyRule クラスのテストを作成する
  3. EmailValidator クラスのテストを作成する
  4. InquiryInput02FormValidator クラスのテストを作成する

手順

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

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

f:id:ksby:20171014144544p:plain

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

package ksbysample.webapp.bootnpmgeb.web.inquiry.form

import spock.lang.Specification
import spock.lang.Unroll

import javax.validation.ConstraintViolation
import javax.validation.Validation

class InquiryInput02FormTest extends Specification {

    def validator
    def inquiryInput02Form

    def setup() {
        validator = Validation.buildDefaultValidatorFactory().getValidator()
        inquiryInput02Form = new InquiryInput02Form(
                zipcode1: "102"
                , zipcode2: "0072"
                , address: "東京都千代田区飯田橋1-1"
                , tel1: "03"
                , tel2: "1234"
                , tel3: "5678"
                , email: "taro.tanaka@sample.co.jp")
    }

    def "placeholder で表示している例の Bean Validation のテスト"() {
        when:
        Set<ConstraintViolation<InquiryInput01Form>> constraintViolations =
                validator.validate(inquiryInput02Form)

        then:
        constraintViolations.size() == 0
    }

    @Unroll
    def "zipcode1 の Bean Validation のテスト(#zipcode1 --> #size)"() {
        setup:
        inquiryInput02Form.zipcode1 = zipcode1
        Set<ConstraintViolation<InquiryInput02Form>> constraintViolations =
                validator.validate(inquiryInput02Form)

        expect:
        constraintViolations.size() == size

        where:
        zipcode1 || size
        ""       || 0
        "1"      || 0
        "1" * 3  || 0
        "1" * 4  || 1
    }

    @Unroll
    def "zipcode2 の Bean Validation のテスト(#zipcode2 --> #size)"() {
        setup:
        inquiryInput02Form.zipcode2 = zipcode2
        Set<ConstraintViolation<InquiryInput02Form>> constraintViolations =
                validator.validate(inquiryInput02Form)

        expect:
        constraintViolations.size() == size

        where:
        zipcode2 || size
        ""       || 0
        "1"      || 0
        "1" * 4  || 0
        "1" * 5  || 1
    }

    @Unroll
    def "address の Bean Validation のテスト(#address --> #size)"() {
        setup:
        inquiryInput02Form.address = address
        Set<ConstraintViolation<InquiryInput02Form>> constraintViolations =
                validator.validate(inquiryInput02Form)

        expect:
        constraintViolations.size() == size

        where:
        address   || size
        ""        || 0
        "a"       || 0
        "a" * 256 || 0
        "a" * 257 || 1
    }

    @Unroll
    def "tel1 の Bean Validation のテスト(#tel1 --> #size)"() {
        setup:
        inquiryInput02Form.tel1 = tel1
        Set<ConstraintViolation<InquiryInput02Form>> constraintViolations =
                validator.validate(inquiryInput02Form)

        expect:
        constraintViolations.size() == size

        where:
        tel1    || size
        ""      || 0
        "1"     || 0
        "1" * 5 || 0
        "1" * 6 || 1
    }

    @Unroll
    def "tel2 の Bean Validation のテスト(#tel2 --> #size)"() {
        setup:
        inquiryInput02Form.tel2 = tel2
        Set<ConstraintViolation<InquiryInput02Form>> constraintViolations =
                validator.validate(inquiryInput02Form)

        expect:
        constraintViolations.size() == size

        where:
        tel2    || size
        ""      || 0
        "1"     || 0
        "1" * 4 || 0
        "1" * 5 || 1
    }

    @Unroll
    def "tel3 の Bean Validation のテスト(#tel3 --> #size)"() {
        setup:
        inquiryInput02Form.tel3 = tel3
        Set<ConstraintViolation<InquiryInput02Form>> constraintViolations =
                validator.validate(inquiryInput02Form)

        expect:
        constraintViolations.size() == size

        where:
        tel3    || size
        ""      || 0
        "1"     || 0
        "1" * 4 || 0
        "1" * 5 || 1
    }

    @Unroll
    def "email の Bean Validation のテスト(#email --> #size)"() {
        setup:
        inquiryInput02Form.email = email
        Set<ConstraintViolation<InquiryInput02Form>> constraintViolations =
                validator.validate(inquiryInput02Form)

        expect:
        constraintViolations.size() == size

        where:
        email     || size
        ""        || 0
        "a"       || 0
        "a" * 256 || 0
        "a" * 257 || 1
    }

}

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

f:id:ksby:20171014151810p:plain

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

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

f:id:ksby:20171014150940p:plain

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

package ksbysample.webapp.bootnpmgeb.web.inquiry.form

import spock.lang.Specification
import spock.lang.Unroll

import javax.validation.ConstraintViolation
import javax.validation.Validation

class InquiryInput02FormNotEmptyRuleTest extends Specification {

    def validator
    def inquiryInput02FormNotEmptyRule

    def setup() {
        validator = Validation.buildDefaultValidatorFactory().getValidator()
        inquiryInput02FormNotEmptyRule = new InquiryInput02FormNotEmptyRule(
                zipcode1: "102"
                , zipcode2: "0072"
                , address: "東京都千代田区飯田橋1-1"
                , tel1: "03"
                , tel2: "1234"
                , tel3: "5678"
                , email: "taro.tanaka@sample.co.jp")
    }

    def "placeholder で表示している例の Bean Validation のテスト"() {
        when:
        Set<ConstraintViolation<InquiryInput02FormNotEmptyRule>> constraintViolations =
                validator.validate(inquiryInput02FormNotEmptyRule)

        then:
        constraintViolations.size() == 0
    }

    @Unroll
    def "zipcode1 の NotEmpty のテスト(#zipcode1 --> #size)"() {
        setup:
        inquiryInput02FormNotEmptyRule.zipcode1 = zipcode1
        Set<ConstraintViolation<InquiryInput02FormNotEmptyRule>> constraintViolations =
                validator.validate(inquiryInput02FormNotEmptyRule)

        expect:
        constraintViolations.size() == size

        where:
        zipcode1 || size
        ""       || 1
        "1"      || 0
    }

    @Unroll
    def "zipcode2 の NotEmpty のテスト(#zipcode2 --> #size)"() {
        setup:
        inquiryInput02FormNotEmptyRule.zipcode2 = zipcode2
        Set<ConstraintViolation<InquiryInput02FormNotEmptyRule>> constraintViolations =
                validator.validate(inquiryInput02FormNotEmptyRule)

        expect:
        constraintViolations.size() == size

        where:
        zipcode2 || size
        ""       || 1
        "1"      || 0
    }

    @Unroll
    def "address の NotEmpty のテスト(#address --> #size)"() {
        setup:
        inquiryInput02FormNotEmptyRule.address = address
        Set<ConstraintViolation<InquiryInput02FormNotEmptyRule>> constraintViolations =
                validator.validate(inquiryInput02FormNotEmptyRule)

        expect:
        constraintViolations.size() == size

        where:
        address || size
        ""      || 1
        "a"     || 0
    }

}

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

f:id:ksby:20171014152525p:plain

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

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

f:id:ksby:20171014185311p:plain

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

package ksbysample.webapp.bootnpmgeb.util.validator

import spock.lang.Specification
import spock.lang.Unroll

class EmailValidatorTest extends Specification {

    @Unroll
    def "メールアドレスの Validation のテスト(#email --> #result)"() {
        expect:
        EmailValidator.validate(email) == result

        where:
        email                                                                   || result
        ""                                                                      || true
        "@"                                                                     || false
        "a@"                                                                    || false
        "@b"                                                                    || false
        "a@b"                                                                   || true
        "@@"                                                                    || false
        "a@@b"                                                                  || false
        "a@b@c"                                                                 || false
        // ASCII文字だけなので OK
        "taro.tanaka@sample.co.jp"                                              || true
        "1234567890@1234567890"                                                 || true
        "ABCDEFGHOJKLMNOPQRSTUVWXYZ@ABCDEFGHOJKLMNOPQRSTUVWXYZ"                 || true
        "abcdefghojklmnopqrstuvwxyz@abcdefghojklmnopqrstuvwxyz"                 || true
        "!\"#\$%&'()*+,-./:;<=>?[\\]^_`{|}~@!\"#\$%&'()*+,-./:;<=>?[\\]^_`{|}~" || true
        // スペースがあるので NG
        "taro tanaka@sample.co.jp"                                              || false
        "taro.tanaka@sample co.jp"                                              || false
        // 非ASCII文字があるので NG
        "田中太郎@sample.co.jp"                                                     || false
        "taro.tanaka@サンプル co.jp"                                                || false
    }

}

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

f:id:ksby:20171014192308p:plain

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

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

f:id:ksby:20171014153257p:plain

src/test/groovy/ksbysample/webapp/bootnpmgeb/web/inquiry/form/InquiryInput02FormValidatorTest.groovy が新規作成されるので、以下の内容を記述します。メールアドレスの入力チェックについては

package ksbysample.webapp.bootnpmgeb.web.inquiry.form

import ksbysample.common.test.helper.TestHelper
import ksbysample.webapp.bootnpmgeb.util.validator.EmailValidator
import org.junit.Before
import org.junit.Test
import org.junit.experimental.runners.Enclosed
import org.junit.runner.RunWith
import org.mockito.Mockito
import org.powermock.api.mockito.PowerMockito
import org.powermock.core.classloader.annotations.PowerMockIgnore
import org.powermock.core.classloader.annotations.PrepareForTest
import org.powermock.modules.junit4.PowerMockRunner
import org.powermock.modules.junit4.PowerMockRunnerDelegate
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.test.context.junit4.SpringRunner
import org.springframework.validation.Errors
import spock.lang.Specification
import spock.lang.Unroll

@RunWith(Enclosed)
class InquiryInput02FormValidatorTest {

    @SpringBootTest
    static class InquiryInput02FormValidator_メールアドレス以外 extends Specification {

        @Autowired
        private InquiryInput02FormValidator input02FormValidator

        Errors errors
        InquiryInput02Form inquiryInput02Form

        def setup() {
            errors = TestHelper.createErrors()
            inquiryInput02Form = new InquiryInput02Form(
                    zipcode1: "102"
                    , zipcode2: "0072"
                    , address: "東京都千代田区飯田橋1-1"
                    , tel1: "03"
                    , tel2: "1234"
                    , tel3: "5678"
                    , email: "taro.tanaka@sample.co.jp")
        }

        def "placeholder で表示している例の Bean Validation のテスト"() {
            setup:
            input02FormValidator.validate(inquiryInput02Form, errors)

            expect:
            errors.hasErrors() == false
        }

        @Unroll
        def "郵便番号の Validation のテスト(#zipcode1,#zipcode2 --> #hasErrors,#size)"() {
            setup:
            inquiryInput02Form.zipcode1 = zipcode1
            inquiryInput02Form.zipcode2 = zipcode2

            expect:
            input02FormValidator.validate(inquiryInput02Form, errors)
            errors.hasErrors() == hasErrors
            errors.getAllErrors().size() == size

            where:
            zipcode1 | zipcode2 || hasErrors | size
            ""       | ""       || false     | 0
            "999"    | ""       || true      | 1
            ""       | "9999"   || true      | 1
            "999"    | "9999"   || false     | 0
        }

        @Unroll
        def "電話番号の Validation のテスト(#tel1,#tel2,#tel3 --> #hasErrors,#size)"() {
            given:
            inquiryInput02Form.tel1 = tel1
            inquiryInput02Form.tel2 = tel2
            inquiryInput02Form.tel3 = tel3
            inquiryInput02Form.email = email

            when:
            input02FormValidator.validate(inquiryInput02Form, errors)

            then:
            errors.hasErrors() == hasErrors
            errors.getAllErrors().size() == size
            // メールアドレスのチェックは行われていないことをチェックする
            0 * EmailValidator.validate(email)

            where:
            tel1    | tel2   | tel3   | email || hasErrors | size
            ""      | ""     | ""     | ""    || true      | 1
            "03"    | ""     | ""     | ""    || true      | 1
            ""      | "1234" | ""     | ""    || true      | 1
            ""      | ""     | "5678" | ""    || true      | 1
            "03"    | "1234" | "5678" | ""    || false     | 0
            "3"     | "1234" | "5678" | ""    || true      | 1
            "03"    | "123"  | "5678" | ""    || true      | 1
            "03123" | "4"    | "5678" | ""    || false     | 0
            "03"    | "1234" | "567"  | ""    || true      | 1
        }

    }

    @RunWith(PowerMockRunner)
    @PowerMockRunnerDelegate(SpringRunner)
    @SpringBootTest
    @PrepareForTest(EmailValidator)
    @PowerMockIgnore("javax.management.*")
    static class InquiryInput02FormValidator_メールアドレス {

        @Autowired
        private InquiryInput02FormValidator input02FormValidator

        Errors errors
        InquiryInput02Form inquiryInput02Form

        @Before
        void setup() {
            errors = TestHelper.createErrors()
            inquiryInput02Form = new InquiryInput02Form(
                    zipcode1: "102"
                    , zipcode2: "0072"
                    , address: "東京都千代田区飯田橋1-1"
                    , tel1: ""
                    , tel2: ""
                    , tel3: ""
                    , email: "taro.tanaka@sample.co.jp")
        }

        @Test
        void "メールアドレスの Validation のテスト"() {
            when: "EmailValidator.validate が true を返すように設定してテストする"
            PowerMockito.mockStatic(EmailValidator)
            PowerMockito.when(EmailValidator.validate(Mockito.any())) thenReturn(true)
            input02FormValidator.validate(inquiryInput02Form, errors)

            then: "入力チェックエラーは発生しない"
            assert errors.hasErrors() == false
            assert errors.getAllErrors().size() == 0
            // EmailValidator.validate が呼び出されていることをチェックする
            PowerMockito.verifyStatic(Mockito.times(1))
            EmailValidator.validate("taro.tanaka@sample.co.jp")

            and: "EmailValidator.validate が false を返すように設定してテストする"
            PowerMockito.when(EmailValidator.validate(Mockito.any())) thenReturn(false)
            input02FormValidator.validate(inquiryInput02Form, errors)

            then: "入力チェックエラーが発生する"
            assert errors.hasErrors() == true
            assert errors.getAllErrors().size() == 1
        }

    }

}

テストを実行して全て成功することを確認します。。。が、1つ失敗しました。電話番号、メールアドレスが全て空の場合に入力チェックが OK になってしまうようです。

f:id:ksby:20171015070055p:plain

src/main/assets/js/inquiry/input02.js と src/main/java/ksbysample/webapp/bootnpmgeb/web/inquiry/form/InquiryInput02FormValidator.java で最初の方の処理が一致していないことが原因でした。InquiryInput02FormValidator クラスの方も ignoreCheckRequired のようなものを入れる必要がありますね。。。

    var validateFunction = function () {
        if (validator.ignoreCheckRequired && form.isAllEmpty(idList)) {
            return;
        }

        if (form.isAllEmpty(idList)) {
            var errmsg = "電話番号とメールアドレスのいずれか一方を入力してください";
            form.setError(telIdFormGroup, errmsg);
            form.setError(emailIdFormGroup, errmsg);
            throw new Error(errmsg);
        } else {
        if (StringUtils.isEmpty(tel1)
                && StringUtils.isEmpty(tel2)
                && StringUtils.isEmpty(tel3)
                && StringUtils.isEmpty(email)) {
            return;
        }

        if (StringUtils.isEmpty(tel1 + tel2 + tel3)
                && StringUtils.isEmpty(email)) {
            errors.reject("InquiryInput02Form.telOrEmail.NotEmpty");
        } else {

以下の内容で修正することにします。

  • input02.html に <input type="hidden" name="ignoreCheckRequired" ... /> を追加します。
  • input02 で必須チェック不要な時には <input type="hidden" name="ignoreCheckRequired" ... /> にも true をセットします。
  • InquiryInput02Form クラスに ignoreCheckRequired を用意します。
  • InquiryInput02FormValidator で InquiryInput02Form.ignoreCheckRequired == true なら一番最初の必須チェックは行わないようにします。

src/main/resources/templates/web/inquiry/input02.html の以下の点を変更します。

          <!--/*@thymesVar id="inquiryInput02Form" type="ksbysample.webapp.bootnpmgeb.web.inquiry.form.InquiryInput02Form"*/-->
          <form id="inquiryInput02Form" class="form-horizontal" method="post" action=""
                th:action="@{/inquiry/input/02/}"
                th:object="${inquiryInput02Form}">
            <input type="hidden" name="copiedFromSession" id="copiedFromSession" th:value="*{copiedFromSession}"/>
            <input type="hidden" name="ignoreCheckRequired" id="ignoreCheckRequired" value="false"/>
  • <input type="hidden" name="ignoreCheckRequired" id="ignoreCheckRequired" value="false"/> を追加します。value は固定で false にします。

src/main/assets/js/inquiry/input02.js の以下の点を変更します。

var btnBackOrNextClickHandler = function (event, url, ignoreCheckRequired) {
    ..........

    // サーバにリクエストを送信する
    $("#ignoreCheckRequired").val(ignoreCheckRequired);
    $("#inquiryInput02Form").attr("action", url);
    $("#inquiryInput02Form").submit();

    // return false は
    // event.preventDefault() + event.stopPropagation() らしい
    return false;
};
  • btnBackOrNextClickHandler 関数の以下の点を変更します。
    • $("#ignoreCheckRequired").val(ignoreCheckRequired); を追加します。

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

@Data
public class InquiryInput02Form implements Serializable {

    ..........

    private boolean ignoreCheckRequired = false;

}
  • private boolean ignoreCheckRequired = false; を追加します。

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

@Component
public class InquiryInput02FormValidator implements Validator {

    ..........

    @Override
    public void validate(Object target, Errors errors) {
        ..........

        checkTelAndEmail(inquiryInput02Form.isIgnoreCheckRequired()
                , inquiryInput02Form.getTel1()
                , inquiryInput02Form.getTel2()
                , inquiryInput02Form.getTel3()
                , inquiryInput02Form.getEmail()
                , errors);
    }

    ..........

    @SuppressWarnings({"PMD.CyclomaticComplexity", "PMD.ConfusingTernary"})
    private void checkTelAndEmail(boolean ignoreCheckRequired, String tel1, String tel2, String tel3, String email, Errors errors) {
        if (ignoreCheckRequired
                && StringUtils.isEmpty(tel1)
                && StringUtils.isEmpty(tel2)
                && StringUtils.isEmpty(tel3)
                && StringUtils.isEmpty(email)) {
            return;
        }

        ..........
    }

}
  • checkTelAndEmail メソッドの以下の点を変更します。
    • 引数に boolean ignoreCheckRequired を追加します。
    • 最初の if 文に ignoreCheckRequired の条件を追加します。
  • validate メソッド内で checkTelAndEmail メソッドを呼び出している部分の引数に inquiryInput02Form.isIgnoreCheckRequired() を追加します。

画面を動かして動作に問題がないことを確認した後(画面キャプチャは省略します)、src/test/groovy/ksbysample/webapp/bootnpmgeb/web/inquiry/form/InquiryInput02FormValidatorTest.groovy の以下の点を変更します。

    @SpringBootTest
    static class InquiryInput02FormValidator_メールアドレス以外 extends Specification {

        ..........

        @Unroll
        def "電話番号の Validation のテスト(#tel1,#tel2,#tel3,#ignoreCheckRequired --> #hasErrors,#size)"() {
            given:
            inquiryInput02Form.tel1 = tel1
            inquiryInput02Form.tel2 = tel2
            inquiryInput02Form.tel3 = tel3
            inquiryInput02Form.email = email
            inquiryInput02Form.ignoreCheckRequired = ignoreCheckRequired

            when:
            input02FormValidator.validate(inquiryInput02Form, errors)

            then:
            errors.hasErrors() == hasErrors
            errors.getAllErrors().size() == size
            // メールアドレスのチェックは行われていないことをチェックする
            0 * EmailValidator.validate(email)

            where:
            tel1    | tel2   | tel3   | email | ignoreCheckRequired || hasErrors | size
            ""      | ""     | ""     | ""    | false               || true      | 1
            ""      | ""     | ""     | ""    | true                || false     | 0
            "03"    | ""     | ""     | ""    | false               || true      | 1
            ""      | "1234" | ""     | ""    | false               || true      | 1
            ""      | ""     | "5678" | ""    | false               || true      | 1
            "03"    | "1234" | "5678" | ""    | false               || false     | 0
            "3"     | "1234" | "5678" | ""    | false               || true      | 1
            "03"    | "123"  | "5678" | ""    | false               || true      | 1
            "03123" | "4"    | "5678" | ""    | false               || false     | 0
            "03"    | "1234" | "567"  | ""    | false               || true      | 1
        }

    }
  • InquiryInput02FormValidator_メールアドレス以外 クラスの以下の点を変更します。
    • 電話番号の Validation のテスト メソッドのメソッド名に ,#ignoreCheckRequired を追加します。
    • given: のところで、inquiryInput02Form.ignoreCheckRequired = ignoreCheckRequired を追加します。
    • where:ignoreCheckRequired を追加しテスト用の値も記述します。

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

f:id:ksby:20171015164144p:plain

履歴

2017/10/15
初版発行。