Gradleを使用したマルチプロジェクトのカバレッジレポート一括出力(JaCoCo Report Aggregation Plugin)

サブプロジェクトのカバレッジレポートを1つにまとめて出力させるにはJaCoCo Report Aggregation Pluginを使用する。

カバレッジレポート一括出力にあたって、SpringBootプロジェクトを使用していてハマった所があるので、以下の実装手順もSpringBootプロジェクトで説明していく。

また、下記の2パターンのマルチプロジェクトの例を記載する。
(書き方は基本的には一緒なので似たような説明が2回記載されているが、どっちでも使えることを示すための記載である。)

プロジェクト構成

パターン①

サブプロジェクトで共通の設定を「aa/build.gradle」に記載しているプロジェクト。

aa
 |
 |ーー aa-common
 |           |ーー build.gradle
 |
 |ーー aa-webapp
 |           |ーー build.gradle
 |
 |ーー aa-functions
 |           |ーー build.gradle
 |
 |ーー build.gradle

aa/build.gradle
※依存関係のバージョンは「gradle.properties」に定義

plugins {
  id 'java'
  id 'eclipse'
  id 'org.springframework.boot' version "${spring_boot_version}" apply false
  id 'io.spring.dependency-management' version "${spring_dependency_management_version}" apply false
}

jar {
  // ルートプロジェクトなのでjarを作成しない
  enabled = false
}

// サブプロジェクトで共通の設定・定義を記述する
subprojects {
  apply plugin: 'java'
  apply plugin: 'java-library'
  apply plugin: "eclipse"
  apply plugin: 'project-report'
  apply plugin: 'io.spring.dependency-management'

  // バージョン
  version = '1.0.0'

  // Javaのバージョン指定
  java.sourceCompatibility = JavaLanguageVersion.of(17)
  java.targetCompatibility = JavaLanguageVersion.of(17)

  // コンパイル時のエンコーディング指定
  [compileJava, compileTestJava]*.options*.encoding = "UTF-8"

  repositories {
    mavenCentral()
  }

  // コンパイル時にAnnotation Processor(lombok)を有効にする
  configurations {
    compileOnly {
      extendsFrom annotationProcessor
    }
    testCompileOnly {
      extendsFrom testAnnotationProcessor
    }
  }

  dependencies {
    // Spring
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
    implementation 'org.springframework.retry:spring-retry'
    implementation 'org.springframework.boot:spring-boot-starter-aop'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'org.springframework.security:spring-security-test'

    // lombok
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    testCompileOnly 'org.projectlombok:lombok'
    testAnnotationProcessor 'org.projectlombok:lombok'

    // JUnit
    testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:${junit_jupiter_version}"
    testImplementation "org.junit.jupiter:junit-jupiter-api:${junit_jupiter_version}"

    // H2
    testImplementation "com.h2database:h2:${h2_version}"

    // WireMock
    testImplementation "com.github.tomakehurst:wiremock-jre8:${wiremock_version}"
  }

  tasks {
    withType(Test) {
      // Junit5有効化
      useJUnitPlatform()

      testLogging {
        // テスト時の標準出力と標準エラー出力を表示しない
        showStandardStreams false

        // イベントを出力 (TestLogEvent)
        events 'started', 'skipped', 'passed', 'failed'

        // 例外発生時の出力設定 (TestExceptionFormat)
        //exceptionFormat 'full'
      }
      systemProperty "file.encoding", "UTF-8"
    }
  }
}

パターン②

サブプロジェクトで共通の設定を独自プラグインjava-common.gradle)に記載しているプロジェクト。

bb
 |
 |ーー buildSrc
 |           |ーー build.gradle 
 |           |ーー src/main/groovy
 |           |           |ーーjava-common.gradle
 |
 |ーー bb-common
 |           |ーー build.gradle
 |
 |ーー bb-webapp
 |           |ーー build.gradle
 |
 |ーー bb-functions
 |           |ーー build.gradle
 |
 |ーー build.gradle

bb/buildSrc/src/main/groovy/java-common.gradle

plugins {
  id 'java'
  id 'eclipse'
  id 'project-report'
  id 'org.springframework.boot'
  id 'io.spring.dependency-management'
}

// Javaのバージョン指定
sourceCompatibility = 17
targetCompatibility = 17

