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
初版発行。
Spring Boot + npm + Geb で入力フォームを作ってテストする ( その25 )( 入力画面2を作成する4 )
概要
記事一覧はこちらです。
Spring Boot + npm + Geb で入力フォームを作ってテストする ( その24 )( 入力画面2を作成する3 ) の続きです。
参照したサイト・書籍
Autocomplete | jQuery UI
https://jqueryui.com/autocomplete/[jQuery UI] AutocompleteのソースをAjaxで取得する
http://www.84kure.com/blog/2015/07/27/jquery-ui-autocompleteのソースをajaxで取得する/1分でわかるjQuery $.ajaxによるJSON・JSONP読み込み方法
https://iwb.jp/jquery-ajax-jsonp/Display jquery ui auto-complete list on focus event
https://stackoverflow.com/questions/4132058/display-jquery-ui-auto-complete-list-on-focus-eventAJAX通信をするときはタイムアウト処理を必ず入れてほしい(切実)
https://qiita.com/tonkotsuboy_com/items/d1b3cf45ae5135441f9bHow to include jQuery UI into my project using Webpack?
https://stackoverflow.com/questions/38796673/how-to-include-jquery-ui-into-my-project-using-webpack
目次
手順
npm で jquery-ui をインストールする
郵便番号を入力したら jQuery-UI の autocomplete で「住所」の入力項目の下にドロップダウンリストを表示して選択できるようにします。
admin-lte パッケージ内の plugins/jQueryUI の下には js ファイルしかないのですが、autocomplete の表示には css ファイルも必要なので npm で jquery-ui パッケージをインストールします。
npm install --save jquery-ui
コマンドを実行して jQueryUI をインストールします。
package.json に jquery-ui の css をコピーする npm-scirpts を追加します。
"scripts": { .......... "copy:all": "run-p copy:bootstrap copy:admin-lte copy:font-awesome copy:ionicons copy:jquery-ui-css", .......... "copy:ionicons": "cpx node_modules/ionicons/dist/{css,fonts}/**/* src/main/resources/static/vendor/ionicons", "copy:jquery-ui-css": "cpx node_modules/jquery-ui/themes/base/**/* src/main/resources/static/vendor/jquery-ui/css", .......... },
"copy:jquery-ui-css": "cpx node_modules/jquery-ui/themes/base/**/* src/main/resources/static/vendor/jquery-ui/css"
を追加します。copy:all
の最後にcopy:jquery-ui-css
を追加します。
npm run copy:jquery-ui-css
コマンドを実行して css ファイルをコピーします。
入力された郵便番号の住所一覧を jQuery-UI の autocomplete で表示・選択する処理を実装する
src/main/assets/js/inquiry/input02.js を以下のように変更します。
"use strict"; var Form = require("lib/class/Form.js"); var converter = require("lib/util/converter.js"); var validator = require("lib/util/validator.js"); require("jquery-ui/ui/widgets/autocomplete.js"); .......... var addressList = []; var findAddressListByZipCode = function (event) { // 入力チェックエラーが発生している場合には処理を中断する if (event.isPropagationStopped()) { return; } // 郵便番号の入力項目全てにフォーカスがセットされていなければ処理を中断する if (!form.isAllFocused(form, ["#zipcode1", "#zipcode2"])) { return; } // 郵便番号検索APIで住所を検索する addressList = []; $.ajax({ type: "get", url: "http://zipcloud.ibsnet.co.jp/api/search", data: {zipcode: $("#zipcode1").val() + $("#zipcode2").val()}, cache: false, dataType: "jsonp", jsonpCallback: "callback", timeout: 5000 }).then( function (json) { if (json.status === 200) { if (json.results === null) { form.setError("#form-group-address", "郵便番号に該当する住所はありませんでした"); return; } json.results.forEach(function (result) { addressList.push(result.address1 + result.address2 + result.address3); }); // 「住所」の入力項目の下に autocomplete のドロップダウンリストを表示する if ($("#address").is(":focus")) { $("#address").autocomplete("search"); } } else { form.setError("#form-group-address", json.message); } }, function () { form.setError("#form-group-address", "郵便番号APIの呼び出しに失敗しました"); } ); }; .......... $(document).ready(function () { // 入力チェック用の validator 関数をセットする .......... // 入力された郵便番号から住所を取得する処理をセットする $("#address").on("focus", findAddressListByZipCode); $("#address").autocomplete({ // source には addressList 変数を直接指定するのではなく、 // 関数を渡して findAddressListByZipCode 関数で変更された addressList // のデータが表示されるようにする source: function (request, response) { // 「住所」に何も入力されていない時だけドロップダウンリストを表示する if ($("#address").val() === "") { response(addressList); } else { response([]); } }, // minLength: 0 を指定しないと findAddressListByZipCode 関数内で // $("#address").autocomplete("search"); を呼び出した時にドロップダウンリスト // が表示されない minLength: 0 }); .......... });
require("jquery-ui/ui/widgets/autocomplete.js");
を追加します。var addressList = [];
を追加します。findAddressListByZipCode
関数を追加します。$(document).ready(function () { ... }
の以下の点を変更します。["#zipcode1", "#zipcode2"].forEach(function (id) { $(id).on("blur", findAddressListByZipCode); });
を追加します。$("#address").autocomplete({ ... });
を追加します。
src/main/resources/templates/web/inquiry/input02.html の以下の点を変更します。
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head th:replace="~{web/common/fragments :: common_header(~{::title}, ~{::link}, ~{::style})}"> <title>入力フォーム - 入力画面2</title> <link rel="stylesheet" href="/vendor/jquery-ui/css/all.css"> </head> ..........
<link rel="stylesheet" href="/vendor/jquery-ui/css/all.css">
を追加します。
ここまでの実装で npm run springboot
コマンドを実行すると、Module not found: Error: Can't resolve 'jquery' in ...
というエラーメッセージが大量に出力されます。
webpack.config.js で jquery
の alias を定義すればこのエラーは出なくなります。以下のように変更します。
resolve: { modules: [ "node_modules", "src/main/assets/js" ], alias: { jquery: "admin-lte/plugins/jQuery/jquery-2.2.3.min.js" } },
alias: { jquery: "admin-lte/plugins/jQuery/jquery-2.2.3.min.js" }
を追加します。
再び npm run springboot
コマンドを実行すると今度はエラーが出ません。
動作確認
動作確認します。npm run springboot
コマンドは実行済なので、Tomcat を起動した後、ブラウザで http://localhost:9080/inquiry/input/01/ にアクセスします。
データを入力してから、「次へ」ボタンをクリックして入力画面2へ遷移します。
「郵便番号」に 079-0177
を入力して「住所」にフォーカスを移動すると、フォーカスが移動した直後はドロップダウンリストは表示されませんが、郵便番号検索APIの検索が終了するとドロップダウンリストが表示されます。
上下矢印キーで選択すると、選択された項目が「住所」の入力項目にセットされます。
選択してから追加で "1丁目" と入力しても特に動作には問題はありません。
存在しない郵便番号を入力すると「住所」の入力項目の下に "郵便番号に該当する住所はありませんでした" のメッセージが表示されます。
次回は。。。
サーバ側のテストを書きます。
履歴
2017/10/10
初版発行。
IntelliJ IDEA を 2017.2.4 → 2017.2.5 へ、Git for Windows を 2.14.1 → 2.14.2(2) へバージョンアップ
IntelliJ IDEA を 2017.2.4 → 2017.2.5 へバージョンアップする
IntelliJ IDEA の 2017.2.5 がリリースされたのでバージョンアップします。
- IntelliJ IDEA 2017.2.5 Release Notes
https://confluence.jetbrains.com/display/IDEADEV/IntelliJ+IDEA+2017.2.5+Release+Notes
※ksbysample-webapp-lending プロジェクトを開いた状態でバージョンアップしています。
IntelliJ IDEA のメインメニューから「Help」-「Check for Updates...」を選択します。
「IDE and Plugin Updates」ダイアログが表示されます。左下に「Update and Restart」ボタンが表示されていますので、「Update and Restart」ボタンをクリックします。
Plugin の update も表示されました。「Error-prone Compiler Integration」はバージョンアップすると動かなくなりますので、これだけチェックを外して「Update and Restart」ボタンをクリックします。
Patch がダウンロードされて IntelliJ IDEA が再起動します。
IntelliJ IDEA が起動すると画面下部に「Indexing…」のメッセージが表示されます。。。と思いましたが、今回は出ませんでした。
IntelliJ IDEA のメインメニューから「Help」-「About」を選択し、2017.2.5 へバージョンアップされていることを確認します。
Gradle Tool Window のツリーを見ると「Tasks」の下に「other」しかない状態になっているので、左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。
clean タスク実行 → Rebuild Project 実行 → build タスクを実行して、"BUILD SUCCESSFUL" のメッセージが出力されることを確認します。
Project Tool Window で src/test を選択した後、コンテキストメニューを表示して「Run 'All Tests' with Coverage」を選択し、テストが全て成功することを確認します。
Git for Windows を 2.14.1 → 2.14.2(2) へバージョンアップする
Git for Windows の 2.14.2(2) がリリースされていたのでバージョンアップします。
https://git-for-windows.github.io/ の「Download」ボタンをクリックして Git-2.14.2.2-64-bit.exe をダウンロードします。
Git-2.14.2.2-64-bit.exe を実行します。
「Git 2.14.2.2 Setup」ダイアログが表示されます。[Next >]ボタンをクリックします。
「Select Components」画面が表示されます。「Git LFS(Large File Support)」だけチェックした状態で [Next >]ボタンをクリックします。
「Adjusting your PATH environment」画面が表示されます。中央の「Use Git from the Windows Command Prompt」が選択されていることを確認後、[Next >]ボタンをクリックします。
「Choosing HTTPS transport backend」画面が表示されます。「Use the OpenSSL library」が選択されていることを確認後、[Next >]ボタンをクリックします。
「Configuring the line ending conversions」画面が表示されます。一番上の「Checkout Windows-style, commit Unix-style line endings」が選択されていることを確認した後、[Next >]ボタンをクリックします。
「Configuring the terminal emulator to use with Git Bash」画面が表示されます。「Use Windows'default console window」が選択されていることを確認した後、[Next >]ボタンをクリックします。
「Configuring extra options」画面が表示されます。「Enable file system caching」だけがチェックされていることを確認した後、[Install]ボタンをクリックします。
インストールが完了すると「Completing the Git Setup Wizard」のメッセージが表示された画面が表示されます。中央の「View Release Notes」のチェックを外した後、「Finish」ボタンをクリックしてインストーラーを終了します。
コマンドプロンプトを起動して
git --version
を実行し、git のバージョンがgit version 2.14.2.windows.2
になっていることを確認します。git-cmd.exe を起動して日本語の表示・入力が問題ないかを確認します。
特に問題はないようですので、2.14.2(2) で作業を進めたいと思います。