かんがるーさんの日記

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

Spring Boot 1.4.x の Web アプリを 1.5.x へバージョンアップする ( その10 )( 起動時の spring.profiles.active のチェック処理を Set.contains を使用した方法に変更する )

概要

記事一覧こちらです。

Spring Boot 1.4.x の Web アプリを 1.5.x へバージョンアップする ( その9 )( Spring Boot を 1.5.3 → 1.5.4 にバージョンアップする ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • IntelliJ IDEA 2017.2 Public Preview の記事が出ていたので読んでみたのですが、その中の「Replacing multiple equals with Set.contains」を見て、Application クラスで spring.profiles.active をチェックしている処理に適用できそうなので、変更することにします。

参照したサイト・書籍

  1. IntelliJ IDEA 2017.2 Public Preview
    https://blog.jetbrains.com/idea/2017/06/intellij-idea-2017-2-public-preview/

目次

  1. Application クラスの spring.profiles.active のチェック処理を Set.contains を使用した方法に変更する

手順

Application クラスの spring.profiles.active のチェック処理を Set.contains を使用した方法に変更する

src/main/java/ksbysample/webapp/lending/Application.java を以下のように変更します。

public class Application {

    private static final Set<String> springProfiles = Collections
            .unmodifiableSet(new HashSet<>(Arrays.asList("product", "develop", "unittest")));

    /**
     * Spring Boot メインメソッド
     *
     * @param args ???
     */
    public static void main(String[] args) {
        String springProfilesActive = System.getProperty("spring.profiles.active");
        if (!springProfiles.contains(springProfilesActive)) {
            throw new UnsupportedOperationException(
                    MessageFormat.format("JVMの起動時引数 -Dspring.profiles.active で "
                                    + "develop か unittest か product を指定して下さい ( -Dspring.profiles.active={0} )。"
                            , springProfilesActive));
        }

        SpringApplication.run(Application.class, args);
    }

}
  • private static final Set<String> springProfiles = Collections.unmodifiableSet(new HashSet<>(Arrays.asList("product", "develop", "unittest"))); を追加します。
  • main メソッド内の if 文で springProfilesActive を StringUtils.equals でチェックしていた処理を !springProfiles.contains(springProfilesActive) に変更します。

動作を確認してみます。bootRun タスクで Tomcat を起動してみると、正常に起動します。

f:id:ksby:20170623061241p:plain

Tomcat を停止した後、build.gradle の bootRun に記述している spring.profiles.active を developdevelopx に変更してから再度 bootRun で Tomcat を起動しようとしてみると、

bootRun {
    jvmArgs = ['-Dspring.profiles.active=developx']
}

今度はエラーになって起動しませんでした。チェックは正常に機能しているようです。

f:id:ksby:20170623061602p:plain

clean タスク → Rebuild Project → build タスクを実行すると “BUILD SUCCESSFUL” のメッセージが出力されることも確認できます。

f:id:ksby:20170623062244p:plain

IntelliJ IDEA 2017.2 は「Spring Boot: dashboard & actuator endpoints」が一番期待している機能なのですが、他にもいろいろ面白そうだったり、よく分からない機能があって、リリースが楽しみです。でも GIFアニメーションがちょっと速すぎて、もう少しゆっくり見たいんだけどな、とも思いました。。。

履歴

2017/06/23
初版発行。

Spring Boot 1.4.x の Web アプリを 1.5.x へバージョンアップする ( 番外編 )( Groovy + JUnit4 でテストを書いてみる、Groovy SQL を使ってみる )

概要

記事一覧こちらです。

Groovy でテストを書く場合 Spock を使用していますが、Spring Boot 1.4.x の Web アプリを 1.5.x へバージョンアップする ( 番外編 )( static メソッドをモック化してテストするには? ) で JUnit4 形式でも書けることに気付いたので、Groovy + JUnit4 で書く場合のサンプルを作ってみます。

また Spock をもっと使えるようにしたいと思い、最近以下の本を購入して読んでいるのですが、

Java Testing With Spock

Java Testing With Spock

Spock: Up and Running: Writing Expressive Tests in Java and Groovy

Spock: Up and Running: Writing Expressive Tests in Java and Groovy

Groovy SQL というものがあることを知りました。DbUnit もいいのですが、DB のデータを手軽に確認したい場合には少し手間がかかるので、Groovy SQL を利用して簡単に確認する方法もサンプルに入れてみます。

参照したサイト・書籍

  1. groovy.sql - Class Sql
    http://docs.groovy-lang.org/latest/html/api/groovy/sql/Sql.html

  2. Groovyで楽にSQLを実行してみよう
    https://www.slideshare.net/simosako/db-16699219

    • Groovy SQL の使い方が分かりやすくまとまっていて非常に参考になりました。

目次

  1. build.gradle を修正して groovy-all を依存関係に追加する
  2. Java + JUnit4 ではなく Groovy + JUnit4 でテストを書いた時に便利な点とは
    1. Groovy では .class は不要
    2. テストメソッド名を “” で囲んで自由に書ける
    3. テストメソッドに throws Exception の記述が不要
    4. Spcok でなくてもテストメソッド内で given:, when:, then:setup:, expect: が書ける(単なるコメントとしてだけのようですが)
    5. Spock の時と同様に PowerAssert の分かりやすいレポートが出せる
  3. Groovy + JUnit4 + Groovy SQL のサンプルを書いてみる
  4. Groovy SQL、IntelliJ IDEA + Groovy SQL で書くと便利な点とは
    1. SQL 文に IntelliJ IDEA の SQL の Code Style が適用される
    2. なぜかメソッドの引数の SQL 文に、テーブル名、カラム名等の補完が効く
    3. SQL 文を書く時にヒアドキュメントが使える
  5. 最後に

手順

build.gradle を修正して groovy-all を依存関係に追加する

以前コンパイルした時にエラーが出たため build.gradle で groovy-all を除外していたのですが、このままでは groovy SQL が使えませんでした。使えるようにするために groovy-all を依存関係に追加します。

まず dependencies の testCompile("org.spockframework:spock-core:${spockVersion}"), testCompile("org.spockframework:spock-spring:${spockVersion}") のところに書いていた { exclude module: "groovy-all" } を削除します。

dependencies {

    ..........

    testCompile("org.spockframework:spock-core:${spockVersion}")
    testCompile("org.spockframework:spock-spring:${spockVersion}")

    ..........

}

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

更新後 clean タスク → Rebuild Project を実行すると以下のエラーが出ます。

f:id:ksby:20170617154842p:plain

以前はこれを見て groovy-all の方を除外してしまったのですが、groovy 方を除外するようにします。コマンドラインから gradlew dependencies を実行して出力された結果を見ると、org.codehaus.groovy:groovy を依存関係に入れているのは org.springframework.boot:spring-boot-starter-thymeleaf でした。

+--- org.springframework.boot:spring-boot-starter-thymeleaf: -> 1.5.4.RELEASE
|    +--- org.springframework.boot:spring-boot-starter:1.5.4.RELEASE (*)
|    +--- org.springframework.boot:spring-boot-starter-web:1.5.4.RELEASE (*)
|    +--- org.thymeleaf:thymeleaf-spring4:2.1.5.RELEASE -> 3.0.6.RELEASE
|    |    +--- org.thymeleaf:thymeleaf:3.0.6.RELEASE
|    |    |    +--- ognl:ognl:3.1.12
|    |    |    |    \--- org.javassist:javassist:3.20.0-GA -> 3.21.0-GA
|    |    |    +--- org.attoparser:attoparser:2.0.4.RELEASE
|    |    |    +--- org.unbescape:unbescape:1.1.4.RELEASE
|    |    |    \--- org.slf4j:slf4j-api:1.6.6 -> 1.7.25
|    |    \--- org.slf4j:slf4j-api:1.6.6 -> 1.7.25
|    \--- nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect:1.4.0 -> 2.2.2
|         +--- nz.net.ultraq.thymeleaf:thymeleaf-expression-processor:1.1.3
|         |    +--- org.codehaus.groovy:groovy:2.4.6 -> 2.4.11
|         |    \--- org.thymeleaf:thymeleaf:3.0.0.RELEASE -> 3.0.6.RELEASE (*)
|         +--- org.codehaus.groovy:groovy:2.4.6 -> 2.4.11
|         \--- org.thymeleaf:thymeleaf:3.0.0.RELEASE -> 3.0.6.RELEASE (*)

build.gradle の compile("org.springframework.boot:spring-boot-starter-thymeleaf") の記述を以下のように変更します。

dependencies {

    ..........

    compile("org.springframework.boot:spring-boot-starter-thymeleaf") {
        exclude group: "org.codehaus.groovy", module: "groovy"
    }

    ..........

}

変更した後、Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新してから、clean タスク → Rebuild Project を実行するとエラーが出なくなります。また build タスクをすると “BUILD SUCCESSFUL” が出力されることも確認できます。

f:id:ksby:20170617160412p:plain

Java + JUnit4 ではなく Groovy + JUnit4 でテストを書いた時に便利な点とは

Groovy で書くのでいろいろシンプルになりますが、特に便利と思った点をメモしておきます。

Groovy では .class は不要

Groovy では .class を書く必要がありません。例えば @RunWith(Enclosed), @RunWith(SpringRunner) のように書けます。

@RunWith(Enclosed)
class SampleTest {

    @RunWith(SpringRunner)
    @SpringBootTest
    static class テストクラス {

        @Test
        void "Groovy + JUnit4 + Groovy SQL のテストサンプル1"() {

        }

    }

}

テストメソッド名を “” で囲んで自由に書ける

Java + JUnit4 では使えなかった ‘.’, ‘-’ や半角スペース等が自由に記述できます。

        @Test
        void "テストメソッド名に .,- () 等も使えます"() {

        }

テストメソッドに throws Exception の記述が不要

上下のサンプルを見ると分かるように、Java + JUnit4 の時にはテストメソッドに付けていた throws Exception を書く必要がなくなります。

Spcok でなくてもテストメソッド内で given:, when:, then:setup:, expect: が書ける(単なるコメントとしてだけのようですが)

given:, when:, then: の記述は Spock だから出来るものと思っていたのですが、Groovy + JUnit4 の場合でも問題なく書けます。

        @Test
        void "Groovy + JUnit4 + Groovy SQL のテストサンプル1"() {
            setup: "データを追加する"

            expect: "データが追加されているか確認する"
        }

Spock の時と同様に PowerAssert の分かりやすいレポートが出せる

※実例はこの後に書きます。

Groovy + JUnit4 + Groovy SQL のサンプルを書いてみる

以下のような処理をするテストを2パターン書きます。

  1. TestDataResource クラスが指定されたテーブルのバックアップを取得した後、用意されたデータをロードします。
  2. Groovy SQL でテーブルにデータを追加します。
  3. Groovy SQL でテーブルからデータを取得して検証します。
  4. TestDataResource クラスがバックアップのデータをリストアします。
package ksbysample.webapp.lending

import groovy.sql.Sql
import ksbysample.common.test.rule.db.BaseTestData
import ksbysample.common.test.rule.db.TestDataResource
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.experimental.runners.Enclosed
import org.junit.runner.RunWith
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.test.context.junit4.SpringRunner

import javax.sql.DataSource

@RunWith(Enclosed)
class SampleTest {

    // テストクラス内で共通で使用する定数を定義する
    class TestConst {
        static def TEST_ROLE_01 = "ROLE_USER"
        static def TEST_ROLE_02 = "ROLE_ADMIN"
    }

    @RunWith(SpringRunner)
    @SpringBootTest
    // @BaseTestData は TestDataResource クラスが使用する自作のアノテーション
    @BaseTestData("testdata/base")
    static class テストクラス {

        @Autowired
        private DataSource dataSource

        // 自作のテーブルのバックアップ・リストア用クラス
        @Rule
        @Autowired
        public TestDataResource testDataResource

        Sql sql

        @Before
        void setUp() {
            sql = new Sql(dataSource)
        }

        @After
        void tearDown() {
            sql.close()
        }

        @Test
        void "Groovy + JUnit4 + Groovy SQL のテストサンプル1"() {
            setup: "データを追加する"
            sql.execute("INSERT INTO user_role(role_id, user_id, role) VALUES (?, ?, ?)"
                    , [100, 1, TestConst.TEST_ROLE_01])

            expect: "データが追加されているか確認する"
            def row = sql.firstRow("SELECT * FROM user_role WHERE role_id = 100 AND user_id = 1")
            assert row.role == "ROLE_USER"
        }

        @Test
        void "Groovy + JUnit4 + Groovy SQL のテストサンプル2"() {
            setup: "データを3件追加する"
            sql.withBatch("INSERT INTO user_role(role_id, user_id, role) VALUES (?, ?, ?)") {
                it.addBatch([100, 6, TestConst.TEST_ROLE_01])
                it.addBatch([101, 7, TestConst.TEST_ROLE_01])
                it.addBatch([102, 7, TestConst.TEST_ROLE_02])
            }

            expect: "追加されたデータをチェックする(カラムは全て取得するが role カラムだけチェックする)"
            def rows = sql.rows("SELECT * FROM user_role WHERE user_id IN (6, 7) ORDER BY role_id")
            assert rows.role == ["ROLE_USER", "ROLE_USER", "ROLE_ADMIN"]

            and: "追加されたデータをチェックする(role_id, role の2カラムのみ取得してチェックする)"
            rows = sql.rows("""\
                SELECT
                  role_id,
                  role
                FROM user_role
                WHERE user_id IN (6, 7)
                ORDER BY role_id
            """)
            assert rows == [[role_id: 100, role: "ROLE_USER"]
                            , [role_id: 101, role: "ROLE_USER"]
                            , [role_id: 102, role: "ROLE_ADMIN"]]
        }

    }

}

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

f:id:ksby:20170617164346p:plain

ちなみに assert row.role == "ROLE_USER"assert row.role == "ROLE_xxx" に変更してテストを失敗させると、Spock の時と同様に PowerAssert の分かりやすいレポートが出力されます。

f:id:ksby:20170617165219p:plain

Groovy SQLIntelliJ IDEA + Groovy SQL で書くと便利な点とは

SQL 文に IntelliJ IDEA の SQL の Code Style が適用される

上のコードでは分かりませんが、IntelliJ IDEA 上だと下のキャプチャのように SQL 文に IntelliJ IDEA の Code Style が適用されて色付きで表示されます。

f:id:ksby:20170617172901p:plain

適用される Code Style の設定は IntelliJ IDEA の「Settings」ダイアログの中の「Editor」-「Code Style」-「SQL」で設定されているものです。

f:id:ksby:20170617173017p:plain

また Ctrl+Alt+L を押してコードフォーマットすると、一部だけですが適用されます。分かる範囲では、小文字で書いていた SQL 文が大文字に変更されましたが、自動で改行されたり不要なスペースが削除されたりはしませんでした。

例えば以下のように SQL 文を全て小文字で書いていて Ctrl+Alt+L を押すと、

f:id:ksby:20170617173801p:plain

以下のように大文字に変更されます。

f:id:ksby:20170617173910p:plain

なぜかメソッドの引数の SQL 文に、テーブル名、カラム名等の補完が効く

なんでこんなところで SQL 文の補完が有効になるの? と不思議になるくらい補完が効きます。ヒアドキュメントで書いている SQL 文でも有効でした。便利です!

f:id:ksby:20170617174152p:plain f:id:ksby:20170617174315p:plain f:id:ksby:20170617174557p:plain

SQL 文を書く時にヒアドキュメントが使える

Groovy で書いているので、文字列にヒアドキュメントが使えます。"""\ ... """を前後に付ければ改行を入れて SQL 文が記述できます。ただし Ctrl+Alt+L を押してもヒアドキュメントの文字列はフォーマットされないので注意が必要です。

f:id:ksby:20170617175057p:plain

最後に

Spock ではなく JUnit4 で書く場合でも Groovy が便利だし、Groovy SQL も便利すぎます。今後テストは基本 Groovy で書こうと思います。

履歴

2017/06/17
初版発行。
2017/06/21
* テストメソッドに @Test を付け忘れていたり、戻り値の型を void にしていない箇所を修正しました。
* テストメソッド名を "" で囲んで自由に書ける を追加しました。
* テストメソッドに throws Exception の記述が不要 を追加しました。

Spring Boot 1.4.x の Web アプリを 1.5.x へバージョンアップする ( その9 )( Spring Boot を 1.5.3 → 1.5.4 にバージョンアップする )

概要

記事一覧こちらです。

Spring Boot 1.4.x の Web アプリを 1.5.x へバージョンアップする ( その8 )( logback-develop.xml, logback-unittest.xml, logback-product.xml の設定を logback-spring.xml と application.properties に移動してファイルを削除する ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • Spring Boot の 1.5.4 がリリースされているのでバージョンアップします。
    • Error Prone 以外のライブラリも最新バージョンにバージョンアップします。

参照したサイト・書籍

目次

  1. build.gradle を変更する
  2. clean タスク → Rebuild Project → build タスクを実行する

手順

build.gradle を変更する

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

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

    Spring Boot が 1.5.4.RELEASE にバージョンアップされていることを確認します。

    f:id:ksby:20170617082209p:plain

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

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

    無事 “BUILD SUCCESSFUL” のメッセージが表示されています。

    f:id:ksby:20170617082742p:plain

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

    f:id:ksby:20170617083241p:plain

ソースコード

build.gradle

group 'ksbysample'
version '1.5.4-RELEASE'

buildscript {
    ext {
        springBootVersion = '1.5.4.RELEASE'
    }
    repositories {
        jcenter()
        maven { url "https://repo.spring.io/release/" }
        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")
        // for Grgit
        classpath("org.ajoberstar:grgit:1.9.3")
        // 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 = '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 = '7.8.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) {
    // 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 '**/*.xml'
        fc.exclude '**/META-INF/**'
        fc.exclude '**/static/**'
        fc.exclude '**/templates/**'
        classes = files(fc.files)
    }
    reports {
        xml.enabled = false
        html.enabled = true
    }
}

repositories {
    jcenter()
}

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

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

dependencies {
    def jdbcDriver = "org.postgresql:postgresql:42.1.1"
    def spockVersion = "1.1-groovy-2.4"
    def domaVersion = "2.16.1"
    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("com.integralblue:log4jdbc-spring-boot-starter:1.0.1")
    compile("org.simpleframework:simple-xml:2.7.1")
    compile("com.univocity:univocity-parsers:2.4.1")
    testCompile("org.dbunit:dbunit:2.5.3")
    testCompile("com.icegreen:greenmail:1.5.5")
    testCompile("org.assertj:assertj-core:3.8.0")
    testCompile("com.jayway.jsonpath:json-path:2.2.0")
    testCompile("org.jsoup:jsoup:1.10.3")
    testCompile("cglib:cglib-nodep:3.2.5")
    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}")
}

..........
  • version '1.5.3-RELEASE'version '1.5.4-RELEASE' へ変更します。
  • buildscript の以下の点を変更します。
    • springBootVersion = '1.5.3.RELEASE'springBootVersion = '1.5.4.RELEASE' へ変更します。
    • maven { url "http://repo.spring.io/repo/" }maven { url "https://repo.spring.io/release/" } へ変更します。
    • classpath("org.ajoberstar:grgit:1.9.2")classpath("org.ajoberstar:grgit:1.9.3") へ変更します。
  • checkstyle の以下の点を変更します。
    • toolVersion = '7.7'toolVersion = '7.8.1' へ変更します。
  • dependencyManagement の以下の点を変更します。
    • mavenBom("io.spring.platform:platform-bom:Brussels-SR2")mavenBom("io.spring.platform:platform-bom:Brussels-SR3") へ変更します。
  • dependencies の以下の点を変更します。
    • def jdbcDriver = "org.postgresql:postgresql:42.0.0"def jdbcDriver = "org.postgresql:postgresql:42.1.1" へ変更します。
    • def domaVersion = "2.16.0"def domaVersion = "2.16.1" へ変更します。
    • testCompile("com.icegreen:greenmail:1.5.4")testCompile("com.icegreen:greenmail:1.5.5") へ変更します。
    • testCompile("org.assertj:assertj-core:3.7.0")testCompile("org.assertj:assertj-core:3.8.0") へ変更します。
    • testCompile("org.jsoup:jsoup:1.10.2")testCompile("org.jsoup:jsoup:1.10.3") へ変更します。

履歴

2017/06/17
初版発行。

IntelliJ IDEA を 2017.1.3 → 2017.1.4 へ、Git for Windows を 2.13.0 → 2.13.1(2) へバージョンアップ

IntelliJ IDEA を 2017.1.3 → 2017.1.4 へバージョンアップする

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

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

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

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

    f:id:ksby:20170617063441p:plain

  3. Plugin の update も表示されました。「Error-prone Compiler Integration」はバージョンアップすると動かなくなりますので、これだけチェックを外して「Update and Restart」ボタンをクリックします。

    f:id:ksby:20170617063651p:plain

  4. Patch がダウンロードされて IntelliJ IDEA が再起動します。

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

    f:id:ksby:20170617065544p:plain

  6. 処理が終了すると Gradle Tool Window のツリーの表示が other グループしかない初期の状態に戻っていますので、左上の「Refresh all Gradle projects」ボタンをクリックして更新します。更新が完了すると build グループ等が表示されます。

  7. IntelliJ IDEA のメインメニューから「Help」-「About」を選択し、2017.1.4 へバージョンアップされていることを確認します。

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

    f:id:ksby:20170617070325p:plain

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

    f:id:ksby:20170617070802p:plain

Git for Windows を 2.13.0 → 2.13.1(2) へバージョンアップする

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

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

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

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

  4. 「Select Components」画面が表示されます。「Git LFS(Large File Support)」だけチェックした状態で [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 Release Notes」のチェックを外した後、「Finish」ボタンをクリックしてインストーラーを終了します。

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

    f:id:ksby:20170617071849p:plain

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

    f:id:ksby:20170617072051p:plain

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

Spring Boot 1.4.x の Web アプリを 1.5.x へバージョンアップする ( 番外編 )( static メソッドをモック化してテストするには? )

概要

記事一覧こちらです。

Spring Framework の DI コンテナが管理するクラスのインスタンスのメソッドから DI コンテナで管理していないクラスの static メソッドが呼び出されている場合に、static メソッドをモック化して、Spring Framework の DI コンテナが管理するクラスのテストを書く方法があるのか知りたくなったので、いろいろ調べてみました。今回はそのメモ書きです。

モック化しないテストは Groovy + Spock で書きたいので、モック化する場合のテストも可能なら Groovy + Spock で、Spock が無理なら Groovy + JUnit4 で書く方法を調べています。

参照したサイト・書籍

  1. Spock Framework Reference Documentation - Interaction Based Testing
    http://spockframework.org/spock/docs/1.1/interaction_based_testing.html

  2. PowerMock
    https://github.com/powermock/powermock

  3. PowerMockとJUnitのRuleを使うときのメモ
    http://irof.hateblo.jp/entry/20130517/p1

  4. Mockito + PowerMock LinkageError while mocking system class
    https://stackoverflow.com/questions/16520699/mockito-powermock-linkageerror-while-mocking-system-class

目次

  1. テストで使用するクラスを書く
  2. Spock で正常処理のテストを書く
  3. static メソッドをモック化して実施したいテストとは?
  4. Groovy + Spock + GroovyMock で static メソッドをモック化してテストを書いてみる
  5. Groovy + JUnit4 + PowerMock で static メソッドをモック化してテストを書いてみる
  6. Groovy + Spock + PowerMock で static メソッドをモック化してテストを書いてみる
  7. Spock ではうまく動作しないので JUnit4 でテストを書いてみる
  8. まとめ
  9. PowerMock で void のメソッドをモック化するには?

手順

テストで使用するクラスを書く

今回テストで使用するクラスを2つ書きます。SampleHelper クラスと BrowfishUtils クラスです。

最終的に static メソッドのモックを作成してテストを書きたいのは SampleHelper クラスです。以下の仕様です。

  • SampleHelper クラスは @Component アノテーションを付加して Spring Framework の DIコンテナでインスタンス化します。
  • SampleHelper#encrypt メソッドは BrowfishUtils#encrypt メソッドを呼び出して渡された文字列を Browfish で暗号化します。
  • BrowfishUtils#encrypt メソッドは static メソッドです。
  • BrowfishUtils#encrypt メソッドは処理の中で発生する例外をそのまま throw しますが、SampleHelper#encrypt メソッドは RuntimeException に置き換えます。
  • 余談ですが、今回は static メソッドのテストの方法を調べるのが目的なので BrowfishUtils クラスでは暗号化モードには ECB を使用したのですが、ECB を使うと Error Prone が InsecureCryptoUsage というエラーを出力します(ECB で暗号化した文字列は常に同じになるので暗号化としてふさわしくない、というエラーのようです)。何となく作ったサンプルでしたが、Error Prone はこんな所までチェックするのか、と驚きました。
package ksbysample.webapp.lending;

import org.springframework.stereotype.Component;

import javax.crypto.BadPaddingException;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;

@Component
public class SampleHelper {

    public String encrypt(String str) {
        try {
            return BrowfishUtils.encrypt(str);
        } catch (NoSuchPaddingException | NoSuchAlgorithmException | InvalidKeyException
                | BadPaddingException | IllegalBlockSizeException e) {
            throw new RuntimeException(e);
        }
    }

}
package ksbysample.webapp.lending;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;

public class BrowfishUtils {

    private static final String KEY = "sample";
    private static final String ALGORITHM = "Blowfish";
    private static final String TRANSFORMATION = "Blowfish/ECB/PKCS5Padding";

    public static String encrypt(String str)
            throws NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException
            , BadPaddingException, IllegalBlockSizeException {
        SecretKeySpec secretKeySpec = new SecretKeySpec(KEY.getBytes(StandardCharsets.UTF_8), ALGORITHM);
        Cipher cipher = Cipher.getInstance(TRANSFORMATION);
        cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec);
        byte[] encryptedBytes = cipher.doFinal(str.getBytes(StandardCharsets.UTF_8));
        return Base64.getEncoder().encodeToString(encryptedBytes);
    }

}

Spock で正常処理のテストを書く

以下のテストを書きます。

package ksbysample.webapp.lending

import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import spock.lang.Specification
import spock.lang.Unroll

@SpringBootTest
class SampleHelperTest extends Specification {

    @Autowired
    private SampleHelper sampleHelper

    @Unroll
    def "SampleHelper.encrypt(#str) --> #result"() {
        expect:
        sampleHelper.encrypt(str) == result

        where:
        str        || result
        "test"     || "bjMKKlhqu/c="
        "xxxxxxxx" || "ERkXmOeArBKwGbCh+M6aHw=="
    }

}

実行して成功することを確認します。

f:id:ksby:20170609013513p:plain

static メソッドをモック化して実施したいテストとは?

最終的に書きたいのは以下の内容のテストです。

  • BrowfishUtils#encrypt メソッドをモック化して、呼び出されたら NoSuchPaddingException が throw されるようにします。
  • SampleHelper#encrypt メソッドを呼び出したら RuntimeException が throw されることを確認します。

Groovy + Spock + GroovyMock で static メソッドをモック化してテストを書いてみる

Interaction Based Testing の中の「Mocking Static Methods」を見ると Spock でも static メソッドをモック化できるようです。

最初は SampleHelper#encrypt メソッドではなく BrowfishUtils#encrypt メソッドでテストを書いてみます。

モックを使ったテストは正常処理のテストとは別にしたいので、SampleHelperTest クラスは @RunWith(Enclosed.class) を付加して extends Specification を削除し、「正常処理のテスト」static class と「異常処理のテスト」static class の2つに分けます。

package ksbysample.webapp.lending

import org.junit.experimental.runners.Enclosed
import org.junit.runner.RunWith
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import spock.lang.Specification
import spock.lang.Unroll

import javax.crypto.NoSuchPaddingException

@RunWith(Enclosed.class)
class SampleHelperTest {

    @SpringBootTest
    static class 正常処理のテスト extends Specification {

        @Autowired
        private SampleHelper sampleHelper

        @Unroll
        def "SampleHelper.encrypt(#str) --> #result"() {
            expect:
            sampleHelper.encrypt(str) == result

            where:
            str        || result
            "test"     || "bjMKKlhqu/c="
            "xxxxxxxx" || "ERkXmOeArBKwGbCh+M6aHw=="
        }

    }

    static class 異常処理のテスト extends Specification {

        def "BrowfishUtils.encryptを呼ぶとNoSuchPaddingExceptionをthrowする"() {
            given:
            GroovyMock(BrowfishUtils, global: true)
            BrowfishUtils.encrypt(_) >> { throw new NoSuchPaddingException() }

            when:
            BrowfishUtils.encrypt("test")

            then:
            thrown(NoSuchPaddingException.class)
        }

    }

}

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

f:id:ksby:20170613005146p:plain

SampleHelper#encrypt メソッドを呼び出すテストに変更してみます。

    @SpringBootTest
    static class GroovyMockを使用した異常処理のテスト extends Specification {

        @Autowired
        private SampleHelper sampleHelper

        def "SampleHelper.encryptを呼ぶとRuntimeExceptionをthrowする"() {
            given:
            GroovyMock(BrowfishUtils, global: true)
            BrowfishUtils.encrypt(_) >> { throw new NoSuchPaddingException() }

            when:
            sampleHelper.encrypt("test")

            then:
            thrown(RuntimeException.class)
        }

    }

テストを実行すると Expected exception of type 'java.lang.RuntimeException', but no exception was thrown というメッセージが出力されて失敗しました。例外を throw するようモックにした BrowfishUtils#encrypt メソッドが SampleHelper#encrypt メソッドからは呼び出されていないようです。

f:id:ksby:20170613005635p:plain

GroovyMock を使う方法だと Spring Framework の DI コンテナが絡んだ時にうまく動作しないようなので、他の方法を探します。

Groovy + JUnit4 + PowerMock で static メソッドをモック化してテストを書いてみる

Spring Boot がサポートしている Mockito で static メソッドをモック化できないか調べてみたところ、Mocito 自体は static メソッドのモック化には対応していませんが、PowerMock を使用するとモック化できるようです。

ライブラリを依存関係に追加します。Mockito_maven のページを参考に build.gradle を以下のように変更した後、Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。

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

    // PowerMock
    testCompile("org.powermock:powermock-module-junit4:1.6.6")
    testCompile("org.powermock:powermock-api-mockito:1.6.6")
}

