かんがるーさんの日記

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

Spring Boot 2.0.x の Web アプリを 2.1.x へバージョンアップする ( その9 )( JUnit 5 へバージョンアップ。。。@RunWith(Enclosed) + @Unroll + useJUnitPlatform() の組み合わせでテストが終わらない問題を解決する )

概要

記事一覧はこちらです。

Spring Boot 2.0.x の Web アプリを 2.1.x へバージョンアップする ( その8 )( JUnit 5 へバージョンアップ。。。したいが @RunWith(Enclosed) + @Unroll + useJUnitPlatform() を組み合わせるとテストが終わらない ) の続きです。

  • 今回の手順で確認できるのは以下の内容です。
    • @RunWith(Enclosed) + @Unroll + useJUnitPlatform() を組み合わせた時にテストが終わらない問題を解決する方法が分かりましたので解決します。
    • 今回の対応で JUnit 4、Spock、JUnit 5 のテストを書いて動かすことが出来るようになります。

参照したサイト・書籍

  1. How to configure gradle to output total number of tests executed?
    https://stackoverflow.com/questions/37173218/how-to-configure-gradle-to-output-total-number-of-tests-executed

  2. How to use JUnit 5 with Gradle
    https://medium.com/@jonashavers/how-to-use-junit-5-with-gradle-fb7c5c3286cc

  3. Apache Groovyチュートリアル - 7.クロージャ
    https://koji-k.github.io/groovy-tutorial/closure/index.html

目次

  1. 解決策
  2. demo プロジェクトで試してみる
    1. build.gradle を変更する
    2. Spock 以外に JUnit 4、JUnit 5 のテストクラスを作成する
    3. 動作確認する
  3. ksbysample-webapp-lending の build.gradle を変更して動作確認する
  4. gradle の test タスクと IntelliJ IDEA の「Run ‘All Tests’」で実行した時のテスト数が異なる原因とは?

手順

解決策

JUnit 5 のモジュールを追加したら junit-vintage-engine を依存関係に追加しないと JUnit 4 のテストを実行できないと思っていたのですが、type: Test のタスク内で useJUnitPlatform() を呼び出さなければ JUnit 4 か Spock のテストのみ実行される(JUnit 5 のテストが実行されない)ということに気づきました。この動作を利用します。

  • test タスクとは別に testJUnit4AndSpock タスクを定義する。
  • testJUnit4AndSpock タスクは type: Test のタスクとして定義する。
  • testRuntime("org.junit.vintage:junit-vintage-engine") は依存関係に追加しない。JUnit 5 の環境下では JUnit 5 のテストのみ実行し、JUnit 4 及び Spock のテストが実行されないようにする。
  • test タスク内では useJUnitPlatform() を記述して JUnit 5 のテストのみ実行する。
  • testJUnit4AndSpock タスク内では useJUnitPlatform() を記述せず JUnit 4 及び Spock のテストのみ実行する。

demo プロジェクトで試してみる

build.gradle を変更する

buildscript {
    ext {
        group "com.example"
        version "1.0.0-RELEASE"
    }
    repositories {
        mavenCentral()
        maven { url "https://repo.spring.io/release/" }
        maven { url "https://plugins.gradle.org/m2/" }
    }
}

plugins {
    id "java"
    id "eclipse"
    id "idea"
    id "org.springframework.boot" version "2.1.3.RELEASE"
    id "io.spring.dependency-management" version "1.0.6.RELEASE"
    id "groovy"
}

sourceCompatibility = 1.8
targetCompatibility = 1.8

[compileJava, compileTestGroovy, compileTestJava]*.options*.encoding = "UTF-8"
[compileJava, compileTestGroovy, compileTestJava]*.options*.compilerArgs = [
        "-Xlint:all,-options,-processing,-path"
]

idea {
    module {
        inheritOutputDirs = false
        outputDir = file("$buildDir/classes/main/")
    }
}

repositories {
    mavenCentral()
}

dependencyManagement {
    imports {
        mavenBom(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES)
        mavenBom("org.junit:junit-bom:5.4.0")
    }
}