// コンパイル時のエンコーディング指定
[compileJava, compileTestJava]*.options*.encoding = "UTF-8"

// グループIDの設定
group = 'jp.co.sample.bb'

repositories {
  mavenCentral()
}

// コンパイル時にAnnotation Processor(lombok,Doma)を有効にする
configurations {
  compileOnly {
    extendsFrom annotationProcessor
  }
  testCompileOnly {
    extendsFrom testAnnotationProcessor
  }
}

dependencies {
  // Spring
  implementation 'org.springframework.boot:spring-boot-starter-web'
  implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
  implementation 'org.springframework.retry:spring-retry'
  implementation 'org.springframework.boot:spring-boot-starter-aop'
  testImplementation 'org.springframework.boot:spring-boot-starter-test'
  testImplementation 'org.springframework.security:spring-security-test'

  // lombok
  compileOnly 'org.projectlombok:lombok'
  annotationProcessor 'org.projectlombok:lombok'
  testCompileOnly 'org.projectlombok:lombok'
  testAnnotationProcessor 'org.projectlombok:lombok'

  // JUnit
  testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:5.9.1"
  testImplementation "org.junit.jupiter:junit-jupiter-api:5.9.1"

  // H2
  testImplementation "com.h2database:h2:2.1.214"

  // WireMock
  testImplementation "com.github.tomakehurst:wiremock-jre8:2.35.1"
}

tasks.named('test') {
  // Junit5有効化
  useJUnitPlatform()

  testLogging {
    // テスト時の標準出力と標準エラー出力を表示しない
    showStandardStreams false

    // イベントを出力 (TestLogEvent)
    events 'started', 'skipped', 'passed', 'failed'

    // 例外発生時の出力設定 (TestExceptionFormat)
    //exceptionFormat 'full'
  }
  systemProperty "file.encoding", "UTF-8"
}

bb/build.gradle

plugins {
    id 'java'
}

実装方法

パターン①

スタンドアロンのプラグイン実装サンプルを基に、設定を追加していく。

1. プラグインの追加

ルートプロジェクトのbuild.gradleに「jacoco-report-aggregationプラグインを追加する。

aa/build.gradle

plugins {
  id 'java'
  id 'eclipse'
  id 'org.springframework.boot' version "2.7.18" apply false
  id 'io.spring.dependency-management' version "1.1.0" apply false
  // ↓追加
  id 'jacoco-report-aggregation'
  // ↑追加
}

また、実装サンプルの説明内に以下の記述があるため、サブプロジェクトに「The JaCoCo Plugin」のプラグインを導入する。

All three projects apply the jacoco plugin, and application consumes both list and utilities via its implementation configuration.

aa/build.gradle

subprojects {
  apply plugin: 'java'
  apply plugin: 'java-library'
  apply plugin: "eclipse"
  apply plugin: 'project-report'
  apply plugin: 'io.spring.dependency-management'
  // ↓追加
  apply plugin: 'jacoco'
  // ↑追加
}

2. 依存関係を取得してくるリポジトリの指定

ルートプロジェクトにリポジトリの設定が記載されていない場合は、記載を追加する。
サブプロジェクトブロック内に記載してあるものはサブプロジェクトに適用する設定のため、そこではなくルートプロジェクト自体に適用されるように記載する。

aa/build.gradle

repositories {
  mavenCentral()
}

3. ユニットテストの結果を基にカバレッジレポートを出力する

一括出力する対象のテストとしてユニットテストを指定するように設定を追加する。

aa/build.gradle

reporting {
  reports {
    testCodeCoverageReport(JacocoCoverageReport) { 
      // Junitのカバレッジレポートを出力する
      testType = TestSuiteType.UNIT_TEST
    }
  }
}

4. カバレッジ集計対象とするプロジェクトを指定する

ルートプロジェクトの依存関係に、カバレッジレポート出力対象としたいサブプロジェクトを指定する。
ここに記載していないプロジェクトもカバレッジレポートには出力されるが、カバレッジが集計されていないので全て0%となってしまうので注意する。

aa/build.gradle

dependencies {
  // カバレッジレポート出力対象のプロジェクト
  jacocoAggregation project(':aa-common')
  jacocoAggregation project(':aa-functions')
  jacocoAggregation project(':aa-webapp')
}

5. カバレッジレポートの出力先を設定する