SampleHelper#encrypt メソッドではなく、BrowfishUtils#encrypt メソッドをモックにして BrowfishUtils#encrypt メソッド自体を呼ぶテストを書いてみます。最初は Groovy + JUnit4 + PowerMock で書きます。

例外が throw されているかチェックするのに AssertJ の assertThatExceptionOfType メソッドを使用したのですが、lambda 式のところで赤波線が表示されてエラーになりました。なぜ?と思って Web で調べてみると、Groovy は lambda 式がサポートされていないことが原因でした。知りませんでした。。。

f:id:ksby:20170609021232p:plain

lambda 式ではなく匿名クラスで書くことにします。isThrownBy メソッドの引数が ThrowableAssert.ThrowingCallable インターフェースであることを確認した後、テストを以下のように書きます。

    @RunWith(PowerMockRunner.class)
    @PowerMockRunnerDelegate(JUnit4.class)
    @PrepareForTest(BrowfishUtils.class)
    static class 異常処理のテスト {

        @Test
        void "BrowfishUtil_encryptを呼ぶとNoSuchPaddingExceptionをthrowする"() {
            // setup:
            PowerMockito.mockStatic(BrowfishUtils.class)
            PowerMockito.when(BrowfishUtils.encrypt(Mockito.any()))
                    .thenThrow(new NoSuchPaddingException())

            // expect:
            assertThatExceptionOfType(NoSuchPaddingException.class).isThrownBy(
                    new ThrowableAssert.ThrowingCallable() {
                        @Override
                        void call() throws Throwable {
                            BrowfishUtils.encrypt("test")
                        }
                    })
        }

    }

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