dependencies {
    def spockVersion = "1.2-groovy-2.5"

    implementation("org.springframework.boot:spring-boot-starter-web")
    runtimeOnly("org.springframework.boot:spring-boot-devtools")
    implementation("org.apache.commons:commons-lang3")
    testImplementation("org.springframework.boot:spring-boot-starter-test")
    testImplementation("org.spockframework:spock-core:${spockVersion}")
    testImplementation("org.spockframework:spock-spring:${spockVersion}")

    // for JUnit 5
    testCompile("org.junit.jupiter:junit-jupiter")
    testRuntime("org.junit.platform:junit-platform-launcher")
}

def printTestCount = { desc, result ->
    if (!desc.parent) { // will match the outermost suite
        println "Results: ${result.resultType} (${result.testCount} tests, ${result.successfulTestCount} successes, ${result.failedTestCount} failures, ${result.skippedTestCount} skipped)"
    }
}
task testJUnit4AndSpock(type: Test) {
    testLogging {
        events "STARTED", "PASSED", "FAILED", "SKIPPED"
        afterSuite printTestCount
    }
}
test.dependsOn testJUnit4AndSpock
test {
    // for JUnit 5
    useJUnitPlatform()

    testLogging {
        events "STARTED", "PASSED", "FAILED", "SKIPPED"
        afterSuite printTestCount
    }
}
  • dependencies block から testRuntime("org.junit.vintage:junit-vintage-engine") を削除します。
  • task testJUnit4AndSpock(type: Test) { ... } を追加します。
  • test.dependsOn testJUnit4AndSpock を記述し、test タスクの前に testJUnit4AndSpock タスクが実行されるようにします。
  • testJUnit4AndSpock タスクでは JUnit 4 と Spock のテストだけが、test タスクでは JUnit 5 のテストだけが実行されていることを確認するために testJUnit4AndSpock タスク、test タスク内に events "STARTED", "PASSED", "FAILED", "SKIPPED" を記述してテストの開始や終了の状況がコンソールに出力されるようにします。
  • 各タスクで実行されたテストの数をコンソールに出力したいので、How to configure gradle to output total number of tests executed? に記載されていた afterSuite closure を追加します。

Spock 以外に JUnit 4、JUnit 5 のテストクラスを作成する

src/test/java/com/example/demo/SampleUtilsJUnit4Test.java を作成し、以下の内容を記述します。org.junit.Test アノテーションが付与されているので、このテストクラスは JUnit 4 のテストクラスとして認識されます。

package com.example.demo;

import org.junit.Test;

import static org.hamcrest.Matchers.is;
import static org.junit.Assert.*;

public class SampleUtilsJUnit4Test {

    @Test
    public void join() {
        assertThat(SampleUtils.join("a", "b"), is("ab"));
    }

}

src/test/java/com/example/demo/SampleUtilsJUnit5Test.java を作成し、以下の内容を記述します。org.junit.jupiter.api.Test アノテーションが付与されているので、このテストクラスは JUnit 5 のテストクラスとして認識されます。

package com.example.demo;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.*;

class SampleUtilsJUnit5Test {

    @Test
    void join() {
        assertEquals("ab", SampleUtils.join("a", "b"));
    }

}

動作確認する

src/test/groovy/com/example/demo/SampleUtilsTest.groovy は @RunWith(Enclosed) + @Unroll を組み合わせたテストにしています。

package com.example.demo


import org.junit.experimental.runners.Enclosed
import org.junit.runner.RunWith
import spock.lang.Specification
import spock.lang.Unroll

@RunWith(Enclosed)
class SampleUtilsTest {

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

        @Unroll
        def "xxx"() {
            expect:
            SampleUtils.join(a, b) == result

            where:
            a   | b   || result
            "a" | "b" || "ab"
        }

    }

}

clean タスク実行 → Rebuild Project 実行 → build タスクを実行してみると、

f:id:ksby:20190226223417p:plain

  • タスクが全て完了し BUILD SUCCESSFUL のメッセージが出力されました。
  • testJUnit4AndSpock タスクでは JUnit 4 のテストクラスである SampleUtilsJUnit4Test、Spock のテストクラスである SampleUtilsTest の2つだけが実行されています。
  • test タスクでは JUnit 5 のテストクラスである SampleUtilsJUnit5Test だけが実行されています。

期待通りの動作です。これで JUnit4、Spock、JUnit 5 のモジュールを依存関係に追加した環境で、JUnit4、Spock、JUnit 5 のテストを書いてそれぞれのテストが実行できるようになりました。

ksbysample-webapp-lending の build.gradle を変更して動作確認する