デフォルトでは、カバレッジレポートはルートプロジェクトの「build\reports\jacoco\testCodeCoverageReport」配下に出力される。
※ルートプロジェクトなのは、プラグインをルートプロジェクトに導入しているため

testCodeCoverageReportにプロパティを追加することでカバレッジレポートの出力場所を変更することができる。
例えば、以下のように設定した場合、ルートプロジェクトの「build\reports\coverage」配下にカバレッジレポートが出力される。

aa/build.gradle

testCodeCoverageReport {
  reports {
    // XML形式のレポートを出力するかどうか 
    xml.required = true
    // XML形式のレポートの出力先(ファイル名まで指定)
    xml.outputLocation = reporting.baseDirectory.file('coverage/jacocoTestReport.xml')

    // HTML形式のレポートを出力するかどうか
    html.required = true
    // HTML形式のレポートの出力先(ディレクトリを指定)
    html.outputLocation = reporting.baseDirectory.dir('coverage/html')
  }
}

6. 「check」タスクにレポート出力タスクを紐づける

5までの設定で、カバレッジレポートを出力することができるが、「build」タスクで実行されるどのタスクとも紐づいていないため、「build」タスクを実行してもレポートが出力されない。
ここでは、「check」タスクにカバレッジレポート出力タスクを紐づけるための設定を追加する。

aa/build.gradle

// checkタスク実行時にカバレッジレポートを出力する
tasks.named('check') {
  dependsOn tasks.named('testCodeCoverageReport', JacocoReport) 
}

7. ビルドしてみる

ここまでで、公式の実装サンプルに記載されている設定は全て完了した。
「aa/build.gradle」は以下のようになっている想定。

plugins {
  id 'java'
  id 'eclipse'
  id 'org.springframework.boot' version "2.7.18" apply false
  id 'io.spring.dependency-management' version "1.1.0" apply false
  id 'jacoco-report-aggregation'
}

jar {
  // ルートプロジェクトなのでjarを作成しない
  enabled = false
}

reporting {
  reports {
    testCodeCoverageReport(JacocoCoverageReport) { 
      // Junitのカバレッジレポートを出力する
      testType = TestSuiteType.UNIT_TEST
    }
  }
}

dependencies {
  // カバレッジレポート出力対象のプロジェクト
  jacocoAggregation project(':aa-common')
  jacocoAggregation project(':aa-functions')
  jacocoAggregation project(':aa-webapp')
}

testCodeCoverageReport {
  reports {
    // XML形式のレポートを出力するかどうか 
    xml.required = true
    // XML形式のレポートの出力先(ファイル名まで指定)
    xml.outputLocation = reporting.baseDirectory.file('coverage/jacocoTestReport.xml')

    // HTML形式のレポートを出力するかどうか
    html.required = true
    // HTML形式のレポートの出力先(ディレクトリを指定)
    html.outputLocation = reporting.baseDirectory.dir('coverage/html')
  }
}

// checkタスク実行時にカバレッジレポートを出力する
tasks.named('check') {
  dependsOn tasks.named('testCodeCoverageReport', JacocoReport) 
}

repositories {
  mavenCentral()
}

subprojects {
  apply plugin: 'java'
  apply plugin: 'java-library'
  apply plugin: "eclipse"
  apply plugin: 'project-report'
  apply plugin: 'io.spring.dependency-management'
  apply plugin: 'jacoco'

  // バージョン
  version = '1.0.0'

  // Javaのバージョン指定
  java.sourceCompatibility = JavaLanguageVersion.of(17)
  java.targetCompatibility = JavaLanguageVersion.of(17)

  // コンパイル時のエンコーディング指定
  [compileJava, compileTestJava]*.options*.encoding = "UTF-8"

  repositories {
    mavenCentral()
  }
  
  // コンパイル時にAnnotation Processor(lombok)を有効にする
  configurations {
    compileOnly {
      extendsFrom annotationProcessor
    }
    testCompileOnly {
      extendsFrom testAnnotationProcessor
    }
  }

  dependencies {
    // Spring
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
    implementation 'org.springframework.retry:spring-retry'
    implementation 'org.springframework.boot:spring-boot-starter-aop'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testImplementation 'org.springframework.security:spring-security-test'

    // lombok
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    testCompileOnly 'org.projectlombok:lombok'
    testAnnotationProcessor 'org.projectlombok:lombok'

    // JUnit
    testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:${junit_jupiter_version}"
    testImplementation "org.junit.jupiter:junit-jupiter-api:${junit_jupiter_version}"

    // H2
    testImplementation "com.h2database:h2:${h2_version}"

    // WireMock
    testImplementation "com.github.tomakehurst:wiremock-jre8:${wiremock_version}"
  }

  tasks {
    withType(Test) {
      // Junit5有効化
      useJUnitPlatform()

      testLogging {
        // テスト時の標準出力と標準エラー出力を表示しない
        showStandardStreams false

        // イベントを出力 (TestLogEvent)
        events 'started', 'skipped', 'passed', 'failed'

        // 例外発生時の出力設定 (TestExceptionFormat)
        //exceptionFormat 'full'
      }
      systemProperty "file.encoding", "UTF-8"
    }
  }
}