f:id:ksby:20170613011138p:plain

Groovy + Spock + PowerMock で static メソッドをモック化してテストを書いてみる

書いたテストを Spock で書き直します。Web で調べると Spock で PowerMock を使いたい場合には PowerMockRule を使うよう書かれている記事を見かけるのですが、PowerMockとJUnitのRuleを使うときのメモ には複数の @Rule が指定されている場合、PowerMockRule は動かなくなる、とも書かれていました。PowerMockRule って使えないのでは?。。。と思いましたが、まずは試してみます。

PowerMockRule を見て build.gradle を以下のように変更します。変更後、Gradle Tool Window の左上にある「Refresh all Gradle projects」ボタンをクリックして更新します。

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

    // PowerMock
    testCompile("org.powermock:powermock-module-junit4:1.6.6")
    testCompile("org.powermock:powermock-api-mockito:1.6.6")
    testCompile("org.powermock:powermock-module-junit4-rule:1.6.6")
    testCompile("org.powermock:powermock-classloading-xstream:1.6.6")
}

まずは JUnit4 のまま PowerMockRule を使う方式へ変えてみます。先程作成した 「異常処理のテスト」static class を以下のように変更します。

    @RunWith(JUnit4.class)
    @PrepareForTest(BrowfishUtils.class)
    static class 異常処理のテスト {

        @Rule
        public PowerMockRule powerMock = new PowerMockRule()

        @Test
        void "BrowfishUtil_encryptを呼ぶとNoSuchPaddingExceptionをthrowする"() {
            // setup:
            PowerMockito.mockStatic(BrowfishUtils.class)
            PowerMockito.when(BrowfishUtils.encrypt(Mockito.any()))
                    .thenThrow(new NoSuchPaddingException())

            // expect:
            assertThatExceptionOfType(NoSuchPaddingException.class).isThrownBy(
                    new ThrowableAssert.ThrowingCallable() {
                        @Override
                        void call() throws Throwable {
                            BrowfishUtils.encrypt("test")
                        }
                    })
        }

    }

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