build.gradle を以下のように変更します。

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

    // for JUnit 5
    // junit-jupiter で junit-jupiter-api, junit-jupiter-params, junit-jupiter-engine の3つが依存関係に追加される
    testCompile("org.junit.jupiter:junit-jupiter")
    testRuntime("org.junit.platform:junit-platform-launcher")

    ..........
}

// -ea -Dspring.profiles.active=develop -Dfile.encoding=UTF-8 -Dsun.nio.cs.map=x-windows-iso2022jp/ISO-2022-JP
def jvmArgsForTask = [
        "-ea",
        "-Dfile.encoding=UTF-8",
        "-Dsun.nio.cs.map=x-windows-iso2022jp/ISO-2022-JP"
]
def printTestCount = { desc, result ->
    if (!desc.parent) { // will match the outermost suite
        println "Results: ${result.resultType} (${result.testCount} tests, ${result.successfulTestCount} successes, ${result.failedTestCount} failures, ${result.skippedTestCount} skipped)"
    }
}

bootRun {
    jvmArgs = jvmArgsForTask + ["-Dspring.profiles.active=develop"]
}

task testJUnit4AndSpock(type: Test) {
    jvmArgs = jvmArgsForTask + ["-Dspring.profiles.active=unittest"]
    testLogging {
        afterSuite printTestCount
    }
}
test.dependsOn testJUnit4AndSpock
test {
    jvmArgs = jvmArgsForTask + ["-Dspring.profiles.active=unittest"]

    // for JUnit 5
    useJUnitPlatform()

    testLogging {
        afterSuite printTestCount
    }
}
  • dependencies block から testRuntime("org.junit.vintage:junit-vintage-engine") を削除します。
  • bootRun タスク、test タスクに個別に記述していた jvmArgs = [ ... ] から -Dspring.profiles.active=... の部分以外の設定を def jvmArgsForTask = [ ... ] としてタスクの外側で変数として定義し、bootRun タスク、test タスクではこの変数を使用するよう変更します。
  • def printTestCount = { ... } を追加します。
  • task testJUnit4AndSpock(type: Test) { ... }test.dependsOn testJUnit4AndSpock を追加します。
  • test タスクに testLogging { afterSuite printTestCount } を追加します。

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

f:id:ksby:20190227045212p:plain

Project Tool Window で src/test を選択した後コンテキストメニューを表示して「Run ‘All Tests’」を選択すると全てのテストが完了しますが、テスト数が 203 と gradle の build タスクの 141 と 62 も違います。。。なぜ?

f:id:ksby:20190227205256p:plain

gradle の test タスクと IntelliJ IDEA の「Run ‘All Tests’」で実行した時のテスト数が異なる原因とは?

build/reports/tests/testJUnit4AndSpock/index.html のレポートと IntelliJ IDEA の「Run ‘All Tests’」の結果を比較してみていたら、IntelliJ IDEA の方で同じテストが二重にカウントされていました。

f:id:ksby:20190227223510p:plain

demo プロジェクトの方でも Project Tool Window から「Run ‘All Tests’」を実行するとテストの数は 3 しかないのに 6 とカウントされます。

f:id:ksby:20190227224320p:plain

テストを1つずつ試すときちんと1とカウントされます。

f:id:ksby:20190227234112p:plain f:id:ksby:20190227234218p:plain f:id:ksby:20190227234332p:plain

いろいろ試してみた結果、src/test から「Run ‘All Tests’」を実行するとテスト数が 203 となりますが、

f:id:ksby:20190228010126p:plain

src/test/groovy/ksbysample から「Run ‘All Tests’」を実行すると 39、

f:id:ksby:20190228010526p:plain

src/test/java/ksbysample から「Run ‘All Tests’」を実行すると 103 で、合計が 142 となり gradle の build タスクの 141 とほぼ同じになります。テストの実行時間も約 30 秒程度短いので、src/test から実行した時には多い分のテストが実行されているようです。

f:id:ksby:20190228011229p:plain

「Run/Debug Configurations」の「JUnit」の設定を見てもこの原因となるような設定は見当たりませんでした。

よく分かりませんが、今後は src/test からはテストを実行せず src/test/groovy、src/test/java の下のルートパッケージから「Run ‘All Tests’」を実行することにしましょうか。。。

履歴

2019/03/01
初版発行。