一度ビルドを実行して、カバレッジレポートが出力できるかどうかを検証する。
と失敗する・・・
「testCodeCoverageReport」タスクで依存関係の解決に失敗している模様。
解決に失敗している依存関係を見ていくと、SpringBoot関連の依存関係が多い。

> Task :testCodeCoverageReport FAILED

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':testCodeCoverageReport'.
> Could not resolve all files for configuration ':allCodeCoverageReportClassDirectories'.
   > Could not find org.springframework.boot:spring-boot-starter-validation:.
     Required by:
         project : > project :aa-common
   > Could not find org.springframework.boot:spring-boot-starter-web:.
     Required by:
         project : > project :aa-common
         project : > project :aa-batch
         project : > project :aa-webapp
         project : > project :aa-webservice
   > Could not find org.springframework.boot:spring-boot-starter-oauth2-client:.
     Required by:
         project : > project :aa-common
         project : > project :aa-batch
         project : > project :aa-webapp
         project : > project :aa-webservice
   > Could not find org.springframework.retry:spring-retry:.
     Required by:
         project : > project :aa-common
         project : > project :aa-batch
         project : > project :aa-webapp
         project : > project :aa-webservice
   > Could not find org.springframework.boot:spring-boot-starter-aop:.
     Required by:
         project : > project :aa-common
         project : > project :aa-batch
         project : > project :aa-webapp
         project : > project :aa-webservice
   > Could not find com.azure.spring:spring-cloud-azure-starter-storage-blob:.
     Required by:
         project : > project :aa-common
         project : > project :aa-batch
         project : > project :aa-webapp
         project : > project :aa-webservice
   > Could not find org.springframework.boot:spring-boot-starter-cache:.
     Required by:
         project : > project :aa-common
   > Could not find org.springframework.boot:spring-boot-starter-webflux:.
     Required by:
         project : > project :aa-batch
   > Could not find org.springframework.boot:spring-boot-starter-actuator:.
     Required by:
         project : > project :aa-webapp
   > Could not find org.springframework.boot:spring-boot-starter-thymeleaf:.
     Required by:
         project : > project :aa-webapp
   > Could not find org.thymeleaf.extras:thymeleaf-extras-springsecurity5:.
     Required by:
         project : > project :aa-webapp

* Try:
> Run with --stacktrace option to get the stack trace.
> Run with --info or --debug option to get more log output.
> Run with --scan to get full insights.

* Get more help at https://help.gradle.org

BUILD FAILED in 25s
45 actionable tasks: 36 executed, 9 up-to-date

8. エラーを解消する

上でエラーとなっている依存関係の共通点として、build.gradleでバージョンを明示的に記載していない、という共通点がある。
SpringBootのプラグインを導入しているプロジェクトでは、SpringBoot関連の依存関係は自動で適切なバージョンが導入されるようになっているため、バージョン指定を省略することができる。

バージョンの記載を省略している依存関係に、明示的にバージョンを指定するようにすると、エラーが解消されてルートプロジェクトの「build\reports\coverage」配下にカバレッジレポートが出力される。
※依存関係のバージョンはルートのプロジェクトだけではなく、サブプロジェクト全てで修正が必要なので注意

implementation 'org.springframework.boot:spring-boot-starter-web'
  ↓バージョンを追加
implementation 'org.springframework.boot:spring-boot-starter-web:2.7.18'

全てバージョンをベタ書きしてしまうと、SpringBootのバージョンを上げる際に、変更箇所が多くて大変になってしまうため、Version Catalogの使用を検討すると良いかもしれない。