f:id:ksby:20170613013606p:plain

「異常処理のテスト」static class を Spock で書き直してみます。

    @PrepareForTest(BrowfishUtils.class)
    static class 異常処理のテスト extends Specification {

        @Rule
        public PowerMockRule powerMock = new PowerMockRule()

        def "BrowfishUtil_encryptを呼ぶとNoSuchPaddingExceptionをthrowする"() {
            given:
            PowerMockito.mockStatic(BrowfishUtils.class)
            PowerMockito.when(BrowfishUtils.encrypt(Mockito.any()))
                    .thenThrow(new NoSuchPaddingException())

            when:
            BrowfishUtils.encrypt("test")

            then:
            thrown(NoSuchPaddingException.class)
        }

    }

テストを実行すると、PowerMockRule 内の処理で NullPointerException が発生して失敗しました。JUnit4 だと動きますが Spock に変えると動きませんね。。。

f:id:ksby:20170613013848p:plain

PowerMockRule ではなく PowerMockRunner を使用する方法に変えてみます。spock.lang.Specification を見ると @RunWith(Sputnik.class) と記述されていました。Spock の Runner は Sputnik.class のようなので、以下のように書いてみます。

    @RunWith(PowerMockRunner.class)
    @PowerMockRunnerDelegate(Sputnik.class)
    @PrepareForTest(BrowfishUtils.class)
    static class 異常処理のテスト extends Specification {

        def "BrowfishUtil_encryptを呼ぶとNoSuchPaddingExceptionをthrowする"() {
            given:
            PowerMockito.mockStatic(BrowfishUtils.class)
            PowerMockito.when(BrowfishUtils.encrypt(Mockito.any()))
                    .thenThrow(new NoSuchPaddingException())

            when:
            BrowfishUtils.encrypt("test")

            then:
            thrown(NoSuchPaddingException.class)
        }

    }

