かんがるーさんの日記

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

Spring Boot + npm + Geb で入力フォームを作ってテストする ( その100 )( Gradle を 6.9.1 → 7.2 へ、Spring Boot を 2.4.10 → 2.5.4 へ、Geb を 4.1 → 5.0 へバージョンアップする2 )

概要

記事一覧はこちらです。

Spring Boot + npm + Geb で入力フォームを作ってテストする ( その99 )( Gradle を 6.9.1 → 7.2 へ、Spring Boot を 2.4.10 → 2.5.4 へ、Geb を 4.1 → 5.0 へバージョンアップする ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • 前回からの続きです。

参照したサイト・書籍

  1. Difference between mockito-core vs mockito-inline
    https://stackoverflow.com/questions/65986197/difference-between-mockito-core-vs-mockito-inline

  2. Mocking static methods (since 3.4.0)
    https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Mockito.html#static_mocks

  3. mvc controller test with session attribute
    https://stackoverflow.com/questions/26341400/mvc-controller-test-with-session-attribute

目次

  1. JUnit 4 の依存関係が残っているので削除する
  2. gebTest タスクが成功するか確認する
  3. powermock を利用しているテストを powermock なしで動作するよう変更する
  4. mockito-inline を依存関係に追加した後にテストが失敗する原因を調査する
  5. apply(sharedHttpSession()) を呼び出して MockMvc の request 間で session 情報が自動で共有されるようにする
  6. @Unroll アノテーションを削除する

手順

JUnit 4 の依存関係が残っているので削除する

gradlew dependencies コマンドを実行して、出力結果から JUnit 4(junit:junit:4) を検索すると以下の2つのモジュールでヒットしました。

  • org.springframework.boot:spring-boot-starter-web
  • com.icegreen:greenmail
+--- org.springframework.boot:spring-boot-starter-web -> 2.5.4
|    ..........
|    +--- org.springframework.boot:spring-boot-starter-json:2.5.4
|    |    +--- org.springframework.boot:spring-boot-starter:2.5.4 (*)
|    |    +--- org.springframework:spring-web:5.3.9
|    |    |    +--- org.springframework:spring-beans:5.3.9 (*)
|    |    |    \--- org.springframework:spring-core:5.3.9 (*)
|    |    +--- com.fasterxml.jackson.core:jackson-databind:2.12.4
|    |    |    +--- com.fasterxml.jackson.core:jackson-annotations:2.12.4
|    |    |    |    \--- com.fasterxml.jackson:jackson-bom:2.12.4
|    |    |    |         +--- junit:junit:4.13.1 -> 4.13.2 (c)
|    |    |    |         +--- com.fasterxml.jackson.core:jackson-annotations:2.12.4 (c)
|    |    |    |         +--- com.fasterxml.jackson.core:jackson-core:2.12.4 (c)
|    |    |    |         +--- com.fasterxml.jackson.core:jackson-databind:2.12.4 (c)
|    |    |    |         +--- com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.12.4 (c)
|    |    |    |         +--- com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.12.4 (c)
|    |    |    |         \--- com.fasterxml.jackson.module:jackson-module-parameter-names:2.12.4 (c)
|    |    |    +--- com.fasterxml.jackson.core:jackson-core:2.12.4
|    |    |    |    \--- com.fasterxml.jackson:jackson-bom:2.12.4 (*)
|    |    |    \--- com.fasterxml.jackson:jackson-bom:2.12.4 (*)
|    |    +--- com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.12.4
|    |    |    +--- com.fasterxml.jackson.core:jackson-core:2.12.4 (*)
|    |    |    +--- com.fasterxml.jackson.core:jackson-databind:2.12.4 (*)
|    |    |    \--- com.fasterxml.jackson:jackson-bom:2.12.4 (*)
|    |    +--- com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.12.4
|    |    |    +--- com.fasterxml.jackson.core:jackson-annotations:2.12.4 (*)
|    |    |    +--- com.fasterxml.jackson.core:jackson-core:2.12.4 (*)
|    |    |    +--- com.fasterxml.jackson.core:jackson-databind:2.12.4 (*)
|    |    |    \--- com.fasterxml.jackson:jackson-bom:2.12.4 (*)
|    |    \--- com.fasterxml.jackson.module:jackson-module-parameter-names:2.12.4
|    |         +--- com.fasterxml.jackson.core:jackson-core:2.12.4 (*)
|    |         +--- com.fasterxml.jackson.core:jackson-databind:2.12.4 (*)
|    |         \--- com.fasterxml.jackson:jackson-bom:2.12.4 (*)
|    ..........
..........
+--- com.icegreen:greenmail:1.6.5
|    +--- com.sun.mail:jakarta.mail:1.6.7 (*)
|    +--- org.slf4j:slf4j-api:1.7.32
|    \--- junit:junit:4.13.2

build.gradle を変更します。

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

    // dependency-management-plugin によりバージョン番号が自動で設定されるもの
    // Appendix F. Dependency versions ( https://docs.spring.io/spring-boot/docs/2.1.4.RELEASE/reference/html/appendix-dependency-versions.html ) 参照
    implementation("org.springframework.boot:spring-boot-starter-web") {
        exclude group: "junit", module: "junit"
    }
    ..........

    // dependency-management-plugin によりバージョン番号が自動で設定されないもの、あるいは最新バージョンを指定したいもの
    ..........
    testImplementation("com.icegreen:greenmail:1.6.5") {
        exclude group: "junit", module: "junit"
    }
  • implementation("org.springframework.boot:spring-boot-starter-web")testImplementation("com.icegreen:greenmail:1.6.5")exclude group: "junit", module: "junit" を追加します。

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

再度 gradlew dependencies コマンドを実行して JUnit 4 が依存関係から削除されていることを確認します。

clean タスク実行 → Rebuild Project を実行すると、ksbysample/webapp/bootnpmgeb/web/inquiry/form/InquiryInput02FormValidatorTest.groovy でエラーが発生しました。

f:id:ksby:20211010230408p:plain

エラーメッセージの箇所にジャンプすると @RunWith(Enclosed) が使用されていました。テストが認識されていないのはこのファイルでした。

f:id:ksby:20211010230605p:plain

以下の点を変更します。

  • @RunWith(Enclosed) を削除します。

clean タスク実行 → Rebuild Project 実行 → build タスクを実行すると "BUILD SUCCESSFUL" のメッセージが出力されて、成功したテストの件数が 147 → 146 になりました。powermock を利用したテストを 1件コメントアウトしているので、これで全てのテストが認識されたことになります。

f:id:ksby:20211010231529p:plain

gebTest タスクが成功するか確認する

まず src/test/groovy/geb/gebspec/inquiry/InquiryTestSpec.groovy で MailServerResource が使用されていたので MailServerExtension に変更します。

class InquiryTestSpec extends GebSpec {

    MailServerExtension mailServerExtension = new MailServerExtension()

    Sql sql

    def setup() {
        mailServerExtension.start()

        // 外部プロセスから接続するので H2 TCP サーバへ接続する
        sql = Sql.newInstance("jdbc:h2:tcp://localhost:9092/mem:bootnpmgebdb", "sa", "")
        sql.execute("truncate table INQUIRY_DATA")
    }

    def cleanup() {
        mailServerExtension.stop()
        sql.close()
    }
  • InquiryTestSpec クラスでは @SringBootTest アノテーションを付与していないため @Autowired を付与しても意味がないので、@Rule アノテーションを削除し MailServerResource mailServerResource = new MailServerResource()MailServerExtension mailServerExtension = new MailServerExtension() に変更します。
  • setup メソッド内に mailServerExtension.start() を追加します。
  • cleanup メソッド内に mailServerExtension.stop() を追加します。
  • テストクラス内の mailServerResource.mailServerExtension. に変更します。

gebTest タスクを実行すると、テストが 1件も実行されていません。。。

f:id:ksby:20211010233832p:plain

Geb のテストクラスに @Test アノテーションは付与していないしテストが認識されない原因は何だろう?と思いつつ調査した結果、build.gradle に記述されている chromeTest、filrefoxTest タスクに useJUnitPlatform() が記述されていないためでした。

build.gradle の task "${driver}Test"(type: Test) { ... } 内に useJUnitPlatform() を追加します。

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

        // for JUnit 5
        useJUnitPlatform()

        testLogging {
            afterSuite printTestCount
        }
    }
}
task gebTest {
    dependsOn drivers.collect { tasks["${it}Test"] }
    enabled = false
}