Version Catalogについては以下の記事参照。:

olafnosuke.hatenablog.com

9. 除外対象のクラスを指定する

現状では全てのクラスがカバレッジ取得対象となっているため、Entityクラスなどカバレッジの取得対象から除外したいクラスを除外するための設定を追加する。
各サブプロジェクトのビルド生成物が格納されているclasses配下のパスを指定する。

aa/build.gradle

// 除外対象のクラス
def jacocoExclude = [
  // カバレッジレポートを見て、テストクラスも一覧に出ていたら追加する
  "**/test/**",
   // Java Config
  "**/jp/co/sample/aa/webapp/config/**"
]

testCodeCoverageReport {
  // ↓追加
  // カバレッジ取得対象外とするパスを追加
  getClassDirectories().setFrom(files(
    subprojects.collect {it.fileTree(dir: "${it.buildDir}/classes", exclude: jacocoExclude)}
  ))
  // ↑追加

  reports {
    // XML形式のレポートを出力するかどうか 
    xml.required = true
    // XML形式のレポートの出力先(ファイル名まで指定)
    xml.outputLocation = reporting.baseDirectory.file('coverage/jacocoTestReport.xml')

    // HTML形式のレポートを出力するかどうか
    html.required = true
    // HTML形式のレポートの出力先(ディレクトリを指定)
    html.outputLocation = reporting.baseDirectory.dir('coverage/html')
  }
}

カバレッジの取得対象から除外する方法には、build.gradleで除外する方法以外にもアノテーションGenerated)を付与する方法もある。
参考:Exclusions from Jacoco Report

また、Lombokで自動生成されるソースをカバレッジ取得の対象外としたい場合は、プロジェクトのルートに「lombok.config」という名前のファイルを作成して、中に以下を記載することで除外することができる。

lombok.addLombokGeneratedAnnotation = true

パターン②

スタンドアロンのプラグイン実装サンプルを基に、設定を追加していく。

1. プラグインの追加

ルートプロジェクトのbuild.gradleに「jacoco-report-aggregationプラグインを追加する。

bb/build.gradle

plugins {
  id 'java'
  id 'eclipse'
  // ↓追加
  id 'jacoco-report-aggregation'
  // ↑追加
}

また、実装サンプルの説明内に以下の記述があるため、独自プラグインに「The JaCoCo Plugin」のプラグインを導入する。

All three projects apply the jacoco plugin, and application consumes both list and utilities via its implementation configuration.

bb/buildSrc/src/main/groovy/java-common.gradle

plugins {
  id 'java'
  id 'eclipse'
  id 'project-report'
  id 'org.springframework.boot'
  id 'io.spring.dependency-management'
  // ↓追加
  id 'jacoco'
  // ↑追加
}

2. 依存関係を取得してくるリポジトリの指定

ルートプロジェクトにリポジトリの設定が記載されていない場合は、記載を追加する。

bb/build.gradle

repositories {
  mavenCentral()
}

3. ユニットテストの結果を基にカバレッジレポートを出力する

一括出力する対象のテストとしてユニットテストを指定するように設定を追加する。

bb/build.gradle

reporting {
  reports {
    testCodeCoverageReport(JacocoCoverageReport) { 
      // Junitのカバレッジレポートを出力する
      testType = TestSuiteType.UNIT_TEST
    }
  }
}

4. カバレッジ集計対象とするプロジェクトを指定する

ルートプロジェクトの依存関係に、カバレッジレポート出力対象としたいサブプロジェクトを指定する。
ここに記載していないプロジェクトもカバレッジレポートには出力されるが、カバレッジが集計されていないので全て0%となってしまうので注意する。

bb/build.gradle

dependencies {
  // カバレッジレポート出力対象のプロジェクト
  jacocoAggregation project(':bb-common')
  jacocoAggregation project(':bb-functions')
  jacocoAggregation project(':bb-webapp')
}

5. カバレッジレポートの出力先を設定する

デフォルトでは、カバレッジレポートはルートプロジェクトの「build\reports\jacoco\testCodeCoverageReport」配下に出力される。
※ルートプロジェクトなのは、プラグインをルートプロジェクトに導入しているため