テストを実行すると、成功はしますが Notifications are not supported for behaviour ALL_TESTINSTANCES_ARE_CREATED_FIRST というメッセージが出力されています。動きはするようなので、このまま進めます。

f:id:ksby:20170613014306p:plain

今度は @SpringBootTest アノテーションを付けて、SampleHelper#encrypt メソッドのテストを書いてみます。

    @RunWith(PowerMockRunner.class)
    @PowerMockRunnerDelegate(Sputnik.class)
    @SpringBootTest
    @PrepareForTest(BrowfishUtils.class)
    static class 異常処理のテスト extends Specification {

        @Autowired
        private SampleHelper sampleHelper

        def "SampleHelper_encryptを呼ぶとRuntimeExceptionをthrowする"() {
            given:
            PowerMockito.mockStatic(BrowfishUtils.class)
            PowerMockito.when(BrowfishUtils.encrypt(Mockito.any()))
                    .thenThrow(new NoSuchPaddingException())

            when:
            sampleHelper.encrypt("test")

            then:
            thrown(RuntimeException.class)
        }

    }

テストを実行すると、java.lang.IllegalStateException: Failed to load ApplicationContext のメッセージが出力されて失敗しました。Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.beans.factory.support.BeanNameGenerator]: Specified class is an interface というメッセージも出力されています。

f:id:ksby:20170613014511p:plain

Spock ではうまく動作しないので JUnit4 でテストを書いてみる

以下のようにテストを書き直します。

    @RunWith(PowerMockRunner.class)
    @PowerMockRunnerDelegate(SpringRunner.class)
    @SpringBootTest
    @PrepareForTest(BrowfishUtils.class)
    static class 異常処理のテスト {

        @Autowired
        private SampleHelper sampleHelper

        @Test
        void "SampleHelper_encryptを呼ぶとRuntimeExceptionをthrowする"() {
            // setup:
            PowerMockito.mockStatic(BrowfishUtils.class)
            PowerMockito.when(BrowfishUtils.encrypt(Mockito.any()))
                    .thenThrow(new NoSuchPaddingException())

            // expect:
            assertThatExceptionOfType(RuntimeException.class).isThrownBy(
                    new ThrowableAssert.ThrowingCallable() {
                        @Override
                        void call() throws Throwable {
                            sampleHelper.encrypt("test")
                        }
                    })
        }

    }

