かんがるーさんの日記

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

Spring Boot 1.3.x の Web アプリを 1.4.x へバージョンアップする ( その20 )( 気になった点を修正する )

概要

記事一覧はこちらです。

Spring Boot 1.3.x の Web アプリを 1.4.x へバージョンアップする ( その19 )( Spring Boot を 1.4.4 → 1.4.5 にバージョンアップする ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • 1.4 系へのバージョンアップとは直接関係ありませんが、修正した方がよさそうな点を修正します。

参照したサイト・書籍

  1. Spring Bootでバリデーションエラー時のメッセージリソースをValidationMessages.propertiesからmessages.propertiesにする
    http://qiita.com/NagaokaKenichi/items/65d0e07151292968d67f

目次

  1. application.properties から hibernate.dialect, spring.jpa.~ を削除する
  2. hibernate.properties を削除する
  3. ValuesEnumValidatorTest.java 内で定義しているクラスに static を追加する
  4. FreeMarkerUtils → FreeMarkerHelper へ変更する
  5. ValidationMessages_ja_JP.properties をやめて messages.properties に1本化する& IDEA の Transparent native-to-ascii conversion のチェックを外して UTF-8 の文字列のまま保存する
  6. clean タスク → Rebuild Project → build タスクを実行する

手順

application.properties から hibernate.dialect, spring.jpa.~ を削除する

Spring Boot で書籍の貸出状況確認・貸出申請する Web アプリケーションを作る ( その35 )( 貸出申請画面の作成6 )@SpringBootApplication アノテーションexclude = {JpaRepositoriesAutoConfiguration.class, HibernateJpaAutoConfiguration.class} を追加して JPA を無効にしましたが、不要になった設定を残したままにしていたので削除します。

  1. src/main/resources/application.properties を リンク先の内容 に変更します。

hibernate.properties を削除する

上で書いたように HibernateJpaAutoConfiguration.class は無効にしたので、関連する設定ファイルである hibernate.properties を削除します。

  1. src/main/resources/hibernate.properties を削除します。

ValuesEnumValidatorTest.java 内で定義しているクラスに static を追加する

Error Prone のバージョンアップの検証をしている時に static が付いていないという警告が出たので、付けることにします。

  1. src/test/java/ksbysample/webapp/lending/values/validation/ValuesEnumValidatorTest.javaリンク先の内容 に変更します。

FreeMarkerUtils → FreeMarkerHelper へ変更する

FreeMarkerUtils クラスは static メソッドを持っておらず、クラスに @Component アノテーションを付加しているので、Utils ではなく Helper クラスに変更します。

  1. Project Tool Window で ksbysample.webapp.lending.util パッケージの下の freemarker を ksbysample.webapp.lending.helper パッケージの下へ移動します。

    「Select Refactoring」ダイアログが表示されますので、そのまま「OK」ボタンをクリックします。

    f:id:ksby:20170408192611p:plain

    「Warning」ダイアログが表示されますが、これも「Yes」ボタンをクリックします。

    f:id:ksby:20170408192821p:plain

    「Move」ダイアログが表示されますので、そのまま「Refactor」ボタンをクリックします。

    f:id:ksby:20170408193033p:plain

  2. src/main/java/ksbysample/webapp/lending/helper/freemarker の下の FreeMarkerUtils クラスのクラス名を FreeMarkerHelper へ変更します。

    IntelliJ IDEA 2017.1 からなのか、クラスを選択して Shift+F6 を押しても「Rename」ダイアログが表示されませんでした。今回は修正箇所を1つずつ修正しています。バグ?

  3. src/test/groovy/ksbysample/webapp/lending/helper/freemarker の下の FreeMarkerUtilsTest クラスのクラス名を FreeMarkerHelperTest へ変更します。

  4. 以下のソースに記述されている FreeMarkerUtils freeMarkerUtilsFreeMarkerHelper freeMarkerHelper へ変更します。

    • src/main/java/ksbysample/webapp/lending/helper/mail/Mail001Helper.java
    • src/main/java/ksbysample/webapp/lending/helper/mail/Mail002Helper.java
    • src/main/java/ksbysample/webapp/lending/helper/mail/Mail003Helper.java
    • src/test/groovy/ksbysample/webapp/lending/helper/freemarker/FreeMarkerHelperTest.groovy

ValidationMessages_ja_JP.properties をやめて messages.properties に1本化する& IDEA の Transparent native-to-ascii conversion のチェックを外して UTF-8 の文字列のまま保存する

Bean Validation に Hibernate Validator を利用している場合、メッセージは ValidationMessages.properties に記述し、日本語が含まれるなら native-to-ascii で変換する必要がありますが、Spring Bootでバリデーションエラー時のメッセージリソースをValidationMessages.propertiesからmessages.propertiesにする の記事にメッセージを messages.properties に一本化する方法が記載されているのを見かけたので、メッセージは messages.properties に一本化し、かつ native-to-ascii を止めて UTF-8 のままで保存するようにします。

  1. src/main/java/ksbysample/webapp/lending/config/ApplicationConfig.javaリンク先の内容 に変更します。

  2. src/main/java/ksbysample/webapp/lending/config の下に WebMvcConfig.java を新規作成し、リンク先の内容 の内容を記述します。

  3. ValidationMessages_ja_JP.properties に定義されたメッセージを messages_ja_JP.properties へ移動します。また使用されていないメッセージを削除します。src/main/resources/messages_ja_JP.properties を リンク先の内容 に変更します。

  4. src/main/resources/ValidationMessages_ja_JP.properties を削除します。

  5. native-to-ascii の設定を解除します。まず src/main/resources/messages_ja_JP.properties のファイルの内容を IntelliJ IDEA とは別のテキストエディタ(メモ帳やサクラエディタ等)にコピーします。

  6. IntelliJ IDEA のメインメニューから「File」-「Settings…」を選択して「Settings」ダイアログを表示した後、以下の画面の「Transparent native-to-ascii conversion」のチェックを外します。

    f:id:ksby:20170408230832p:plain

  7. チェックを外した後に src/main/resources/messages_ja_JP.properties を開くとメッセージが AbstractUserDetailsAuthenticationProvider.locked=\u5165\u529B\u3055\u308C\u305F ID \u306F\u30ED\u30C3\u30AF\u3055\u308C\u3066\u3044\u307E\u3059 のように表示されるので、テキストエディタにコピーしておいたメッセージを戻します。

clean タスク → Rebuild Project → build タスクを実行する

最後に build, テストが正常に終了することを確認します。

  1. clean タスク → Rebuild Project → build タスクを実行して “BUILD SUCCESSFUL” が表示されることが確認できました。

    f:id:ksby:20170408235229p:plain

  2. Project Tool Window の src/test から「Run ‘All Tests’ with Coverage」を実行してテストが全て成功しました。

    f:id:ksby:20170409000955p:plain

ソースコード

application.properties

doma.dialect=org.seasar.doma.jdbc.dialect.PostgresDialect

spring.session.store-type=redis

spring.freemarker.cache=true
spring.freemarker.charset=UTF-8
spring.freemarker.enabled=false
spring.freemarker.prefer-file-system-access=false
  • 以下の設定を削除します。
    • hibernate.dialect=org.hibernate.dialect.PostgreSQL9Dialect
    • spring.jpa.hibernate.ddl-auto=none
    • spring.jpa.hibernate.naming_strategy=org.hibernate.cfg.ImprovedNamingStrategy

ValuesEnumValidatorTest.java

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

    ..........

    // テスト用 POJO クラス
    @Data
    private static class NotAllowEmptyTestClass {
        @ValuesEnum(enumClass = TestValues.class)
        private String testStr;
    }

    // テスト用 POJO クラス
    @Data
    private static class AllowEmptyTestClass {
        @ValuesEnum(enumClass = TestValues.class, allowEmpty = true)
        private String testStr;
    }

    private Validator validator;

    ..........
  • NotAllowEmptyTestClass と AllowEmptyTestClass クラスの定義に static を追加します。

ApplicationConfig.java

@Configuration
public class ApplicationConfig {

    ..........

    private final MessageSource messageSource;

    /**
     * @param connectionFactory {@link ConnectionFactory} bean
     * @param messageSource     {@link MessageSource} bean
     */
    public ApplicationConfig(ConnectionFactory connectionFactory
            , MessageSource messageSource) {
        this.connectionFactory = connectionFactory;
        this.messageSource = messageSource;
    }

    ..........

    /**
     * Controller クラスで直接 {@link Validator} を呼び出すために Bean として定義している
     * また Hibernate Validator のメッセージを ValidationMessages.properties ではなく
     * messages.properties に記述できるようにするためにも使用している
     *
     * @return new {@link LocalValidatorFactoryBean}
     */
    @Bean
    public Validator validator() {
        LocalValidatorFactoryBean localValidatorFactoryBean = new LocalValidatorFactoryBean();
        localValidatorFactoryBean.setValidationMessageSource(this.messageSource);
        return localValidatorFactoryBean;
    }

    ..........
  • フィールド変数 private final MessageSource messageSource; を追加し、コンストラクタインジェクションを追加します。
  • validator メソッドの処理を上記のように変更します。localValidatorFactoryBean.setValidationMessageSource(this.messageSource); を呼び出すようにします。

WebMvcConfig.java

package ksbysample.webapp.lending.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.validation.Validator;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

@Configuration
public class WebMvcConfig extends WebMvcConfigurerAdapter {

    private final Validator validator;

    /**
     * @param validator {@link Validator} bean
     */
    public WebMvcConfig(Validator validator) {
        this.validator = validator;
    }

    /**
     * Hibernate Validator のメッセージを ValidationMessages.properties ではなく
     * messages.properties に記述するために Override して {@link Validator} bean を返している
     *
     * @return {@link Validator} bean
     */
    @Override
    public Validator getValidator() {
        return validator;
    }

}

messages_ja_JP.properties

AbstractUserDetailsAuthenticationProvider.locked=入力された ID はロックされています
AbstractUserDetailsAuthenticationProvider.disabled=入力された ID は使用できません
AbstractUserDetailsAuthenticationProvider.expired=入力された ID の有効期限が切れています
AbstractUserDetailsAuthenticationProvider.credentialsExpired=入力された ID のパスワードの有効期限が切れています
AbstractUserDetailsAuthenticationProvider.badCredentials=入力された ID あるいはパスワードが正しくありません
UserInfoUserDetailsService.usernameNotFound=入力された ID あるいはパスワードが正しくありません

# Bean Validation 用メッセージ
error.size.max = {max}文字以内で入力して下さい。
javax.validation.constraints.NotNull.message=必須の入力項目です。
org.hibernate.validator.constraints.Email.message=メールアドレスを入力して下さい。
org.hibernate.validator.constraints.NotBlank.message=必須の入力項目です。
typeMismatch.java.lang.Long=数値を入力して下さい。

Global.optimisticLockException=既にデータが更新されているため更新できませんでした。データを読み込み直してください。

..........
  • 以下のメッセージを追加します。
    • error.size.max = {max}文字以内で入力して下さい。
    • javax.validation.constraints.NotNull.message=必須の入力項目です。
    • org.hibernate.validator.constraints.Email.message=メールアドレスを入力して下さい。
    • org.hibernate.validator.constraints.NotBlank.message=必須の入力項目です。
  • 以下のメッセージを削除します。
    • typeMismatch.java.math.BigDecimal=数値を入力して下さい。
  • 以下のメッセージは ValidationMessages_ja_JP.properties から移動せず削除します。
    • error.digits.integerandfraction = 数値を整数{integer}桁以内、小数{fraction}桁以内で入力して下さい。
    • error.digits.integeronly = 数値を整数{integer}桁以内で入力して下さい。

履歴

2017/04/09
初版発行。

Spring Boot 1.3.x の Web アプリを 1.4.x へバージョンアップする ( その19 )( Spring Boot を 1.4.4 → 1.4.5 にバージョンアップする )

概要

記事一覧はこちらです。

Spring Boot 1.3.x の Web アプリを 1.4.x へバージョンアップする ( その18 )( Gradle のバージョンを 2.13 → 3.x へバージョンアップ。。。しようと思いましたが止めました ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • 記事を書き始めた時の Spring Boot のバージョンは 1.4.4 でしたが、1.4.5 が出ているのでバージョンアップします。
    • Error Prone 以外のライブラリも最新バージョンにバージョンアップします。
    • build.gradle の書き方で、最近は jar { ... } は書かずに group, version を一番上に書くようにしているので、そのように変更します。また Error Prone の compileJava.options.compilerArgs += ['-Xep:RemoveUnusedImports:WARN'] の設定を追加して不要な import 文が検出されるようにします。

参照したサイト・書籍

目次

  1. build.gradle を変更する
  2. clean タスク → Rebuild Project → build タスクを実行する
  3. メモ書き

手順

build.gradle を変更する

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

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

clean タスク → Rebuild Project → build タスクを実行する

  1. clean タスク → Rebuild Project → build タスクを実行します。

    f:id:ksby:20170408013157p:plain f:id:ksby:20170408013605p:plain

    build.gradle に追加した compileJava.options.compilerArgs += ['-Xep:RemoveUnusedImports:WARN'] により、使用されていない import 文が入っているソースがログに出力されていました。以下の2ファイルです。不要な import 文を削除します。

    • src/main/java/ksbysample/webapp/lending/Application.java
    • src/main/java/ksbysample/webapp/lending/helper/url/UrlAfterLoginHelper.java

    他は問題ないようで無事 “BUILD SUCCESSFUL” のメッセージが表示されています。

  2. Project Tool Window の src/test から「Run ‘All Tests’ with Coverage」を実行すると、こちらも全てのテストが成功しました。

    f:id:ksby:20170408075659p:plain

メモ書き

  • Lombok の 1.16.16 が出ていたので Lombok Changelog を見てみたのですが、JDK9 now supported と記述されていました。これでいよいよ Error Prone のバージョンアップが出来るかも!?

ソースコード

build.gradle

group 'ksbysample'
version '1.4.5-RELEASE'

buildscript {
    ext {
        springBootVersion = '1.4.5.RELEASE'
    }
    repositories {
        jcenter()
        maven { url "http://repo.spring.io/repo/" }
        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.9")
        // for Grgit
        classpath("org.ajoberstar:grgit:1.9.0")
        // Gradle Download Task
        classpath("de.undercouch:gradle-download-task:3.2.0")
    }
}

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

sourceCompatibility = 1.8
targetCompatibility = 1.8

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

[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 = '7.6.1'
    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) {
    reports {
        xml.enabled = false
        html.enabled = true
    }
}

repositories {
    jcenter()
}

dependencyManagement {
    imports {
        mavenBom("io.spring.platform:platform-bom:Athens-SR4") {
            bomProperty 'commons-lang3.version', '3.5'
            bomProperty 'guava.version', '21.0'
        }
    }
}

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

dependencies {
    def jdbcDriver = "org.postgresql:postgresql:9.4.1212"
    def spockVersion = "1.1-groovy-2.4-rc-4"
    def domaVersion = "2.16.0"
    def lombokVersion = "1.16.16"
    def errorproneVersion = "2.0.15"

    // 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")
    compile("org.thymeleaf.extras:thymeleaf-extras-springsecurity4")
    compile("org.thymeleaf.extras:thymeleaf-extras-java8time")
    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-starter-data-redis")
    compile("org.springframework.boot:spring-boot-starter-amqp")
    compile("org.springframework.boot:spring-boot-devtools")
    compile("org.springframework.session:spring-session")
    compile("org.springframework.retry:spring-retry")
    compile("com.fasterxml.jackson.datatype:jackson-datatype-jsr310")
    compile("com.fasterxml.jackson.dataformat:jackson-dataformat-xml")
    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 によりバージョン番号が自動で設定されないもの、あるいは最新バージョンを指定したいもの
    runtime("${jdbcDriver}")
    compile("org.bgee.log4jdbc-log4j2:log4jdbc-log4j2-jdbc4.1:1.16")
    compile("org.simpleframework:simple-xml:2.7.1")
    compile("com.univocity:univocity-parsers:2.3.1")
    testCompile("org.dbunit:dbunit:2.5.3")
    testCompile("com.icegreen:greenmail:1.5.3")
    testCompile("org.assertj:assertj-core:3.6.2")
    testCompile("com.jayway.jsonpath:json-path:2.2.0")
    testCompile("org.spockframework:spock-core:${spockVersion}") {
        exclude module: "groovy-all"
    }
    testCompile("org.spockframework:spock-spring:${spockVersion}") {
        exclude module: "groovy-all"
    }
    testCompile("com.google.code.findbugs:jsr305:3.0.2")

    // 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("${jdbcDriver}")

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

..........
  • group 'ksbysample' を追加します。
  • version '1.4.5-RELEASE' を追加します。バージョン番号は Spring Boot のバージョンに合わせます。
  • buildscript の以下の点を変更します。
    • springBootVersion = '1.4.4.RELEASE'springBootVersion = '1.4.5.RELEASE' に変更します。
    • classpath("org.ajoberstar:grgit:1.8.0")classpath("org.ajoberstar:grgit:1.9.0") に変更します。
  • compileJava.options.compilerArgs += ['-Xep:RemoveUnusedImports:WARN'] を追加します。
  • jar { ... } を削除します。
  • checkstyle の以下の点を変更します。
    • toolVersion = '7.5.1'toolVersion = '7.6.1' に変更します。
  • dependencyManagement の以下の点を変更します。
    • mavenBom("io.spring.platform:platform-bom:Athens-SR3")mavenBom("io.spring.platform:platform-bom:Athens-SR4") に変更します。
  • dependencies の以下の点を変更します。
    • def jdbcDriver = "org.postgresql:postgresql:9.4.1212"def jdbcDriver = "org.postgresql:postgresql:42.0.0" に変更します。
    • def spockVersion = "1.1-groovy-2.4-rc-3"def spockVersion = "1.1-groovy-2.4-rc-4" に変更します。
    • def domaVersion = "2.15.0"def domaVersion = "2.16.0" に変更します。
    • def lombokVersion = "1.16.12"def lombokVersion = "1.16.16" に変更します。
    • compile("com.univocity:univocity-parsers:2.3.1")compile("com.univocity:univocity-parsers:2.4.1") に変更します。
    • testCompile("com.google.code.findbugs:jsr305:3.0.1")testCompile("com.google.code.findbugs:jsr305:3.0.2") に変更します。
    • // for Doma Gen// for Doma へコメントを変更し、compile("org.seasar.doma:doma:${domaVersion}") をこのコメントの下に移動します。Doma の記述は1つにまとめることにします。

履歴

2017/04/08
初版発行。

Spring Boot 1.3.x の Web アプリを 1.4.x へバージョンアップする ( 番外編 )( Optional をもう少しまともに使ってみる )

概要

記事一覧はこちらです。

src/main/java/ksbysample/webapp/lending/util/cookie/CookieUtils.java のソースに書いた Optional の使い方を見つけて、以前は全然使い方を理解できていなかったんだな。。。と思ったので、修正します。

Optional らしく書き直してみる

src/main/java/ksbysample/webapp/lending/util/cookie/CookieUtils.java で Optional を以下のように使っていたのですが、

    public static String getCookieValue(String cookieName, HttpServletRequest request) {
        Optional<String> result = Optional.empty();
        if (request != null) {
            Cookie[] cookies = request.getCookies();
            if (cookies != null) {
                result = Arrays.asList(cookies).stream()
                        .filter(cookie -> StringUtils.equals(cookie.getName(), cookieName))
                        .map(cookie -> cookie.getValue())
                        .findFirst();
            }
        }
        return result.orElse(null);
    }

上の内容なら if 文を使わずに、もう少しすっきりしたソースにできそうです。戻り値も StringOptional<String> に変更して書き直してみます。

    public static Optional<String> getCookieValue(String cookieName, HttpServletRequest request) {
        return Optional.ofNullable(request)
                .flatMap(req -> Optional.ofNullable(req.getCookies()))
                .flatMap(cookies -> Arrays.stream(cookies)
                            .filter(cookie -> StringUtils.equals(cookie.getName(), cookieName))
                            .map(Cookie::getValue)
                            .findFirst());
    }

CookieUtils#getCookieValue を呼び出している箇所も修正します。

src/main/java/ksbysample/webapp/lending/helper/url/UrlAfterLoginHelper.java は以下のように実装されているのを、

    public static String getUrlAfterLogin(Authentication authentication, HttpServletRequest request) {
        String targetUrl = WebSecurityConfig.DEFAULT_SUCCESS_URL;

        // 特定の権限を持っている場合には対応するURLへリダイレクトする
        GrantedAuthority roleAdmin = new SimpleGrantedAuthority("ROLE_ADMIN");
        if (authentication.getAuthorities().contains(roleAdmin)) {
            // 管理権限 ( ROLE_ADMIN ) を持っている場合には検索対象図書館登録画面へ遷移させる
            targetUrl = Constant.URL_AFTER_LOGIN_FOR_ROLE_ADMIN;
        }

        // LastLendingAppId Cookie に貸出申請ID をセットされている場合には貸出申請画面へリダイレクトさせる
        String cookieLastLendingAppId = CookieUtils.getCookieValue(CookieLastLendingAppId.COOKIE_NAME, request);
        if (StringUtils.isNotBlank(cookieLastLendingAppId)) {
            targetUrl = String.format("%s?lendingAppId=%s", Constant.URL_LENDINGAPP, cookieLastLendingAppId);
        }

        return targetUrl;
    }

以下のように修正します。

    public static String getUrlAfterLogin(Authentication authentication, HttpServletRequest request) {
        String targetUrl = WebSecurityConfig.DEFAULT_SUCCESS_URL;

        // 特定の権限を持っている場合には対応するURLへリダイレクトする
        GrantedAuthority roleAdmin = new SimpleGrantedAuthority("ROLE_ADMIN");
        if (authentication.getAuthorities().contains(roleAdmin)) {
            // 管理権限 ( ROLE_ADMIN ) を持っている場合には検索対象図書館登録画面へ遷移させる
            targetUrl = Constant.URL_AFTER_LOGIN_FOR_ROLE_ADMIN;
        }

        // LastLendingAppId Cookie に貸出申請ID をセットされている場合には貸出申請画面へリダイレクトさせる
        targetUrl = CookieUtils.getCookieValue(CookieLastLendingAppId.COOKIE_NAME, request)
                .map(cookieLastLendingAppId ->
                        String.format("%s?lendingAppId=%s", Constant.URL_LENDINGAPP, cookieLastLendingAppId))
                .orElse(targetUrl);

        return targetUrl;
    }

src/test/groovy/ksbysample/webapp/lending/util/cookie/CookieUtilsTest.groovy は以下のように実装されているのを、

    def "GetCookieValueのテスト"() {
        setup:
        def request = new MockHttpServletRequest()
        
        expect:
        Cookie cookieTest = new Cookie(CookieTest.COOKIE_NAME, "テスト")
        Cookie cookieSample = new Cookie(CookieSample.COOKIE_NAME, "サンプル")
        request.setCookies(cookieTest, cookieSample)
        CookieUtils.getCookieValue(CookieTest.COOKIE_NAME, request) == "テスト"
        CookieUtils.getCookieValue(CookieSample.COOKIE_NAME, request) == "サンプル"
    }

以下のように修正します。

    def "GetCookieValueのテスト"() {
        setup:
        def request = new MockHttpServletRequest()

        expect:
        Cookie cookieTest = new Cookie(CookieTest.COOKIE_NAME, "テスト")
        Cookie cookieSample = new Cookie(CookieSample.COOKIE_NAME, "サンプル")
        request.setCookies(cookieTest, cookieSample)
        CookieUtils.getCookieValue(CookieTest.COOKIE_NAME, request).get() == "テスト"
        CookieUtils.getCookieValue(CookieSample.COOKIE_NAME, request).get() == "サンプル"
        CookieUtils.getCookieValue(CookieTest.COOKIE_NAME, null) == Optional.empty()
        CookieUtils.getCookieValue("NotExistsCookie", request) == Optional.empty()
    }

最後にテストが通しで全て成功することを確認します。

f:id:ksby:20170407020451p:plain

履歴

2017/04/07
初版発行。

Spring Boot 1.3.x の Web アプリを 1.4.x へバージョンアップする ( その18 )( Gradle のバージョンを 2.13 → 3.x へバージョンアップ。。。しようと思いましたが止めました )

概要

記事一覧はこちらです。

Spring Boot 1.3.x の Web アプリを 1.4.x へバージョンアップする ( その17 )( テストクラスのモックを @MockBean + Mockito で作り直す2 ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • 現在プロジェクトで使用している Gradle のバージョンは 2.13 ですが、3系に上げると処理スピードが上がるそうなので 3.x へバージョンアップします。。。と思いましたが止めました、という話です。

参照したサイト・書籍

  1. Gradle Goodness: Replacing << Operator For Tasks
    http://mrhaki.blogspot.jp/2016/11/gradle-goodness-replacing-operator-for.html

  2. Deprecation warnings
    http://stackoverflow.com/questions/41619040/deprecation-warnings

  3. Gradle logs a non-fatal error message with FindBugs https://github.com/gradle/gradle/issues/1094

目次

  1. 3系のどのバージョンにするか?
  2. Gradle を 3.3 へバージョンアップする
  3. The Task.leftShift(Closure) method has been deprecated and is scheduled to be removed in Gradle 5.0. Please use Task.doLast(Action) instead.
  4. FindBugs の Cannot open codebase filesystem:...
  5. 今回のバージョンアップの変更は全て取り消します

手順

3系のどのバージョンにするか?

Gradle の Installation のページを見ると最新バージョンは 3.4.1 ですが、IntelliJ IDEA 2017.1 で Gradle プロジェクトを作成してみると、使用されているバージョンは 3.3 でした。

f:id:ksby:20170404013243p:plain

ビルドツールは安定性を求めたいので、IDEA で使用されている 3.3 にバージョンアップします。

Gradle を 3.3 へバージョンアップする

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

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

    f:id:ksby:20170404014234p:plain

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

    f:id:ksby:20170404014435p:plain

  3. Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。いつもより少し時間がかかりますが、エラーが出ずに終了しました。

  4. clean タスク実行 → Rebuild Project → build タスクを実行してみます。

    最後に BUILD SUCCESSFUL のメッセージが出力されてタスクは成功したようですが、以下のメッセージも出力されていました。メッセージが出ないようにします。

    • The Task.leftShift(Closure) method has been deprecated and is scheduled to be removed in Gradle 5.0. Please use Task.doLast(Action) instead. というエラーメッセージが出ます。build.gradle 内の task domaGen << {<< が deprecated のようです。
    • FindBugs の出力が変わりました。しかも大量に Cannot open codebase filesystem:... というメッセージが出力されました。

    f:id:ksby:20170404015744p:plain f:id:ksby:20170404015922p:plain

The Task.leftShift(Closure) method has been deprecated and is scheduled to be removed in Gradle 5.0. Please use Task.doLast(Action) instead.

Gradle Goodness: Replacing << Operator For Tasks を見ると、以下のように変更すればよいようです。

task domaGen << {
    ..........
}

↓↓↓

task domaGen {
    doLast {
        ..........
    }
}

Gradle の User Guide の Chapter 19. More about Tasks も見つけましたが、こちらも doLast { ... } の記述になっていました。この書き方へ変更します。

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

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

  3. 実行しても影響が出ない printClassWhatNotMakeTest タスクを実行してみると特にエラーは出ずに終了しました。

    f:id:ksby:20170404023504p:plain

  4. clean タスク → Rebuild Project → build タスクを実行してみると、起動時のメッセージは出なくなりました。

    f:id:ksby:20170404023958p:plain

FindBugsCannot open codebase filesystem:...

調べると gradle/gradle の Issue に Gradle logs a non-fatal error message with FindBugs というものがありました。3.3 からロギングの仕組みを一部変更したのがメッセージが出るようになった原因のようです。また debug レベルのメッセージが出ているだけで問題はないらしい(確かに BUILD SUCCESSFUL は出ていました)。

でもこの Issue はまだクローズされておらず、解決はしていないようです。エラーでないとはいえ、メッセージが大量に出るものはさすがに使いたくないですね。。。

gradle/gradleReleases のページを見ると #1646: Noisy Findbugs plugin with quiet log level with Gradle 3.5 RC1 という記述がありますが、中身を見ると該当するようなしないような。。。 ただし tbroyer/gradle-errorprone-plugin のページを見ると、現在使用している 0.0.9 は 3.4 までしか対応していないようです。

試しに 3.4.1 にバージョンアップして clean タスク → Rebuild Project → build タスクを実行してみましたが、FindBugs が大量にメッセージを出力する現象は変わりませんでした。

3.5-rc-3 にバージョンアップすると今度は gradle-errorprone-plugin でエラーになります。

今回のバージョンアップの変更は全て取り消します

3.2 にすれば FindBugs のメッセージが出る現象はなくなるようなのですが、2 系でも特に不満がないので今回は 2.13 に戻すことにします。3 系にバージョンアップするのはもう少し様子を見てからにします。

ソースコード

build.gradle

■その1

sourceCompatibility = 1.8
targetCompatibility = 1.8

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

■その2

// for Doma-Gen
task domaGen {
    doLast {
        ..........
    }
}

task downloadCssFontsJs {
    doLast {
        ..........
    }
}

task printClassWhatNotMakeTest {
    doLast {
        ..........
    }
}

履歴

2017/04/05
初版発行。

Spring Boot 1.3.x の Web アプリを 1.4.x へバージョンアップする ( その17 )( テストクラスのモックを @MockBean + Mockito で作り直す2 )

概要

記事一覧はこちらです。

Spring Boot 1.3.x の Web アプリを 1.4.x へバージョンアップする ( その16 )( テストクラスのモックを @MockBean + Mockito で作り直す ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • LendingUserDetailsHelper に @Component アノテーションを付加して Bean にし、static メソッドを止めます。
    • LendingUserDetailsHelper を使用しているクラスを修正します。
    • その後で前回修正できなかった BooklistServiceTest.java を修正します。

参照したサイト・書籍

目次

  1. LendingUserDetailsHelper.java の @Component アノテーションを付加して Bean にする
  2. LendingUserDetailsHelper を使用しているクラスを修正する
  3. BooklistServiceTest.java を修正する
  4. build.gradle から JMockit を外す
  5. 全てのテストを実行してみる
  6. 次回は。。。

手順

LendingUserDetailsHelper.java の @Component アノテーションを付加して Bean にする

  1. src/main/java/ksbysample/webapp/lending/security/LendingUserDetailsHelper.javaリンク先の内容 に変更します。

LendingUserDetailsHelper を使用しているクラスを修正する

  1. src/main/java/ksbysample/webapp/lending/security/LendingUserDetailsHelper.java の getLoginUserId メソッドにカーソルを移動して右クリックしてコンテキストメニューを表示した後、「Find Usages」を選択します。

    f:id:ksby:20170402182817p:plain

  2. 画面下に使用箇所一覧が表示されますので、以下のように修正します。

    f:id:ksby:20170402183020p:plain

    • フィールド変数 private final LendingUserDetailsHelper lendingUserDetailsHelper; を追加し、コンストラクタインジェクションの処理も追加します。ただしテストクラスの場合には @Autowired private LendingUserDetailsHelper lendingUserDetailsHelper; を追加し、コンストラクタインジェクションにはしません。
    • LendingUserDetailsHelper#getLoginUserId を呼び出している箇所の記述を LendingUserDetailsHelper.getLoginUserId()lendingUserDetailsHelper.getLoginUserId() に変更します。

    修正したのは以下のソースです。

    • src/main/java/ksbysample/webapp/lending/web/booklist/BooklistService.java
    • src/main/java/ksbysample/webapp/lending/web/confirmresult/ConfirmresultController.java
    • src/main/java/ksbysample/webapp/lending/web/lendingapproval/LendingapprovalService.java
    • src/test/java/ksbysample/webapp/lending/security/LendingUserDetailsHelperTest.java

BooklistServiceTest.java を修正する

  1. src/test/java/ksbysample/webapp/lending/web/booklist/BooklistServiceTest.javaリンク先の内容 に変更します。

  2. BooklistServiceTest クラスのテストのみ実行し、成功することを確認します。

    f:id:ksby:20170402185442p:plain

build.gradle から JMockit を外す

  1. build.gradle の dependencies から testCompile("org.jmockit:jmockit:1.30") を削除します。

全てのテストを実行してみる

  1. まずは clean タスク → Rebuild Project → build タスクを実行して正常に終了することを確認します。

    f:id:ksby:20170402192245p:plain

  2. 次に Project Tool Window の src/test から「Run ‘All Tests’ with Coverage」を実行して、全てのテストが成功することを確認します。

    f:id:ksby:20170402192642p:plain

次回は。。。

今回のテストクラスの対応で 1.4 系へのバージョンアップの作業自体はほぼ終了ですが、他にやっておきたいものを書き上げてみました。やりたいものから進めてみます。

  • Spring Boot のバージョンを 1.4.4 → 1.4.5 に上げます。
  • application.properties に spring.datasource.tomcat の最低限の設定しか記述していないので、設定を見直してみます。
  • Log4jdbc Spring Boot Starter というライブラリが出ていました。興味があるので試してみたいと思います。
  • Gradle のバージョンが 2.13 なので 3.x へ上げます。
  • 他に気になっている点を修正します。
    • FreeMarkerUtils は内容が Helper クラスなので、クラス名やパッケージを変更します。
    • application.properties から hibernate.dialect, spring.jpa.~ を削除します。
    • ValidationMessages_ja_JP.properties をやめて messages.properties に1本化し、かつ IDEA の Transparent native-to-ascii conversion のチェックを外して UTF-8 の文字列のまま保存されるようにします。
    • Hibernate を無効にしているので hibernate.properties は不要な気がするので削除してみます。

ソースコード

LendingUserDetailsHelper.java

@Component
public class LendingUserDetailsHelper {

    /**
     * 現在ログインしているユーザのユーザIDを取得する
     *
     * @return ユーザID(user_info.user_id)
     */
    public Long getLoginUserId() {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        LendingUserDetails lendingUserDetails = (LendingUserDetails) auth.getPrincipal();
        return lendingUserDetails.getUserId();
    }

}
  • クラスに @Component アノテーションを付加します。
  • getLoginUserId メソッドから static を削除します。

BooklistServiceTest.java

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

    private static final String MAILADDR_TANAKA_TARO = "tanaka.taro@sample.com";

    @Rule
    @Autowired
    public TestDataResource testDataResource;

    @Autowired
    private DataSource dataSource;

    @Autowired
    private BooklistService booklistService;

    @MockBean
    private LendingUserDetailsHelper lendingUserDetailsHelper;

    @Test
    public void testTemporarySaveBookListCsvFile() throws Exception {
        given(lendingUserDetailsHelper.getLoginUserId()).willReturn(1L);

        UploadBooklistForm uploadBooklistForm = new UploadBooklistForm();
        // テスト用のユーティリティクラスを作るべきですが、今回は他のテストクラスのメソッドをそのまま使います
        BooklistCsvFileServiceTest booklistCsvFileServiceTest = new BooklistCsvFileServiceTest();
        uploadBooklistForm.setFileupload(booklistCsvFileServiceTest.createNoErrorCsvFile());

        ..........
  • @MockBean private LendingUserDetailsHelper lendingUserDetailsHelper; を追加します。
  • testTemporarySaveBookListCsvFile メソッド内の処理を new Expectations(LendingUserDetailsHelper.class) {{ ... }};given(lendingUserDetailsHelper.getLoginUserId()).willReturn(1L); へ変更します。

履歴

2017/04/04
初版発行。

Spring Boot 1.3.x の Web アプリを 1.4.x へバージョンアップする ( その16 )( テストクラスのモックを @MockBean + Mockito で作り直す )

概要

記事一覧はこちらです。

Spring Boot 1.3.x の Web アプリを 1.4.x へバージョンアップする ( その15 )( テストクラスのアノテーションを 1.4 のものに変更する ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • 現在テストクラスでモックを使いたい場合 JMockit を利用して作成していますが、Spring Boot 1.4 で提供されている @MockBean アノテーションで作り直してみます。

参照したサイト・書籍

  1. Spring Boot Reference Guide - 40.3.4 Mocking and spying beans
    http://docs.spring.io/spring-boot/docs/1.4.x/reference/htmlsingle/#boot-features-testing-spring-boot-applications-mocking-beans

  2. Mockito
    http://site.mockito.org/

  3. Mocking static methods with Mockito
    http://stackoverflow.com/questions/21105403/mocking-static-methods-with-mockito

  4. How can I mock a private static method with PowerMockito?
    http://stackoverflow.com/questions/31796736/how-can-i-mock-a-private-static-method-with-powermockito

  5. powermock/powermock - MockitoUsage
    https://github.com/powermock/powermock/wiki/MockitoUsage

  6. Can Mockito stub a method without regard to the argument?
    http://stackoverflow.com/questions/5969630/can-mockito-stub-a-method-without-regard-to-the-argument

目次

  1. @MockBean アノテーションは Mockito を使うが、バージョンは 2 ではなく 1 らしい
  2. 変更手順を考える
  3. build.gradle を修正する
  4. clean タスク → Rebuild Project を実行して修正対象のソースを洗い出す
  5. LibraryHelperTest.java を修正する
  6. BooklistServiceTest.java を修正する
  7. InquiringStatusOfBookQueueListenerTest.java を修正する
  8. 全てのテストを実行してみる
  9. 感想&次回は。。。

手順

@MockBean アノテーションは Mockito を使うが、バージョンは 2 ではなく 1 らしい

40.3.4 Mocking and spying beans を読むと @MockBean は Mockito を利用してモックを作成するようです。

Mockito のホームページ を見ると “Current version is 2” と記述されており、jcenter で検索すると最新バージョンは 2.7.20 と表示されるのですが、Spring Boot Reference Guide の Appendix F. Dependency versions を見ると mockito-core のバージョンは 1.10.19 が記述されており、Athens-SR3 の Spring IO Platform Reference Guide の Appendix A. Dependency versions でも 1.10.19 でした。

Mockito のバージョン 2 系は使用できないのかな?と思って調べると、Spring Boot の以下の Issue が見つかりました。2 系が使えるのは 1.5 以降のようです。今回は 1 系を使うことにします。

変更手順を考える

以下の手順で変更していきます。

  • build.gradle を編集して JMockit を外して Mockito を追加します。
  • clean タスク → Rebuild Project を実行してエラーが出る箇所 ( JMockit を利用している箇所 ) を洗い出します。
  • 一旦 build.gradle に JMockit を戻します。
  • エラーが出た箇所を @MockBean + Mockito で書き直します。
  • build.gradle から JMockit を外します。
  • 最後に全てのテストを通しで実行して成功することを確認します。

build.gradle を修正する

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

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

clean タスク → Rebuild Project を実行して修正対象のソースを洗い出す

clean タスク → Rebuild Project を実行すると以下のソースでエラーが出ました。

  • src/test/java/ksbysample/webapp/lending/helper/library/LibraryHelperTest.java
  • src/test/java/ksbysample/webapp/lending/web/booklist/BooklistServiceTest.java
  • src/test/java/ksbysample/webapp/lending/listener/rabbitmq/InquiringStatusOfBookQueueListenerTest.java

1つずつ見ていきます。

build.gradle は testCompile("org.jmockit:jmockit:1.30")コメントアウトを一旦元に戻して JMockit が入っている状態にします。

LibraryHelperTest.java を修正する

テストクラスは JMockit を使用して以下のように実装されています。

package ksbysample.webapp.lending.helper.library;

import ksbysample.webapp.lending.dao.LibraryForsearchDao;
import ksbysample.webapp.lending.entity.LibraryForsearch;
import mockit.Delegate;
import mockit.Expectations;
import mockit.Injectable;
import mockit.Tested;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import static org.assertj.core.api.Assertions.assertThat;

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

    @Tested
    private LibraryHelper libraryHelper;

    @Injectable
    private LibraryForsearchDao libraryForsearchDao;

    @Test
    public void testGetSelectedLibrary_図書館が選択されていない場合() throws Exception {
        new Expectations() {{
            libraryForsearchDao.selectSelectedLibrary();
            result = null;
        }};

        String result = libraryHelper.getSelectedLibrary();
        assertThat(result).isEqualTo("※図書館が選択されていません");
    }

    @Test
    public void testGetSelectedLibrary_図書館が選択されている場合() throws Exception {
        new Expectations() {{
            libraryForsearchDao.selectSelectedLibrary();
            result = new Delegate<LibraryForsearch>() {
                LibraryForsearch aDelegateMethod() {
                    LibraryForsearch libraryForsearch = new LibraryForsearch();
                    libraryForsearch.setSystemid("System_Id");
                    libraryForsearch.setFormal("図書館名");
                    return libraryForsearch;
                }
            };
        }};

        String result = libraryHelper.getSelectedLibrary();
        assertThat(result).isEqualTo("選択中:図書館名");
    }

}

これを @MockBean アノテーションを使用して以下のように変更します。

package ksbysample.webapp.lending.helper.library;

import ksbysample.webapp.lending.dao.LibraryForsearchDao;
import ksbysample.webapp.lending.entity.LibraryForsearch;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.junit4.SpringRunner;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;

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

    @Autowired
    private LibraryHelper libraryHelper;

    @MockBean
    private LibraryForsearchDao libraryForsearchDao;

    @Test
    public void testGetSelectedLibrary_図書館が選択されていない場合() throws Exception {
        given(libraryForsearchDao.selectSelectedLibrary()).willReturn(null);

        String result = libraryHelper.getSelectedLibrary();
        assertThat(result).isEqualTo("※図書館が選択されていません");
    }

    @Test
    public void testGetSelectedLibrary_図書館が選択されている場合() throws Exception {
        LibraryForsearch libraryForsearch = new LibraryForsearch();
        libraryForsearch.setSystemid("System_Id");
        libraryForsearch.setFormal("図書館名");
        given(libraryForsearchDao.selectSelectedLibrary()).willReturn(libraryForsearch);

        String result = libraryHelper.getSelectedLibrary();
        assertThat(result).isEqualTo("選択中:図書館名");
    }

}
  • private LibraryHelper libraryHelper;アノテーション@Tested@Autowired へ変更します。
  • private LibraryForsearchDao libraryForsearchDao;アノテーション@Injectable@MockBean へ変更します。
  • testGetSelectedLibrary_図書館が選択されていない場合() テストメソッド内のモック化の処理を new Expectations() {{ ... }};given(libraryForsearchDao.selectSelectedLibrary()).willReturn(null); へ変更します。
  • testGetSelectedLibrary_図書館が選択されている場合() テストメソッド内のモック化の処理を new Expectations() {{ ... }};given(libraryForsearchDao.selectSelectedLibrary()).willReturn(libraryForsearch); へ変更します。

LibraryHelperTest クラスのテストのみ実行してみると、全てのテストが成功しました。

f:id:ksby:20170401091222p:plain

BooklistServiceTest.java を修正する

テストクラスは JMockit を使用して以下のように実装されています。

package ksbysample.webapp.lending.web.booklist;

import ksbysample.common.test.rule.db.TableDataAssert;
import ksbysample.common.test.rule.db.TestDataResource;
import ksbysample.webapp.lending.entity.LendingBook;
import ksbysample.webapp.lending.security.LendingUserDetailsHelper;
import ksbysample.webapp.lending.service.file.BooklistCsvFileServiceTest;
import mockit.Expectations;
import org.dbunit.dataset.IDataSet;
import org.dbunit.dataset.csv.CsvDataSet;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

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

import static org.assertj.core.api.Assertions.assertThat;

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

    private static final String MAILADDR_TANAKA_TARO = "tanaka.taro@sample.com";

    @Rule
    @Autowired
    public TestDataResource testDataResource;

    @Autowired
    private DataSource dataSource;

    @Autowired
    private BooklistService booklistService;

    @Test
    public void testTemporarySaveBookListCsvFile() throws Exception {
        new Expectations(LendingUserDetailsHelper.class) {{
            LendingUserDetailsHelper.getLoginUserId();
            result = Long.valueOf(1L);
        }};

        UploadBooklistForm uploadBooklistForm = new UploadBooklistForm();
        // テスト用のユーティリティクラスを作るべきですが、今回は他のテストクラスのメソッドをそのまま使います
        BooklistCsvFileServiceTest booklistCsvFileServiceTest = new BooklistCsvFileServiceTest();
        uploadBooklistForm.setFileupload(booklistCsvFileServiceTest.createNoErrorCsvFile());

        Long lendingAppId = booklistService.temporarySaveBookListCsvFile(uploadBooklistForm);
        assertThat(lendingAppId).isNotEqualTo(Long.valueOf(0L));
        IDataSet dataSet = new CsvDataSet(
                new File("src/test/resources/ksbysample/webapp/lending/web/booklist/assertdata/001"));
        TableDataAssert tableDataAssert = new TableDataAssert(dataSet, dataSource);
        tableDataAssert.assertEquals("lending_app"
                , new String[]{"lending_app_id", "approval_user_id", "version"});
        tableDataAssert.assertEquals("lending_book"
                , new String[]{"lending_book_id", "lending_app_id", "lending_state", "lending_app_flg"
                        , "lending_app_reason", "approval_result", "approval_reason", "version"});

        List<LendingBook> lendingBookList = booklistService.getLendingBookList(lendingAppId);
        assertThat(lendingBookList).hasSize(5);
    }

    @Test
    public void testSendMessageToInquiringStatusOfBookQueue() throws Exception {
        // 現在の実装では InquiringStatusOfBookQueueServiceTest が通ればOKなので、こちらは実装しない
    }

}

よく見たらモックにしている LendingUserDetailsHelper.getLoginUserId(); は static メソッドでした。調べてみると Mockito では static メソッドはモックにできないので、対応するとすれば以下のいずれかの方法になるようです。

  • PowerMock を使用する。
  • LendingUserDetailsHelper に @Component アノテーションを付加して Bean にし、LendingUserDetailsHelper#getLoginUserId メソッドから static を取り除く。

Helper クラスなのに static メソッドにしていたのか。。。ということに気付いたので、出来れば後者の方法にしてしまいたいところですが、PowerMock を使ってみたいので、前者の方法で対応してみます。

まず build.gradle をリンク先のその2内容 に変更し、変更後、Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。

BooklistServiceTest.java を以下のように変更します。

@RunWith(PowerMockRunner.class)
@PowerMockRunnerDelegate(SpringRunner.class)
@SpringBootTest
@PrepareForTest(LendingUserDetailsHelper.class)
public class BooklistServiceTest {

    ..........

    @Test
    public void testTemporarySaveBookListCsvFile() throws Exception {
        PowerMockito.mockStatic(LendingUserDetailsHelper.class);
        given(LendingUserDetailsHelper.getLoginUserId()).willReturn(1L);

        UploadBooklistForm uploadBooklistForm = new UploadBooklistForm();
  • @RunWith(SpringRunner.class)@RunWith(PowerMockRunner.class) + @PowerMockRunnerDelegate(SpringRunner.class) へ変更します。
  • @PrepareForTest(LendingUserDetailsHelper.class) を class に付加します。
  • testTemporarySaveBookListCsvFile メソッド内で new Expectations(LendingUserDetailsHelper.class) {{ ... }};PowerMockito.mockStatic(LendingUserDetailsHelper.class); + given(LendingUserDetailsHelper.getLoginUserId()).willReturn(1L); へ変更します。

BooklistServiceTest クラスのテストのみ実行してみると、java.lang.IllegalStateException: Failed to load ApplicationContext のエラーメッセージが表示されてテストが失敗しました。。。

f:id:ksby:20170401184410p:plain

Web で調べたり、コードをいろいろ変えて試してみましたが、テストが成功しません。stackoverflow を見ると上記のアノテーションの組み合わせで PowerMock が使えている人もいるようですが、正直原因が全然分かりません。。。 一旦元に戻して(build.gradle の powermock の記述も削除します)、次のファイルを見ることにします。

InquiringStatusOfBookQueueListenerTest.java を修正する

テストクラスは JMockit を使用して以下のように実装されています。

package ksbysample.webapp.lending.listener.rabbitmq;

import com.google.common.base.Charsets;
import ksbysample.common.test.rule.db.TableDataAssert;
import ksbysample.common.test.rule.db.TestData;
import ksbysample.common.test.rule.db.TestDataResource;
import ksbysample.common.test.rule.mail.MailServerResource;
import ksbysample.webapp.lending.dao.LibraryForsearchDao;
import ksbysample.webapp.lending.entity.LibraryForsearch;
import ksbysample.webapp.lending.service.calilapi.CalilApiService;
import ksbysample.webapp.lending.service.calilapi.response.Book;
import ksbysample.webapp.lending.service.calilapi.response.Libkey;
import ksbysample.webapp.lending.service.calilapi.response.SystemData;
import ksbysample.webapp.lending.service.queue.InquiringStatusOfBookQueueMessage;
import mockit.Mock;
import mockit.MockUp;
import org.dbunit.dataset.IDataSet;
import org.dbunit.dataset.csv.CsvDataSet;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import javax.mail.internet.MimeMessage;
import javax.sql.DataSource;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import static mockit.Deencapsulation.getField;
import static mockit.Deencapsulation.setField;
import static org.assertj.core.api.Assertions.assertThat;

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

    @Rule
    @Autowired
    public TestDataResource testDataResource;

    @Rule
    @Autowired
    public MailServerResource mailServer;

    @Autowired
    private DataSource dataSource;

    @Autowired
    private MessageConverter messageConverter;

    @Autowired
    private InquiringStatusOfBookQueueListener listener;

    @Test
    @TestData("listener/rabbitmq/testdata/001")
    public void testReceiveMessage() throws Exception {
        // モックに入れ替える前のフィールドの実体を退避する
        LibraryForsearchDao libraryForsearchDaoOrg = getField(listener, "libraryForsearchDao");
        CalilApiService calilApiServiceOrg = getField(listener, "calilApiService");
        try {
            /**
             * モック定義部
             */
            // InquiringStatusOfBookQueueListener.libraryForsearchDao をモックに入れ替える
            LibraryForsearchDao libraryForsearchDao = new MockUp<LibraryForsearchDao>() {
                @Mock
                LibraryForsearch selectSelectedLibrary() {
                    LibraryForsearch libraryForsearch = new LibraryForsearch();
                    libraryForsearch.setSystemid("System_Id");
                    libraryForsearch.setFormal("図書館名");
                    return libraryForsearch;
                }
            }.getMockInstance();
            setField(listener, "libraryForsearchDao", libraryForsearchDao);

            // InquiringStatusOfBookQueueListener.calilApiService をモックに入れ替える
            CalilApiService calilApiService = new MockUp<CalilApiService>() {
                @Mock
                public List<Book> check(String systemid, List<String> isbnList) {
                    List<Book> bookList = new ArrayList<>();
                    bookList.add(new Book("978-4-7741-6366-6", null, new SystemData(null, null, null, Arrays.asList(new Libkey(null, "貸出可")))));
                    bookList.add(new Book("978-4-7741-5377-3", null, new SystemData(null, null, null, Arrays.asList(new Libkey(null, "蔵書あり")))));
                    bookList.add(new Book("978-4-7973-8014-9", null, new SystemData(null, null, null, Arrays.asList(new Libkey(null, "貸出中")))));
                    bookList.add(new Book("978-4-7973-4778-4", null, new SystemData(null, null, null, Arrays.asList(new Libkey(null, "準備中")))));
                    bookList.add(new Book("978-4-87311-704-1", null, new SystemData(null, null, null, Arrays.asList(new Libkey(null, "蔵書なし")))));
                    return bookList;
                }
            }.getMockInstance();
            setField(listener, "calilApiService", calilApiService);

            /**
             * テスト本体
             */
            InquiringStatusOfBookQueueMessage queueMessage = new InquiringStatusOfBookQueueMessage();
            queueMessage.setLendingAppId(1L);
            Message message = messageConverter.toMessage(queueMessage, new MessageProperties());
            listener.receiveMessage(message);

            /**
             * 検証
             */
            // テーブルの以下のカラムのデータを検証する
            //  ・lending_app.status
            //  ・lending_book.lending_state
            IDataSet dataSet = new CsvDataSet(new File("src/test/resources/ksbysample/webapp/lending/listener/rabbitmq/assertdata/001"));
            TableDataAssert tableDataAssert = new TableDataAssert(dataSet, dataSource);
            tableDataAssert.assertEquals("lending_app", new String[]{"lending_app_id", "lending_user_id", "approval_user_id", "version"});
            tableDataAssert.assertEquals("lending_book", new String[]{"lending_book_id", "lending_app_id", "isbn", "book_name", "lending_app_flg", "lending_app_reason", "approval_result", "approval_reason", "version"});

            // 送信されたメールを検証する
            assertThat(mailServer.getMessagesCount()).isEqualTo(1);
            MimeMessage mimeMessage = mailServer.getFirstMessage();
            assertThat(mimeMessage.getRecipients(javax.mail.Message.RecipientType.TO))
                    .extracting(Object::toString)
                    .containsOnly("tanaka.taro@sample.com");
            assertThat(mimeMessage.getContent())
                    .isEqualTo(com.google.common.io.Files.toString(
                            new File("src/test/resources/ksbysample/webapp/lending/helper/mail/assertdata/001/message.txt")
                            , Charsets.UTF_8));
        } finally {
            // モックに差し替えたフィールドを退避しておいた元の実体に戻す
            setField(listener, "libraryForsearchDao", libraryForsearchDaoOrg);
            setField(listener, "calilApiService", calilApiServiceOrg);
        }
    }

}

これを @MockBean アノテーションを使用して以下のように変更します。

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

    ..........

    @Autowired
    private InquiringStatusOfBookQueueListener listener;

    @MockBean
    private LibraryForsearchDao libraryForsearchDao;

    @MockBean
    private CalilApiService calilApiService;

    @Test
    @TestData("listener/rabbitmq/testdata/001")
    public void testReceiveMessage() throws Exception {
        /**
         * モック定義部
         */
        LibraryForsearch libraryForsearch = new LibraryForsearch();
        libraryForsearch.setSystemid("System_Id");
        libraryForsearch.setFormal("図書館名");
        given(libraryForsearchDao.selectSelectedLibrary()).willReturn(libraryForsearch);

        List<Book> bookList = new ArrayList<>();
        bookList.add(new Book("978-4-7741-6366-6", null, new SystemData(null, null, null, Arrays.asList(new Libkey(null, "貸出可")))));
        bookList.add(new Book("978-4-7741-5377-3", null, new SystemData(null, null, null, Arrays.asList(new Libkey(null, "蔵書あり")))));
        bookList.add(new Book("978-4-7973-8014-9", null, new SystemData(null, null, null, Arrays.asList(new Libkey(null, "貸出中")))));
        bookList.add(new Book("978-4-7973-4778-4", null, new SystemData(null, null, null, Arrays.asList(new Libkey(null, "準備中")))));
        bookList.add(new Book("978-4-87311-704-1", null, new SystemData(null, null, null, Arrays.asList(new Libkey(null, "蔵書なし")))));
        given(calilApiService.check(any(), any())).willReturn(bookList);

        /**
         * テスト本体
         */
        InquiringStatusOfBookQueueMessage queueMessage = new InquiringStatusOfBookQueueMessage();
        queueMessage.setLendingAppId(1L);
        Message message = messageConverter.toMessage(queueMessage, new MessageProperties());
        listener.receiveMessage(message);

        ..........
    }

}
  • @MockBean private LibraryForsearchDao libraryForsearchDao; を追加します。
  • @MockBean private CalilApiService calilApiService; を追加します。
  • // モックに入れ替える前のフィールドの実体を退避する ... try { の部分と } finally { ... } の部分を削除します。
  • libraryForsearch のデータ生成処理 + given(libraryForsearchDao.selectSelectedLibrary()).willReturn(libraryForsearch); を追加します。
  • bookList のデータ生成処理 + given(calilApiService.check(any(), any())).willReturn(bookList); を追加します。

InquiringStatusOfBookQueueListenerTest クラスのテストのみ実行してみると、全てのテストが成功しました。

f:id:ksby:20170401234026p:plain

全てのテストを実行してみる

BooklistServiceTest.java はまだ JMockit を使用したままなので build.gradle から JMockit は外しません。

今の状態で全てのテストを通しで実行して成功するか確認します。まずは clean タスク → Rebuild Project → build タスクを実行してみます。

“BUILD SUCCESSFUL” のメッセージが表示されました。問題ないようです。

f:id:ksby:20170402000224p:plain

今度は Project Tool Window の src/test から「Run ‘All Tests’ with Coverage」を実行してみます。

こちらも全てのテストが成功しました。

f:id:ksby:20170402000641p:plain

感想&次回は。。。

@MockBean がすごく使いやすいです。前にモックを作った時はいろいろできる JMockit がいいかな、と思いましたが、この使い勝手なら @MockBean + Mockito に切り替え確定です。Spring Boot 1.4 のテストアノテーションの変更ですが、本当にテストがやりやすくなっていると思います。

PowerMock を使う方法が分からなかったので static メソッドをモック化したい場合が少し不安ですが、static メソッドをモック化しないとテストできないのはそもそも作りがおかしいのでは?という気がしています。

次回は、LendingUserDetailsHelper に @Component アノテーションを付加して Bean にし、static メソッドを止めます。その後で今回修正できなかった BooklistServiceTest.java を修正します。

ソースコード

build.gradle

■その1

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

    // dependency-management-plugin によりバージョン番号が自動で設定されるもの
    // Appendix A. Dependency versions ( http://docs.spring.io/platform/docs/current/reference/htmlsingle/#appendix-dependency-versions ) 参照
    ..........
    testCompile("org.yaml:snakeyaml")
    testCompile("org.mockito:mockito-core")

    // dependency-management-plugin によりバージョン番号が自動で設定されないもの、あるいは最新バージョンを指定したいもの
    ..........
    testCompile("com.jayway.jsonpath:json-path:2.2.0")
//    testCompile("org.jmockit:jmockit:1.30")
    testCompile("org.spockframework:spock-core:${spockVersion}") {
        exclude module: "groovy-all"
    }
    ..........
  • testCompile("org.mockito:mockito-core") を追加します。
  • testCompile("org.jmockit:jmockit:1.30")コメントアウトします。

■その2

dependencies {
    ..........
    def powermockitoVersion = "1.6.6"

    ..........

    // dependency-management-plugin によりバージョン番号が自動で設定されないもの、あるいは最新バージョンを指定したいもの
    ..........
    testCompile("com.google.code.findbugs:jsr305:3.0.1")
    testCompile("org.powermock:powermock-module-junit4:${powermockitoVersion}")
    testCompile("org.powermock:powermock-api-mockito:${powermockitoVersion}")
  • def powermockitoVersion = "1.6.6" を追加します。
  • testCompile("org.powermock:powermock-module-junit4:${powermockitoVersion}") を追加します。
  • testCompile("org.powermock:powermock-api-mockito:${powermockitoVersion}") を追加します。

履歴

2017/04/02
初版発行。

IntelliJ IDEA を 2016.3.5 → 2017.1 へ、Git for Windows を 2.12.0 → 2.12.2 へバージョンアップ

IntelliJ IDEA を 2016.3.5 → 2017.1 へバージョンアップする

IntelliJ IDEA の 2017.1 がリリースされたのでバージョンアップします。

※ksbysample-webapp-lending プロジェクトを開いた状態でバージョンアップしています。

  1. IntelliJ IDEA のメインメニューから「Help」-「Check for Updates…」を選択します。

  2. 「Platform and Plugin Updates」ダイアログが表示されます。今回は左下に「Update and Restart」ボタンが表示されていませんので、「Download」ボタンをクリックします。

    f:id:ksby:20170329213306p:plain

  3. ブラウザが起動して Download IntelliJ IDEA ページが表示されますので ideaIU-2017.1.exe をダウンロードします。

  4. 起動している IntelliJ IDEA を終了します。

  5. ideaIU-2017.1.exe を実行します。

  6. IntelliJ IDEA Setup」ダイアログが表示されます。「Next >」ボタンをクリックします。

    f:id:ksby:20170329231025p:plain

  7. 「Uninstall old versions」画面が表示されます。以下の画面のように画面上のチェックボックスを全てチェックした後、「Next >」ボタンをクリックします。

    f:id:ksby:20170329231146p:plain

  8. 「Choose Install Location」画面が表示されます。「Destination Folder」を C:\IntelliJ_IDEA\2017.1 へ変更した後、「Next >」ボタンをクリックします。

    f:id:ksby:20170329231434p:plain

  9. 「Installation Options」画面が表示されます。何もチェックせずに「Next >」ボタンをクリックします。

    f:id:ksby:20170329231551p:plain

  10. 「Choose Start Menu Folder」画面が表示されます。何も変更せずに「Install」ボタンをクリックします。

    f:id:ksby:20170329231751p:plain

  11. 「Installing」画面が表示され、インストールが行われます。

  12. インストールが完了すると「Completing the IntelliJ IDEA Setup Wizard」画面が表示されます。「Finish」ボタンをクリックします。

    f:id:ksby:20170329232105p:plain

  13. C:\IntelliJ_IDEA\2016.3 フォルダが残っているので削除します。

  14. C:\IntelliJ_IDEA\2017.1\bin\idea64.exe を実行します。

  15. 「Complete Installatioin」ダイアログが表示されます。表示された設定はそのままで「OK」ボタンをクリックします。

    f:id:ksby:20170329232458p:plain

  16. IntelliJ IDEA が起動すると画面下部に「Indexing…」のメッセージが表示されますので、終了するまで待機します。

    f:id:ksby:20170329232807p:plain

  17. Gradle projects View のツリーを見ると「Tasks」の下に「other」しかない状態になっているので、左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。

    f:id:ksby:20170329233201p:plain

  18. 「other」以外に「build」等も表示された状態に戻ります。

    f:id:ksby:20170329233512p:plain

  19. C:\Users\<ユーザ名>\.IntelliJIdea2016.3 が残っているので削除します。

  20. Plugin にアップデートがないか確認します。IntelliJ IDEA のメインメニューから「Help」-「Check for Updates…」を選択します。

    今回はアップデートされた Plugin はありませんでした。

    f:id:ksby:20170329234200p:plain

  21. clean タスク実行 → Rebuild Project 実行をした後、build タスクを実行して “BUILD SUCCESSFUL” のメッセージが表示されることを確認します。

    f:id:ksby:20170329235409p:plain

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

    f:id:ksby:20170330000251p:plain

Git for Windows を 2.12.0 → 2.12.2 へバージョンアップする

Git for Windows の 2.12.2 がリリースされていたのでバージョンアップします。

  1. https://git-for-windows.github.io/ の「Download」ボタンをクリックして Git-2.12.2-64-bit.exe をダウンロードします。

  2. Git-2.12.2-64-bit.exe を実行します。

  3. 「Git 2.12.2 Setup」ダイアログが表示されます。[Next >]ボタンをクリックします。

  4. 「Select Components」画面が表示されます。全てのチェックが外れたままであることを確認した後、[Next >]ボタンをクリックします。

  5. 「Adjusting your PATH environment」画面が表示されます。中央の「Use Git from the Windows Command Prompt」が選択されていることを確認後、[Next >]ボタンをクリックします。

  6. 「Choosing HTTPS transport backend」画面が表示されます。「Use the OpenSSL library」が選択されていることを確認後、[Next >]ボタンをクリックします。

  7. 「Configuring the line ending conversions」画面が表示されます。「Checkout Windows-style, commit Unix-style line endings」が選択されていることを確認した後、[Next >]ボタンをクリックします。

  8. 「Configuring the terminal emulator to use with Git Bash」画面が表示されます。「Use Windows'default console window」が選択されていることを確認した後、[Next >]ボタンをクリックします。

  9. 「Configuring extra options」画面が表示されます。「Enable file system caching」だけがチェックされていることを確認した後、[Install]ボタンをクリックします。

  10. インストールが完了すると「Completing the Git Setup Wizard」のメッセージが表示された画面が表示されます。中央の「View ReleaseNotes.html」のチェックを外した後、「Finish」ボタンをクリックしてインストーラーを終了します。

  11. コマンドプロンプトを起動して git のバージョンが git version 2.12.2.windows.1 になっていることを確認します。

    f:id:ksby:20170330005133p:plain

  12. git-cmd.exe を起動して日本語の表示・入力が問題ないかを確認します。

    f:id:ksby:20170330005326p:plain

  13. 特に問題はないようですので、2.12.2 で作業を進めたいと思います。