testCodeCoverageReportにプロパティを追加することでカバレッジレポートの出力場所を変更することができる。
例えば、以下のように設定した場合、ルートプロジェクトの「build\reports\coverage」配下にカバレッジレポートが出力される。

bb/build.gradle

testCodeCoverageReport {
  reports {
    // XML形式のレポートを出力するかどうか 
    xml.required = true
    // XML形式のレポートの出力先(ファイル名まで指定)
    xml.outputLocation = reporting.baseDirectory.file('coverage/jacocoTestReport.xml')

    // HTML形式のレポートを出力するかどうか
    html.required = true
    // HTML形式のレポートの出力先(ディレクトリを指定)
    html.outputLocation = reporting.baseDirectory.dir('coverage/html')
  }
}

6. 「check」タスクにレポート出力タスクを紐づける

5までの設定で、カバレッジレポートを出力することができるが、「build」タスクで実行されるどのタスクとも紐づいていないため、「build」タスクを実行してもレポートが出力されない。
ここでは、「check」タスクにカバレッジレポート出力タスクを紐づけるための設定を追加する。

bb/build.gradle

// checkタスク実行時にカバレッジレポートを出力する
tasks.named('check') {
  dependsOn tasks.named('testCodeCoverageReport', JacocoReport) 
}

7. ビルドしてみる

ここまでで、公式の実装サンプルに記載されている設定は全て完了した。 「bb/build.gradle」は以下のようになっている想定。

plugins {
  id 'java'
  id 'eclipse'
  id 'jacoco-report-aggregation'
}

repositories {
  mavenCentral()
}

reporting {
  reports {
    testCodeCoverageReport(JacocoCoverageReport) { 
      // Junitのカバレッジレポートを出力する
      testType = TestSuiteType.UNIT_TEST
    }
  }
}

dependencies {
  // カバレッジレポート出力対象のプロジェクト
  jacocoAggregation project(':bb-common')
  jacocoAggregation project(':bb-functions')
  jacocoAggregation project(':bb-webapp')
}

testCodeCoverageReport {
  reports {
    // XML形式のレポートを出力するかどうか 
    xml.required = true
    // XML形式のレポートの出力先(ファイル名まで指定)
    xml.outputLocation = reporting.baseDirectory.file('coverage/jacocoTestReport.xml')

    // HTML形式のレポートを出力するかどうか
    html.required = true
    // HTML形式のレポートの出力先(ディレクトリを指定)
    html.outputLocation = reporting.baseDirectory.dir('coverage/html')
  }
}

// checkタスク実行時にカバレッジレポートを出力する
tasks.named('check') {
  dependsOn tasks.named('testCodeCoverageReport', JacocoReport) 
}

一度ビルドを実行して、カバレッジレポートが出力できるかどうかを検証する。 と失敗する・・・
「testCodeCoverageReport」タスクで依存関係の解決に失敗している模様。
解決に失敗している依存関係を見ていくと、SpringBoot関連の依存関係が多い。

> Task :testCodeCoverageReport FAILED

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':testCodeCoverageReport'.
> Could not resolve all files for configuration ':allCodeCoverageReportClassDirectories'.
   > Could not find org.springframework.boot:spring-boot-starter-validation:.
     Required by:
         project : > project :bb-common
   > Could not find org.springframework.boot:spring-boot-starter-web:.
     Required by:
         project : > project :bb-common
         project : > project :bb-batch
         project : > project :bb-webapp
         project : > project :bb-webservice
   > Could not find org.springframework.boot:spring-boot-starter-oauth2-client:.
     Required by:
         project : > project :bb-common
         project : > project :bb-batch
         project : > project :bb-webapp
         project : > project :bb-webservice
   > Could not find org.springframework.retry:spring-retry:.
     Required by:
         project : > project :bb-common
         project : > project :bb-batch
         project : > project :bb-webapp
         project : > project :bb-webservice
   > Could not find org.springframework.boot:spring-boot-starter-aop:.
     Required by:
         project : > project :bb-common
         project : > project :bb-batch
         project : > project :bb-webapp
         project : > project :bb-webservice
   > Could not find com.azure.spring:spring-cloud-azure-starter-storage-blob:.
     Required by:
         project : > project :bb-common
         project : > project :bb-batch
         project : > project :bb-webapp
         project : > project :bb-webservice
   > Could not find org.springframework.boot:spring-boot-starter-cache:.
     Required by:
         project : > project :bb-common
   > Could not find org.springframework.boot:spring-boot-starter-webflux:.
     Required by:
         project : > project :bb-batch
   > Could not find org.springframework.boot:spring-boot-starter-actuator:.
     Required by:
         project : > project :bb-webapp
   > Could not find org.springframework.boot:spring-boot-starter-thymeleaf:.
     Required by:
         project : > project :bb-webapp
   > Could not find org.thymeleaf.extras:thymeleaf-extras-springsecurity5:.
     Required by:
         project : > project :bb-webapp

