Spring Boot + npm + Geb で入力フォームを作ってテストする ( その31 )( テスト対象のブラウザに Headless Chrome と HtmlUnit を追加する+Chrome, Firefox, HtmlUnit で連続テストする gradle タスクを作成する )
概要
記事一覧はこちらです。
Spring Boot + npm + Geb で入力フォームを作ってテストする ( その30 )( Geb を 2.0 へバージョンアップする+Firefox headless モードを使用する ) の続きです。
- 今回の手順で確認できるのは以下の内容です。
参照したサイト・書籍
ヘッドレス Chrome ことはじめ
https://developers.google.com/web/updates/2017/04/headless-chrome?hl=jaHeadless Chrome and Selenium on Windows?
https://stackoverflow.com/questions/43880619/headless-chrome-and-selenium-on-windowsSeleniumHQ/htmlunit-driver
https://github.com/SeleniumHQ/htmlunit-driverWhere 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【入門】Geb+SpockではじめるWebテスト~クロスブラウザテスト編~ / Setting up and running of the cross-browser test
http://yfj2.hateblo.jp/entry/2014/11/09/004011ChromeDriver - WebDriver for Chrome
https://sites.google.com/a/chromium.org/chromedriver/geb/geb-example-gradle/build.gradle
https://github.com/geb/geb-example-gradle/blob/master/build.gradle
目次
- Chrome と HtmlUnit 用の Selenium WebDriver をインストールする
- ChromeDriver をダウンロードして配置する
- GebConfig.groovy を修正する
- build.gradle にタスクを定義する
- 動作確認
手順
Chrome と HtmlUnit 用の 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 をその下に配置します。
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つのタスクが表示されます。
動作確認
最初に clean タスク → Rebuild Project → build タスクを実行して BUILD SUCCESSFUL のメッセージが出力されることを確認します。追加した chromeTest
, firefoxTest
, gebTest
, htmlunitTest
のタスクが実行されていないことも確認できます。
chromeTest タスクを実行します。ブラウザの画面が表示されず、テストは成功します。また ChromeDriver のメッセージが出力されており、Chrome がテストに使われていることが分かります。
firefoxTest タスクを実行します。こちらもブラウザの画面は表示されず、テストは成功します。
htmlunitTest タスクを実行します。HtmlUnit はブラウザではないので当然画面は表示されず、テストは成功します。
最後に gebTest タスクを実行します。chromeTest
, firefoxTest
, htmlunitTest
の3つのタスクが実行されてそれぞれ成功し、gebTest
タスクは build.gradle 内で enabled = false
を記述しているので SKIP されることが確認できます。
ちなみに 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 タスクでエラーが出て先に進みません。
履歴
2017/10/29
初版発行。
Spring Boot + npm + Geb で入力フォームを作ってテストする ( その30 )( Geb を 2.0 へバージョンアップする+Firefox headless モードを使用する )
概要
記事一覧はこちらです。
Spring Boot + npm + Geb で入力フォームを作ってテストする ( その29 )( Geb をインストールする ) の続きです。
- 今回の手順で確認できるのは以下の内容です。
参照したサイト・書籍
Geb - Very Groovy Browser Automation
http://www.gebish.org/Headless mode
https://developer.mozilla.org/en-US/Firefox/Headless_modeDisable HttpClient logging
https://stackoverflow.com/questions/4915414/disable-httpclient-loggingHow do I disable Firefox logging in Selenium using Geckodriver?
https://stackoverflow.com/questions/41387794/how-do-i-disable-firefox-logging-in-selenium-using-geckodriver
目次
- Geb を 2.0-rc-1 → 2.0 へバージョンアップする
- geckodriver.exe の場所は
System.setProperty("webdriver.gecko.driver", "...")
で指定する - Firefox headless モードを使用する
手順
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-1
→2.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
を削除します。
動作確認します。
最初に clean タスク → Rebuild Project → build タスクを実行して BUILD SUCCESSFUL のメッセージが出力されることを確認します(画面キャプチャは省略します)。
次に gebTest タスクを実行すると Firefox が起動し、テストは成功します。
最後に src/test/groovy/geb/gebspec/SimpleTestSpec.groovy を開き、左側のアイコンから Run '動作確認用()'
を選択します。
こちらも Firefox が起動し、テストは成功します。
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 ログが大量に出ます。。。 これは出来れば出ないようにしたいですね。
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 はエラーメッセージが分かりやすくていいですね。
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 モードでも問題なく動いているようです。
大量に出力される 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
というログが出力されています。
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.exe
→C:/geckodriver/0.19.0/geckodriver.bat
に変更します。
gebTest タスクを実行すると Marionette DEBUG
のログは全く出力されなくなりました。
履歴
2017/10/28
初版発行。
Spring Boot + npm + Geb で入力フォームを作ってテストする ( その29 )( Geb をインストールする )
概要
記事一覧はこちらです。
参照したサイト・書籍
Geb - Very Groovy Browser Automation
http://www.gebish.org/Web画面自動テストフレームワーク「Geb」の紹介
http://lab.astamuse.co.jp/entry/geb_test_01Geb with spock
https://www.slideshare.net/MonikaGurram/geb-with-spock-24710923mozilla/geckodriver
https://github.com/mozilla/geckodriverGradle – How to exclude some tests
https://www.mkyong.com/gradle/gradle-how-to-exclude-some-tests/Getting Started With Gradle: Integration Testing
https://www.petrikainulainen.net/programming/gradle/getting-started-with-gradle-integration-testing/Spring Boot and Gradle: Separating tests
https://moelholm.com/2016/10/22/spring-boot-separating-tests/
目次
- 方針
- Geb に必要なモジュールをインストールする
- GebConfig.groovy を作成する
- テスト用のパッケージを作成する
- 入力画面1の Page Objects を作成する
- 簡単なテストを作成する
- geckodriver をインストールする
- gebTest タスクを作成する
- 動作確認
- 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
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 パッケージを作成します。
入力画面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 の下に配置します。
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
というエラーメッセージが出て失敗しました。
モジュールを見てみると selenium 関連で4つ表示されているのですが、selenium-api と selenium-remote-driver が 2.53.1 と古いバージョンになっています。全て 3.6.0 になるよう調整します。
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 になりました。
再度 getTest タスクを実行すると、今度は Firefox が起動して入力画面1にアクセスし、テストが成功しました。
Tomcat を停止した後、clean タスク → Rebuild Project → build タスクを実行すると BUILD SUCCESSFUL のメッセージが出力されました。test タスク中に Firefox は起動しなかったので、Geb で作成したテストは実行されませんでした。
IntelliJ IDEA 上からテストを実行できるようにする
今の設定だけでは IntelliJ IDEA のエディタの左側のメニューから Run 動作確認用()
を選択してもテストを実行できません。
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
というエラーメッセージが出力されます。
IntelliJ IDEA のメインメニューから「Run」-「Edit Configurations...」を選択して「Run/Debug Configurations」ダイアログを表示した後、「JUnit」の「VM Options」に -Dwebdriver.gecko.driver=C:/geckodriver/0.19.0/geckodriver.exe
を追加します。
再度 Run 動作確認用()
を選択してテストを実行すると Firefox が起動して入力画面1にアクセスし、テストが成功しました。
履歴
2017/10/27
初版発行。
Spring Boot + npm + Geb で入力フォームを作ってテストする ( 番外編 )( ModelMapper メモ書き )
概要
記事一覧はこちらです。
最近 POJO 間のデータコピーに ModelMapper を使用していますが、使っていて気づいた点やつまずいた点をメモしておきます。
尚、ModelMapper の利用には rozidan/modelmapper-spring-boot-starter を利用しています(利用しなくても ModelMapper を使うのは難しくありませんが少し便利になる感じです)。
参照したサイト・書籍
目次
- String --> int 変換は何も定義しなくてもやってくれる
- skip に指定したフィールドがプリミティブ型だと実行時に NullPointerException が発生する
- 部分的に特別な処理をしたい時には setPreConverter で定義する
- コピー元からコピー先へ通常のフィールドコピーが行われるフィールドに preConverter で特別な処理をする場合には、一緒に skip も指定して通常のフィールドコピーが行われないようにする
- sourceType, destinationType に指定するクラスが同じで変換ルールが異なる TypeMap を作りたい場合には name を設定する
- rozidan/modelmapper-spring-boot-starter を使わずに ModelMapper を使用するには?
- 最後に
本文
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())); } }
上のテストを実行すると成功します。
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())); } }
以下のような 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 が発生して起動しません。
これは skip に指定するフィールドがプリミティブ型であることが原因です。参照型に変更するとこのエラーは出ません。
@Data @Builder @NoArgsConstructor @AllArgsConstructor static class DstData { // コピー先をプリミティブ型(int)にする --> 参照型に変更する // private int age; private Integer age; }
int --> Integer に変更すると Tomcat は起動します。
部分的に特別な処理をしたい時には 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(); } }
上のテストを実行すると成功します。
コピー元からコピー先へ通常のフィールドコピーが行われるフィールドに 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()); } }
上のテストを実行すると成功します。
ちなみに 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)); } }
フィールドのコピー処理が行われるのでテストは失敗します。
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()); } }
上のテストを実行すると成功します。
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 の最新バージョンを使う方法が理解できました。
参照したサイト・書籍
目次
- Spring Boot を 1.5.4 → 1.5.7 へバージョンアップする(他のライブラリもバージョンアップする)
- 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 タスクを実行してみますが、checkstyle で Property 'maxLineLength' in module LeftCurly does not exist, please check the documentation
というエラーが出ました。
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" のメッセージが出力されました。
Project Tool Window で src/test を選択した後、コンテキストメニューを表示して「Run 'All Tests' with Coverage」を選択し、テストが全て成功することも確認できました。
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
アノテーションで引っかかっているようです。
もう少しエラーが発生している詳細な原因が知りたいので、コマンドプロンプトから gradlew --stacktrace build
コマンドを実行してみます。
結果として上の画像のメッセージが出力されますが、ここで注目するのは com.google.errorprone.bugpatterns
のどのクラスで引っかかっているのか、です。今回は NestedInstanceOfConditions
で引っかかっています。
NestedInstanceOfConditions
で Google で検索すると 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
で引っかかっています。
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" が出力されました。
Project Tool Window で src/test を選択した後、コンテキストメニューを表示して「Run 'All Tests' with Coverage」を選択し、テストが全て成功することも確認しておきます。
履歴
2017/10/18
初版発行。
Spring Boot + npm + Geb で入力フォームを作ってテストする ( その27 )( 入力画面2を作成する6 )
概要
記事一覧はこちらです。
Spring Boot + npm + Geb で入力フォームを作ってテストする ( その26 )( 入力画面2を作成する5 ) の続きです。
- 今回の手順で確認できるのは以下の内容です。
- 入力画面2の作成
- サーバ側のテストを作成します。前回からの続きです。
参照したサイト・書籍
目次
- InquiryInputController クラスのテストを変更する
- build タスクを実行したら PMD でエラーが出たので pmd-project-rulesets.xml と InquiryInput02FormValidator.java を修正する
- 次回は。。。
手順
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のテスト
クラスを追加します。
テストを実行して全て成功することを確認します。
build タスクを実行したら PMD でエラーが出たので pmd-project-rulesets.xml と InquiryInput02FormValidator.java を修正する
clean タスク実行 → Rebuild Project 実行 → build タスクを実行すると、PMD で何件かメッセージが出ました。
build/reports/pmd/main.xml を見ると以下の3種類の Rule が原因でした。
- https://pmd.github.io/pmd-5.8.1/pmd-java/rules/java/codesize.html#ModifiedCyclomaticComplexity
- https://pmd.github.io/pmd-5.8.1/pmd-java/rules/java/codesize.html#StdCyclomaticComplexity
- https://pmd.github.io/pmd-5.8.1/pmd-java/rules/java/basic.html#CollapsibleIfStatements
ただし 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" のメッセージが出力されました。
次回は。。。
入力画面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回に分けます。
参照したサイト・書籍
- Verify Static Method Call using PowerMockito 1.6
https://stackoverflow.com/questions/34323909/verify-static-method-call-using-powermockito-1-6
目次
- InquiryInput02Form クラスのテストを作成する
- InquiryInput02FormNotEmptyRule クラスのテストを作成する
- EmailValidator クラスのテストを作成する
- InquiryInput02FormValidator クラスのテストを作成する
手順
InquiryInput02Form クラスのテストを作成する
src/main/java/ksbysample/webapp/bootnpmgeb/web/inquiry/form/InquiryInput02Form.java で Ctrl+Shift+T を押して「Create Test」ダイアログを表示してから、以下の画像の値にした後「OK」ボタンをクリックします。
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 } }
テストを実行して全て成功することを確認します。
InquiryInput02FormNotEmptyRule クラスのテストを作成する
src/main/java/ksbysample/webapp/bootnpmgeb/web/inquiry/form/InquiryInput02FormNotEmptyRule.java で Ctrl+Shift+T を押して「Create Test」ダイアログを表示してから、以下の画像の値にした後「OK」ボタンをクリックします。
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 } }
テストを実行して全て成功することを確認します。
EmailValidator クラスのテストを作成する
src/main/java/ksbysample/webapp/bootnpmgeb/util/validator/EmailValidator.java で Ctrl+Shift+T を押して「Create Test」ダイアロ グを表示してから、以下の画像の値にした後「OK」ボタンをクリックします。
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 } }
テストを実行して全て成功することを確認します。
InquiryInput02FormValidator クラスのテストを作成する
src/main/java/ksbysample/webapp/bootnpmgeb/web/inquiry/form/InquiryInput02FormValidator.java で Ctrl+Shift+T を押して「Create Test」ダイアログを表示してから、以下の画像の値にした後「OK」ボタンをクリックします。
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 になってしまうようです。
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
を追加しテスト用の値も記述します。
テストを実行すると今度は全て成功しました。
履歴
2017/10/15
初版発行。