gebTest タスクを実行すると全てのテストが実行されて成功しました。

f:id:ksby:20211010235424p:plain

powermock を利用しているテストを powermock なしで動作するよう変更する

src/test/groovy/ksbysample/webapp/bootnpmgeb/web/inquiry/form/InquiryInput02FormValidatorTest.groovy に記述している powermock を利用しているテストを、

    @RunWith(PowerMockRunner)
    @PowerMockRunnerDelegate(SpringRunner)
    @SpringBootTest
    @PrepareForTest(EmailValidator)
    @PowerMockIgnore(["javax.management.*", "com.sun.org.apache.xerces.*", "javax.xml.*", "org.xml.*", "org.w3c.dom.*", "com.sun.org.apache.xalan.*"])
    static class InquiryInput02FormValidator_メールアドレス {

        @Autowired
        private InquiryInput02FormValidator input02FormValidator

        Errors errors
        InquiryInput02Form inquiryInput02Form

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

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

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

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

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

    }

以下のように変更します。

    @Nested
    @SpringBootTest
    static class InquiryInput02FormValidator_メールアドレス {

        @Autowired
        private InquiryInput02FormValidator input02FormValidator

        Errors errors
        InquiryInput02Form inquiryInput02Form

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

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

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

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

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

    }

Mockito で static method のテストを実装するには mockito-inline が必要なので、build.gradle の dependencies block に testImplementation("org.mockito:mockito-inline") を追加します。