* Try:
> Run with --stacktrace option to get the stack trace.
> Run with --info or --debug option to get more log output.
> Run with --scan to get full insights.

* Get more help at https://help.gradle.org

BUILD FAILED in 25s
45 actionable tasks: 36 executed, 9 up-to-date

8. エラーを解消する

上でエラーとなっている依存関係の共通点として、build.gradleでバージョンを明示的に記載していない、という共通点がある。
SpringBootのプラグインを導入しているプロジェクトでは、SpringBoot関連の依存関係は自動で適切なバージョンが導入されるようになっているため、バージョン指定を省略することができる。

バージョンの記載を省略している依存関係に、明示的にバージョンを指定するようにすると、エラーが解消されて「bb\build\reports\jacoco\testCodeCoverageReport」配下にカバレッジレポートが出力される。
※ルートのプロジェクトだけではなく、サブプロジェクト全てで修正が必要なので注意する

implementation 'org.springframework.boot:spring-boot-starter-web'
  ↓バージョンを追加
implementation 'org.springframework.boot:spring-boot-starter-web:2.7.18'

全てバージョンをベタ書きしてしまうと、SpringBootのバージョンを上げる際に、変更箇所が多くて大変になってしまうため、Version Catalogの使用を検討すると良いかもしれない。

Version Catalogについては以下の記事参照。:

olafnosuke.hatenablog.com

9. 除外対象のクラスを指定する

現状では全てのクラスがカバレッジ取得対象となっているため、Entityクラスなどカバレッジの取得対象から除外したいクラスを除外するための設定を追加する。
各サブプロジェクトのビルド生成物が格納されているclasses配下のパスを指定する。

aa/build.gradle

// 除外対象のクラス
def jacocoExclude = [
  // カバレッジレポートを見て、テストクラスも一覧に出ていたら追加する
  "**/test/**",
   // Java Config
  "**/jp/co/sample/bb/webapp/config/**"
]

testCodeCoverageReport {
  // ↓追加
  // カバレッジ取得対象外とするパスを追加
  getClassDirectories().setFrom(files(
    subprojects.collect {it.fileTree(dir: "${it.buildDir}/classes", exclude: jacocoExclude)}
  ))
  // ↑追加

  reports {
    // XML形式のレポートを出力するかどうか 
    xml.required = true
    // XML形式のレポートの出力先(ファイル名まで指定)
    xml.outputLocation = reporting.baseDirectory.file('coverage/jacocoTestReport.xml')

    // HTML形式のレポートを出力するかどうか
    html.required = true
    // HTML形式のレポートの出力先(ディレクトリを指定)
    html.outputLocation = reporting.baseDirectory.dir('coverage/html')
  }
}

build.gradleで除外する方法以外にもアノテーションGenerated)を付与する方法もある。
参考:Exclusions from Jacoco Report

また、Lombokで自動生成されるソースを対象外としたい場合は、プロジェクトのルートに「lombok.config」という名前のファイルを作成して、中に以下の記載することで除外することができる。

lombok.addLombokGeneratedAnnotation = true

注意ポイント

マルチプロジェクト内で、パッケージ名もクラス名も全く同じクラスが存在する場合、カバレッジレポートの出力に失敗してしまうので注意する。

build.gradleに記載する除外設定は、パッケージ配下のパスしか指定できないため、1つのプロジェクトを除いて同名のクラスを除外する場合は以下のような、カバレッジ対象から除外させるアノテーションGenerated)を付与するのが良い。
参考:Exclusions from Jacoco Report

この対処法は、ある程度開発が進んでしまっていて、パッケージやクラス名の変更にためらわれる場合のみに推奨される。

新規開発の場合は、全く同じパッケージ・クラス名となるクラスを複数作成しないように留意するのが望ましい。