テストを実行すると、こちらも java.lang.IllegalStateException: Failed to load ApplicationContext のメッセージが出力されて失敗しました。ただし、こちらは Caused by: java.lang.LinkageError: loader constraint violation: loader (instance of org/powermock/core/classloader/MockClassLoader) previously initiated loading for a different type with name "javax/management/MBeanServer" というメッセージが出力されます。

f:id:ksby:20170610070939p:plain

stackoverflow を検索してみると Mockito + PowerMock LinkageError while mocking system class の記事が見つかりました。@PowerMockIgnore アノテーションを記述して無視させればよいようです。

テストを以下のように修正します。

    @RunWith(PowerMockRunner.class)
    @PowerMockRunnerDelegate(SpringRunner.class)
    @SpringBootTest
    @PrepareForTest(BrowfishUtils.class)
    @PowerMockIgnore("javax.management.*")
    static class 異常処理のテスト {

        @Autowired
        private SampleHelper sampleHelper

        @Test
        void "SampleHelper_encryptを呼ぶとRuntimeExceptionをthrowする"() {
            // setup:
            PowerMockito.mockStatic(BrowfishUtils.class)
            PowerMockito.when(BrowfishUtils.encrypt(Mockito.any()))
                    .thenThrow(new NoSuchPaddingException())

            // expect:
            assertThatExceptionOfType(RuntimeException.class).isThrownBy(
                    new ThrowableAssert.ThrowingCallable() {
                        @Override
                        void call() throws Throwable {
                            sampleHelper.encrypt("test")
                        }
                    })
        }

    }

テストを実行すると、何もエラー・警告のメッセージは出力されずに成功しました。

f:id:ksby:20170610071445p:plain

まとめ

  • Groovy + Spock でテストを書く方法は分かりませんでした。。。 PowerMockRule を使用する記事をよく見かけるのですが、なぜかうまく動作しません。
  • Groovy + JUnit4 + PowerMock で動作する方法は分かりました。この方法なら Spock で書いたテストと同じファイル内に書けます。
    • build.gradle の dependencies には以下の2行を記述します。バージョン番号は Mockito_maven のページを見て、その時の最新バージョンにします。
      • testCompile("org.powermock:powermock-module-junit4:1.6.6")
      • testCompile("org.powermock:powermock-api-mockito:1.6.6")
    • static メソッドをモック化するテストを書くクラスに以下のアノテーションを付加します。
      • @RunWith(PowerMockRunner.class)
      • @PowerMockRunnerDelegate(SpringRunner.class)
      • @SpringBootTest
      • @PowerMockIgnore("javax.management.*")
    • static メソッドをモック化するテストを書くクラスに、以下のアノテーションでモック化するクラスを記述します。
      • @PrepareForTest(~.class)
    • テストメソッドの最初に PowerMockito.mockStatic(...) でモック化するクラスを宣言し、PowerMockito.when(...).then~(...) でモック化する static メソッドとモック化した時の挙動を記述します。

テストクラスは最終的に以下のようになりました。

package ksbysample.webapp.lending

import org.assertj.core.api.ThrowableAssert
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 spock.lang.Specification
import spock.lang.Unroll

import javax.crypto.NoSuchPaddingException

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

@RunWith(Enclosed.class)
class SampleHelperTest {

    @SpringBootTest
    static class 正常処理のテスト extends Specification {

        @Autowired
        private SampleHelper sampleHelper

        @Unroll
        def "SampleHelper.encrypt(#str) --> #result"() {
            expect:
            sampleHelper.encrypt(str) == result

            where:
            str        || result
            "test"     || "bjMKKlhqu/c="
            "xxxxxxxx" || "ERkXmOeArBKwGbCh+M6aHw=="
        }

    }

    @RunWith(PowerMockRunner.class)
    @PowerMockRunnerDelegate(SpringRunner.class)
    @SpringBootTest
    @PrepareForTest(BrowfishUtils.class)
    @PowerMockIgnore("javax.management.*")
    static class 異常処理のテスト {

        @Autowired
        private SampleHelper sampleHelper

        @Test
        void "SampleHelper_encryptを呼ぶとRuntimeExceptionをthrowする"() {
            // setup:
            PowerMockito.mockStatic(BrowfishUtils.class)
            PowerMockito.when(BrowfishUtils.encrypt(Mockito.any()))
                    .thenThrow(new NoSuchPaddingException())

            // expect:
            assertThatExceptionOfType(RuntimeException.class).isThrownBy(
                    new ThrowableAssert.ThrowingCallable() {
                        @Override
                        void call() throws Throwable {
                            sampleHelper.encrypt("test")
                        }
                    })
        }

    }

}

PowerMock で void のメソッドをモック化するには?

戻り値が void の場合、モック化の書き方が上とは変わります。SampleHelper クラス、BrowfishUtils クラスに以下のように void のメソッドを追加して、

@Component
public class SampleHelper {

    ..........

    public void noReturn(String arg) {
        BrowfishUtils.noReturn(arg);
    }

}
public class BrowfishUtils {

    ..........

    public static void noReturn(String arg) {

    }

}

BrowfishUtils#noReturn が RuntimeException を throw するようモック化してテストを書くと以下のようになります。

    @RunWith(PowerMockRunner.class)
    @PowerMockRunnerDelegate(SpringRunner.class)
    @SpringBootTest
    @PrepareForTest(BrowfishUtils.class)
    @PowerMockIgnore("javax.management.*")
    static class voidメソッドをモック化するテスト {

        @Autowired
        private SampleHelper sampleHelper

        @Test
        void "SampleHelper_noReturnを呼ぶとRuntimeExceptionをthrowする"() {
            // setup:
            PowerMockito.mockStatic(BrowfishUtils.class)
            PowerMockito.doThrow(new RuntimeException())
                    .when(BrowfishUtils.class, "noReturn", Mockito.any())

            // expect:
            assertThatExceptionOfType(RuntimeException.class).isThrownBy(
                    new ThrowableAssert.ThrowingCallable() {
                        @Override
                        void call() throws Throwable {
                            sampleHelper.noReturn("test")
                        }
                    })
        }

    }

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

f:id:ksby:20170615001407p:plain

履歴

2017/06/15
初版発行。

Spring Boot 1.4.x の Web アプリを 1.5.x へバージョンアップする ( その8 )( logback-develop.xml, logback-unittest.xml, logback-product.xml の設定を logback-spring.xml と application.properties に移動してファイルを削除する )

概要

記事一覧こちらです。