dependencies {
    ..........
    testImplementation("org.codehaus.groovy:groovy-sql")
    testImplementation("org.mockito:mockito-inline")

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

テスト単体で実行すると成功しました。

f:id:ksby:20211011002454p:plain

clean タスク実行 → Rebuild Project 実行 → build タスクを実行すると、test タスクで今回変更したテスト以外のテストがなぜか失敗しました。。。

f:id:ksby:20211011003133p:plain

mockito-inline を依存関係に追加した後にテストが失敗する原因を調査する

エラーの原因を調べようとテストを IntelliJ IDEA から単体で実行してみると成功します。

f:id:ksby:20211011160108p:plain

mockito-inline を依存関係に追加したのが原因だろうと思い、依存関係から外して powermock を利用しているテストを powermock なしで動作するよう変更する で変更したテストもコメントアウトして build タスクを実行すると、テストは全て成功します。

f:id:ksby:20211011161040p:plain

mockito-inline を依存関係に追加し、コメントアウトしたテストも元に戻します。

コンソールに出力されているエラーメッセージだけでは詳細が分からないので build/reports/tests/test/index.html のレポートファイルを開いてみると、失敗した手テスト全てで Caused by: java.lang.IllegalArgumentException: セットされるはずのデータがセットされていません のログが出力されていました。

f:id:ksby:20211011214003p:plain

上のログから該当箇所を確認してみると、mockito-inline が依存関係に追加されると mockMvc.perform(TestHelper.postForm("/inquiry/input/02?move=next", inquiryInput02Form_001).with(csrf()).session(session)) でデータを渡せなくなる(TestHelper.postForm メソッドでデータをセットできない?)ように見えるのですが、これだけでは解決の仕方が分かりません。

Web でいろいろ検索して調べたところ、以下の2つのページを見つけました。

Mockito.mockStatic メソッドを使用する場合には try-with-resources 構文を使うように書かれていますね。。。 まさか、Mockito.mockStatic 戻り値を変数に取っておいて close メソッドを呼び出す必要があるとは思いませんでした。

powermock を利用しているテストを powermock なしで動作するよう変更する で変更したテストを以下のように変更します。

        @Test
        void "メールアドレスの Validation のテスト"() {
            given:
            MockedStatic mockedEmailValidator = Mockito.mockStatic(EmailValidator)

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

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

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

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

            cleanup:
            mockedEmailValidator.close()
        }
  • given block を追加して Mockito.mockStatic を呼び出している行をこの下に移動し、Mockito.mockStatic(EmailValidator)MockedStatic mockedEmailValidator = Mockito.mockStatic(EmailValidator) に変更します。
  • cleanup block を追加し、mockedEmailValidator.close() を追加します。

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

f:id:ksby:20211012091902p:plain

apply(sharedHttpSession()) を呼び出して MockMvc の request 間で session 情報が自動で共有されるようにする

mockito-inline を依存関係に追加した後にテストが失敗する原因を調査する の調査をしていた時に mvc controller test with session attribute のページを見つけました。MockMvc のインスタンスを生成する時に apply(sharedHttpSession()) を呼び出せば session 情報が自動で共有されるようになるとのこと。

以下のような実装の場合、

    @BeforeEach
    void setup() {
        mockMvc = MockMvcBuilders.webAppContextSetup(context)
                .apply(springSecurity())
                .build()
    }

    @Test
    void "完了画面で「入力画面へ」ボタンをクリックして入力画面1へ戻ると入力していたデータがクリアされる"() {
        when: "入力画面1で項目全てに入力して「次へ」ボタンをクリックする"
        MvcResult result = mockMvc.perform(TestHelper.postForm("/inquiry/input/01?move=next", inquiryInput01Form_001).with(csrf()))
                .andExpect(status().isFound())
                .andExpect(redirectedUrlPattern("**/inquiry/input/02"))
                .andReturn()
        MockHttpSession session = result.getRequest().getSession()

        and: "入力画面2で項目全てに入力して「次へ」ボタンをクリックする"
        mockMvc.perform(TestHelper.postForm("/inquiry/input/02?move=next", inquiryInput02Form_001).with(csrf()).session(session))
                .andExpect(status().isFound())
                .andExpect(redirectedUrlPattern("**/inquiry/input/03"))

        ..........
    }

以下のように変更します。

    @BeforeEach
    void setup() {
        mockMvc = MockMvcBuilders.webAppContextSetup(context)
                .apply(springSecurity())
                .apply(sharedHttpSession())
                .build()
    }

    @Test
    void "完了画面で「入力画面へ」ボタンをクリックして入力画面1へ戻ると入力していたデータがクリアされる"() {
        when: "入力画面1で項目全てに入力して「次へ」ボタンをクリックする"
        mockMvc.perform(TestHelper.postForm("/inquiry/input/01?move=next", inquiryInput01Form_001).with(csrf()))
                .andExpect(status().isFound())
                .andExpect(redirectedUrlPattern("**/inquiry/input/02"))

        and: "入力画面2で項目全てに入力して「次へ」ボタンをクリックする"
        mockMvc.perform(TestHelper.postForm("/inquiry/input/02?move=next", inquiryInput02Form_001).with(csrf()))
                .andExpect(status().isFound())
                .andExpect(redirectedUrlPattern("**/inquiry/input/03"))

        ..........
    }
  • MockMvcBuilders クラスで MockMvc のインスタンスを生成している処理に .apply(sharedHttpSession()) を追加します。
  • テストメソッド内で MvcResult result =.andReturn() を削除します。
  • MockHttpSession session = result.getRequest().getSession() も削除します。
  • その下の .session(session) の部分を全て削除します。

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

f:id:ksby:20211012095031p:plain

ちなみに @AutoConfigureMockMvc アノテーションを付与して MockMvc のインスタンスを自動生成した場合、session 情報は共有されませんでした。apply(sharedHttpSession()) による session 情報共有の機能を利用したい場合には自分でインスタンスを生成する必要があります。

@Unroll アノテーションを削除する

Spock 2 から @Unroll アノテーションの記述が不要になったので、削除します。

削除後に clean タスク実行 → Rebuild Project 実行 → build タスクを実行すると "BUILD SUCCESSFUL" のメッセージが出力されて、テスト数も削除前と同じ 147 でした。

f:id:ksby:20211012100409p:plain

履歴

2021/10/12
初版発行。