Kengo's blog

Technical articles about original projects, JVM, Static Analysis and TypeScript.

Gradleのjvm-test-suiteプラグインがテスト周りの定型コードを排除するのに便利そう

Gradle v7.5の時点ではまだIncubating段階の機能ではあるのですが、Gradleの新しいプラグイン jvm-test-suite がいい感じなので紹介します。

docs.gradle.org

解きたい課題:サブモジュールや統合テストが出てくるととたんに面倒になるビルドスクリプト

Gradleは設定をDSLで記述するので基本的には何でもありなのですが、やはり定形コード(boilerplate)は少ないほうがビルドスクリプトの見通しも良くなります。もちろんGradleは「設定より規約(Convention over Configuration)」の考えを持っているため、ある程度は空気を読んでSourceSetやTaskを自動的に生成してくれます。しかしテスト周りにおいてはこうした自動生成は十分ではなく、次に挙げるような課題がありました:

  1. サブプロジェクト全てに対して実行したタスクのレポートを統合するのが面倒。ここでレポートとは単体テスト実行結果、ないしJaCoCoによるカバレッジ測定結果などを指す。
  2. スモークテストや統合テストといった単体テスト以外のテストを実行する際に、SourceSetやConfiguration(依存管理)、Taskなどを自分で書かなくてはならない

例えばJaCoCoのカバレッジ測定結果をSonarQubeに渡すために、SpotBugsプロジェクトでは以下のようなタスクを自分で書いて実行しています。一部はプロジェクト固有の課題を解決するためのコードですが、複雑さが伝わればOKです:

task jacocoRootReport(type: JacocoReport) {
  description = 'Merge all coverage reports before submit to SonarQube'
  def reportTasks = project.getTasksByName("jacocoTestReport", true).minus(rootProject.jacocoTestReport)
  dependsOn reportTasks

  executionData.setFrom reportTasks.executionData
  sourceDirectories.setFrom reportTasks.sourceDirectories

  // Only enable class directories related to non-test project
  classDirectories.setFrom files(reportTasks.classDirectories).filter {
    !it.toString().contains("-test") && !it.toString().contains("Test") && !it.toString().contains("junit")
  }

  reports {
    // JaCoCo SonarQube plugin needs a XML file to parse
    // https://docs.sonarqube.org/display/PLUG/JaCoCo+Plugin
    xml.required = true
  }
}

jvm-test-suiteプラグインの新規性

jvm-test-suite プラグインは java-library など既存のプラグインを導入しているプロジェクトであれば既に有効化されています。既存のプラグイン達とのシームレスな統合を前提に開発されていると言っていいでしょう。

また別種のテスト追加やレポートの統合機能もこれに統合されており、例えば前述のJaCoCoレポート統合は test-report-aggregation プラグインによって提供されますが、 jvm-test-suite プラグインの提供するレールに乗っていれば testCodeCoverageReport というTaskが自動的に生成されます。先述の定形コードがまるっと削除できるわけです。

後述するSourceSetやConfiguraitonの自動生成と合わせて、多くの定形コードを排除するのに役立ちそうです。

jvm-test-suiteプラグインの提供するレール

jvm-test-suiteプラグインを適用する上で注意すべきはひとつ、テストの実行に必要な情報を testing Extensionに集約することです。

詳しい説明は公式サイトに譲りますが、例として spotbugs-gradle-plugin で試している設定は以下のようなものになりました:

testing {
    suites {
        val test by getting(JvmTestSuite::class) {
            useJUnitJupiter()
            dependencies {
                implementation(gradleTestKit()) // Gradle v7.6+ が必要。 https://github.com/gradle/gradle/issues/19849
            }
            targets {
                all {
                    testTask.configure {
                        maxParallelForks = Runtime.getRuntime().availableProcessors()
                    }
                }
            }
        }
        val functionalTest by registering(JvmTestSuite::class) {
            useSpock()
            testType.set(TestSuiteType.FUNCTIONAL_TEST)
            targets {
                all {
                    testTask.configure {
                        description = "Runs the functional tests."
                    }
                }
            }
        }
    }
}

これだけで functionalTest Taskと、その実行に必要なSourceSetとConfiguraitonが自動的に生成されます。useJUnitJupiter(), useJUnit(), useSpock(), useTestNG()など依存やバージョンを一括管理してくれる便利メソッドもある(Gradle 7.6からuseKotlinTest() も増えそう)ため、今後は testImplemenentation などのConfiguraitonを自分で設定する機会も激減しそうです。

まとめ

jvm-test-suite プラグインを利用することで、ちょっと複雑なGradleプロジェクトでは頻出だったテスト周りの定形コードの多くを省くことができます。 サンプルプロジェクトがGradle公式から提供されているので、興味のある方は確認してみてください!

github.com