Spring Boot 1.4.x の Web アプリを 1.5.x へバージョンアップする ( その7 )( Gradle を 2.13 → 3.5 へバージョンアップし、FindBugs Gradle Plugin が出力する大量のログを抑制する ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • Log4jdbc Spring Boot Starter の README.md と、そこにリンクのあった Spring Boot Reference Guide - 76. Logging を読んでいて、ログレベルを application.properties で設定できることに気づきました。古いバージョンの Spring Boot Reference Guide も読み返してみると Spring Boot 1.2 の頃から設定できていたようです。。。
    • ログレベルの設定を logback-develop.xml, logback-unittest.xml, logback-product.xml から application.properties に移動します。移動すると logback-spring.xml 以外のファイルはほとんど記述する内容がなくなるので、残りの設定を logback-spring.xml へ移動してファイルを削除することにします。

参照したサイト・書籍

  1. Spring Boot Reference Guide - 76. Logging
    https://docs.spring.io/spring-boot/docs/current/reference/html/howto-logging.html

  2. Spring Bootのログ出力(概要)
    http://qiita.com/NagaokaKenichi/items/7cf2f427d88dfd4f56a8

目次

  1. logback-develop.xml, logback-unittest.xml, logback-product.xml の設定を application.properties, logback-spring.xml に移動して削除する

手順

logback-develop.xml, logback-unittest.xml, logback-product.xml の設定を application.properties, logback-spring.xml に移動して削除する

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

  2. src/main/resources の下の logback-spring.xmlリンク先の内容 に変更します。

  3. src/main/resources の下の logback-develop.xml, logback-unittest.xml, logback-product.xml を削除します。

  4. clean タスク実行 → Rebuild Project → build タスクを実行して、"BUILD SUCCESSFUL" が出力されることを確認します。

    f:id:ksby:20170607230300p:plain

  5. bootRun を実行して Tomcat を起動してみると、コンソールにログが出力されることが確認できます。

    f:id:ksby:20170608000052p:plain

  6. build タスクを実行して作成された ksbysample-webapp-lending-1.5.3-RELEASE.jar を C:\webapps\ksbysample-webapp-lending\lib の下にコピーし、C:\webapps\ksbysample-webapp-lending\bat\webapp_startup.bat を修正してから起動してみます。

    バナーの後には何も出力されていません。

    f:id:ksby:20170608000833p:plain

    C:\webapps\ksbysample-webapp-lending\logs の下には ksbysample-webapp-lending.log が作成されており、ログファイルにはログが出力されていることが確認できます。

    f:id:ksby:20170608001029p:plain

ソースコード

application.properties, application-develop.properties, application-unittest.properties

■application.properties

..........

valueshelper.classpath.prefix=

logging.level.root=INFO
logging.level.org.seasar.doma=ERROR
  • logging.level.~ の設定を追加します(2行)。

■application-develop.properties

..........

spring.redis.sentinel.master=mymaster
spring.redis.sentinel.nodes=localhost:6381,localhost:6382,localhost:6383

# Spring MVC
logging.level.org.springframework.web=DEBUG
# log4jdbc-log4j2
logging.level.jdbc.sqlonly=DEBUG
logging.level.jdbc.sqltiming=INFO
logging.level.jdbc.audit=INFO
logging.level.jdbc.resultset=ERROR
logging.level.jdbc.resultsettable=ERROR
logging.level.jdbc.connection=DEBUG
  • logging.level.~ の設定を追加します(7行)。

■application-unittest.properties

..........

spring.redis.sentinel.master=mymaster
spring.redis.sentinel.nodes=localhost:6381,localhost:6382,localhost:6383

logging.level.root=OFF
  • logging.level.~ の設定を追加します(1行)。

logback-spring.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <include resource="org/springframework/boot/logging/logback/defaults.xml"/>
    <include resource="org/springframework/boot/logging/logback/console-appender.xml"/>

    <if condition='"${spring.profiles.active}" == "product"'>
        <then>
            <property name="LOG_FILE" value="${LOG_FILE}"/>
            <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
                <encoder>
                    <pattern>${FILE_LOG_PATTERN}</pattern>
                </encoder>
                <file>${LOG_FILE}</file>
                <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
                    <fileNamePattern>${LOG_FILE}.%d{yyyy-MM-dd}</fileNamePattern>
                    <maxHistory>30</maxHistory>
                </rollingPolicy>
            </appender>
        </then>
    </if>

    <!-- Tomcat JDBC Connection Pool SlowQueryReport interceptor 用ログファイル -->
    <if condition='"${spring.profiles.active}" == "product" &amp;&amp; "${slowquery.logging.file}" != ""'>
        <then>
            <springProperty scope="context" name="slowQueryLogFile" source="slowquery.logging.file"/>
            <appender name="SLOWQUERY_LOG_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
                <encoder>
                    <pattern>${FILE_LOG_PATTERN}</pattern>
                </encoder>
                <file>${slowQueryLogFile}</file>
                <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
                    <fileNamePattern>${slowQueryLogFile}.%d{yyyy-MM-dd}</fileNamePattern>
                    <maxHistory>30</maxHistory>
                </rollingPolicy>
            </appender>

            <logger name="org.apache.tomcat.jdbc.pool.interceptor.SlowQueryReport" level="INFO">
                <appender-ref ref="SLOWQUERY_LOG_FILE"/>
            </logger>
        </then>
    </if>

    <if condition='"${spring.profiles.active}" == "develop"'>
        <then>
            <root>
                <appender-ref ref="CONSOLE"/>
            </root>
        </then>
    </if>
    <if condition='"${spring.profiles.active}" == "product"'>
        <then>
            <root>
                <appender-ref ref="FILE"/>
            </root>
        </then>
    </if>
</configuration>
  • <appender name="CONSOLE" ...> ... </appender><include resource="org/springframework/boot/logging/logback/console-appender.xml"/> に変更します。
  • <!-- Tomcat JDBC Connection Pool SlowQueryReport interceptor 用ログファイル --> の if 文の中に <logger name="org.apache.tomcat.jdbc.pool.interceptor.SlowQueryReport" level="INFO"> ... </logger> の設定を追加します。
  • <include resource="logback-develop.xml"/><root><appender-ref ref="CONSOLE"/></root> に変更します。
  • <if condition='"${spring.profiles.active}" == "unittest"'> ... </if> を削除します。
  • <include resource="logback-product.xml"/><root><appender-ref ref="FILE"/></root> に変更します。

履歴

2017/06/08
初版発行。

Spring Boot 1.4.x の Web アプリを 1.5.x へバージョンアップする ( その7 )( Gradle を 2.13 → 3.5 へバージョンアップし、FindBugs Gradle Plugin が出力する大量のログを抑制する )

概要

記事一覧はこちらです。

Spring Boot 1.4.x の Web アプリを 1.5.x へバージョンアップする ( その6 )( Thymeleaf を 2.1.5 → 3.0.6 へバージョンアップする2 ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • Gradle を 2.13 → 3.5 へバージョンアップします。
    • Gradle 3.3 以降にすると FindBugs Gradle Plugin が大量のログを出力するようになるのでバージョンアップを止めていたのですが、出力しないようにする方法が見つかったので反映します。

参照したサイト・書籍

  1. Task cache enabled with findbugs causes the build to fail
    https://discuss.gradle.org/t/task-cache-enabled-with-findbugs-causes-the-build-to-fail/22572

目次

  1. Gradle を 2.13 → 3.5 へバージョンアップする
  2. Gradle 3.3 以降にすると FindBugs Gradle Plugin が大量にログを出力する現象を抑制する

手順

Gradle を 2.13 → 3.5 へバージョンアップする

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

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

    f:id:ksby:20170530003517p:plain

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

    f:id:ksby:20170530003710p:plain

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

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

    最後に “BUILD SUCCESSFUL” は出ますが、まだ FindBugs Gradle Plugin の大量ログ出力の対応を入れていないので、途中に Cannot open codebase filesystem:... のログが大量に出力されます。

    f:id:ksby:20170530005611p:plain f:id:ksby:20170530005738p:plain

Gradle 3.3 以降にすると FindBugs Gradle Plugin が大量にログを出力する現象を抑制する

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

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

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

    今回は Cannot open codebase filesystem:... のログが全く出力されません。

    f:id:ksby:20170530011531p:plain

    試しに Spring Boot 1.3.x の Web アプリを 1.4.x へバージョンアップする ( その8 )( build.gradle への checkstyle, findbugs の導入+CheckStyle-IDEA, FindBugs-IDEA Plugin の導入 ) で修正した src/main/java/ksbysample/webapp/lending/web/booklist/RegisterBooklistForm.java を以下のように元に戻してから clean タスク実行 → Rebuild Project → build タスク を実行してみると、

    f:id:ksby:20170530013858p:plain

    • public static classpublic class に変更しています。

    FindBugs rule violations were found. のログが出力されており、

    f:id:ksby:20170530014340p:plain

    main.html を開くと static を付けるよう指摘されています。

    f:id:ksby:20170530014530p:plain

    問題なさそうですので、このまま 3.5 を使います。

ソースコード

build.gradle

■その1

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

..........

// for Doma-Gen
task domaGen {
    doLast {
        // まず変更が必要なもの
        def rootPackageName = 'ksbysample.webapp.lending'
        def daoPackagePath = 'src/main/java/ksbysample/webapp/lending/dao'
        def dbUrl = 'jdbc:postgresql://localhost/ksbylending'
        def dbUser = 'ksbylending_user'
        def dbPassword = 'xxxxxxxx'
        def tableNamePattern = '.*'
        // おそらく変更不要なもの
        def importOfComponentAndAutowiredDomaConfig = "${rootPackageName}.util.doma.ComponentAndAutowiredDomaConfig"
        def workDirPath = 'work'
        def workDaoDirPath = "${workDirPath}/dao"

        // 作業用ディレクトリを削除する
        clearDir("${workDirPath}")

        // 現在の Dao インターフェースのバックアップを取得する
        copy() {
            from "${daoPackagePath}"
            into "${workDaoDirPath}/org"
        }

        // Dao インターフェース、Entity クラスを生成する
        ant.taskdef(resource: 'domagentask.properties',
                classpath: configurations.domaGenRuntime.asPath)
        ant.gen(url: "${dbUrl}", user: "${dbUser}", password: "${dbPassword}", tableNamePattern: "${tableNamePattern}") {
            entityConfig(packageName: "${rootPackageName}.entity", useListener: false)
            daoConfig(packageName: "${rootPackageName}.dao")
            sqlConfig()
        }

        // 生成された Dao インターフェースを作業用ディレクトリにコピーし、
        // @ComponentAndAutowiredDomaConfig アノテーションを付加する
        copy() {
            from "${daoPackagePath}"
            into "${workDaoDirPath}/replace"
            filter {
                line ->
                    line.replaceAll('import org.seasar.doma.Dao;', "import ${importOfComponentAndAutowiredDomaConfig};\nimport org.seasar.doma.Dao;")
                            .replaceAll('@Dao', '@Dao\n@ComponentAndAutowiredDomaConfig')
            }
        }

        // @ComponentAndAutowiredDomaConfig アノテーションを付加した Dao インターフェースを
        // dao パッケージへ戻す
        copy() {
            from "${workDaoDirPath}/replace"
            into "${daoPackagePath}"
        }

        // 元々 dao パッケージ内にあったファイルを元に戻す
        copy() {
            from "${workDaoDirPath}/org"
            into "${daoPackagePath}"
        }

        // 作業用ディレクトリを削除する
        clearDir("${workDirPath}")

        // 自動生成したファイルを git add する
        addGit()
    }
}

task downloadCssFontsJs {
    doLast {
        def staticDirPath = 'src/main/resources/static'
        def workDirPath = 'work'
        def adminLTEVersion = '2.2.0'
        def jQueryVersion = '2.1.4'
        def fontAwesomeVersion = '4.3.0'
        def ioniconsVersion = '2.0.1'
        def html5shivJsVersion = '3.7.2'
        def respondMinJsVersion = '1.4.2'

        // 作業用ディレクトリを削除する
        clearDir("${workDirPath}")

        // Bootstrap & AdminLTE Dashboard & Control Panel Template
        downloadAdminLTE("${adminLTEVersion}", "${jQueryVersion}", "${workDirPath}", "${staticDirPath}")

        // Font Awesome Icons
        downloadFontAwesome("${fontAwesomeVersion}", "${workDirPath}", "${staticDirPath}")

        // Ionicons
        downloadIonicons("${ioniconsVersion}", "${workDirPath}", "${staticDirPath}")

        // html5shiv.js
        downloadHtml5shivJs("${html5shivJsVersion}", "${workDirPath}", "${staticDirPath}")

        // respond.min.js
        downloadRespondMinJs("${respondMinJsVersion}", "${workDirPath}", "${staticDirPath}")

        // fileinput.min.js ( v4.2.7 )
        downloadBootstrapFileInputMinJs("${workDirPath}", "${staticDirPath}")

        // 作業用ディレクトリを削除する
        clearDir("${workDirPath}")

        // 追加したファイルを git add する
        addGit()
    }
}

task printClassWhatNotMakeTest {
    doLast {
        def srcDir = new File("src/main/java")
        def excludePaths = [
                "src/main/java/ksbysample/webapp/lending/Application.java"
                , "src/main/java/ksbysample/webapp/lending/config"
                , "src/main/java/ksbysample/webapp/lending/cookie"
                , "src/main/java/ksbysample/webapp/lending/dao"
                , "src/main/java/ksbysample/webapp/lending/entity"
                , "src/main/java/ksbysample/webapp/lending/exception"
                , "src/main/java/ksbysample/webapp/lending/helper/download/booklistcsv"
                , "src/main/java/ksbysample/webapp/lending/helper/download/DataDownloadHelper.java"
                , "src/main/java/ksbysample/webapp/lending/helper/page/PagenationHelper.java"
                , "src/main/java/ksbysample/webapp/lending/security/LendingUser.java"
                , "src/main/java/ksbysample/webapp/lending/security/RoleAwareAuthenticationSuccessHandler.java"
                , "src/main/java/ksbysample/webapp/lending/service/calilapi/response"
                , "src/main/java/ksbysample/webapp/lending/service/file/BooklistCSVRecord.java"
                , "src/main/java/ksbysample/webapp/lending/service/openweathermapapi"
                , "src/main/java/ksbysample/webapp/lending/service/queue/InquiringStatusOfBookQueueMessage.java"
                , "src/main/java/ksbysample/webapp/lending/util/doma"
                , "src/main/java/ksbysample/webapp/lending/util/velocity/VelocityUtils.java"
                , "src/main/java/ksbysample/webapp/lending/values/validation/ValuesEnum.java"
                , "src/main/java/ksbysample/webapp/lending/view/BookListCsvView.java"
                , "src/main/java/ksbysample/webapp/lending/web/.+/.+Service.java"
                , "src/main/java/ksbysample/webapp/lending/webapi/common/CommonWebApiResponse.java"
                , "src/main/java/ksbysample/webapp/lending/webapi/weather"
        ]
        def excludeFileNamePatterns = [
                ".*EventListener.java"
                , ".*Dto.java"
                , ".*Form.java"
                , ".*Values.java"
        ]

        compareSrcAndTestDir(srcDir, excludePaths, excludeFileNamePatterns)
    }
}

..........
  • gradleVersion = '2.13'gradleVersion = '3.5' に変更します。
  • domaGen, downloadCssFontsJs, printClassWhatNotMakeTest の書き方を task ... << { ... }task ... { doLast { ... } } に変更します。

■その2

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 '**/*.xml'
        fc.exclude '**/META-INF/**'
        fc.exclude '**/static/**'
        fc.exclude '**/templates/**'
        classes = files(fc.files)
    }
    reports {
        xml.enabled = false
        html.enabled = true
    }
}
  • doFirst { ... } を追加します。これで FindBugs の Analysis の対象のファイルから Cannot open codebase filesystem:... のログが出力されるファイルを除外します。

履歴

2017/05/30
初版発行。