CircleCI上でJUnitテストを並列実行して分割されてしまったカバレッジを一つに集約する方法

## はじめに

テスト実行が遅い場合、てっとりばやく高速化するのに有効な方法は並列化だと思います。

しかし、CircleCI上で並列化した場合、JUnitで実行したテストのカバレッジが分割されｔれしまいうまく収集できなかったためその対策をしました。

調べれば解決策となる情報は見つかるのですが、断片的でありそのものズバリな情報は見当たりませんでした。

しかし、同じ課題は他のエンジニアでも必ず遭遇するだろうと感じた＆自分でも振り替えれるように記事にまとめておくことにします。


## 技術スタック
- CircleCI
- Kotlin
  - 筆者のメイン言語だからKotlinなだけで、Javaでも変わりません。
- JUnit 4
  - gradle initで自動で解決されたのが4だったのでそのままにしているだけです。JUnit 5でも変わりません。
- Jacoco

## 概要
1. CircleCI上でJUnit実行時に生成される実行データ(test.exec)に並列化されたそれぞれのテストを通して一意となる名前をつける
    - 例）test1.exec, test2.exec
2. workflowのtest並列実行とcorverage集約＆収集のjobを分ける
3. test並列実行jobにより分割されてしまったカバレッジをcorverage用のjobで結合する

(3)のステップがこの記事のキモです。

## 解説
### 1. CircleCI上でJUnit実行時に生成されるtest.execに並列化されたテスト全体で一意となる名前をつける
```groovy
test {
    jacoco {
        if (System.env.CIRCLE_NODE_INDEX != null) {
            destinationFile = file("$buildDir/jacoco/test${System.env.CIRCLE_NODE_INDEX}.exec") // ①
        }
    }
}
```

1. CIRCLE_NODE_INDEXはCircleCI上で実行時に自動的に設定される環境変数。CircleCIのビルドインスタンスのインデックスが入る。2並列だと0と1になる。

### 2. workflowのtest並列実行とcorverage収集のjobを分ける

circleciのconfig.ymlの該当箇所のみ抜粋します。

なお、並列テストの実行方法自体は解説しません。検索すればたくさん出てきます。

私は[CircleCI実践入門](https://gihyo.jp/book/2020/978-4-297-11411-4)を参考にしました。

```yml
  test:
    parallelism: 4
    docker:
      - image: cimg/openjdk:11.0.8
    steps:
      - checkout
      - restore_cache:
          key: v1-gradle-wrapper-{{ checksum "gradle/wrapper/gradle-wrapper.properties" }}
      - restore_cache:
          key: v1-gradle-cache-{{ checksum "build.gradle" }}
      - run:
          name: テスト実行
          command: |
            cd src/test/kotlin
            CLASSNAMES=$(circleci tests glob "**/*Test.kt" \
              | sed 's@/@.@g' \
              | sed 's/.kt//' \
              | circleci tests split --split-by=timings --timings-type=classname)
            cd ../../..
            GRADLE_ARGS=$(echo $CLASSNAMES | awk '{for (i=1; i<=NF; i++) print "--tests",$i}')
            echo "Prepared arguments for Gradle: $GRADLE_ARGS"
            ./gradlew test $GRADLE_ARGS -Pcircleci
      - persist_to_workspace:
          root: ./build
          paths:
            - jacoco # ①
            - classes # ②
  jacoco:
    docker:
      - image: cimg/openjdk:11.0.8
    steps:
      - checkout
      - restore_cache:
          key: v1-gradle-wrapper-{{ checksum "gradle/wrapper/gradle-wrapper.properties" }}
      - restore_cache:
          key: v1-gradle-cache-{{ checksum "build.gradle" }}
      - attach_workspace:
          at: ./build # ③
      - run:
          name: カバレッジのマージ＆レポート作成
          command: ./gradlew jacocoTestReport # ④
      - store_artifacts:
          path: ./build/reports
```

1. カバレッジを保存する目的のバイナリデータが保存されるディレクトリ
    - ちなみに、このファイルを指して何という名前で呼ぶのが一般的なのか調べてもよくわからず…
2. ビルド後のクラスファイルがないとカバレッジのレポートが正しく出せない。次のジョブでレポートを出力する際に必要になるので次のジョブに渡す。
3. 前のjobから実行データを受け取るために、workspaceをattachする
4. 分散されてしまったカバレッジを集約＆収集して一つレポートにするのはこのタイミングで行われる

## 3. test並列実行jobで収集した分割されてしまったカバレッジをcorverage収集のjobで結合する

```groovy
task jacocoMerge(type: JacocoMerge) {
    FileTree tree = fileTree(dir: 'build/jacoco') // ①
    tree.visit { // ②
        executionData it.file
    }

    destinationFile file("${buildDir}/jacoco/test.exec") // ③
}

jacocoTestReport {
    dependsOn jacocoMerge // ④
    executionData jacocoMerge.destinationFile // ⑤
}
```

1. 実行データ保存されているディレクトリを指定し、実行データのファイル名一覧を取得する
2. すべての実行データを指定し、カバレッジをマージする
3. マージ後のファイル名を指定
4. jacocoTestReportの依存にjacocoMergeタスクを指定
5. マージ後の実行データを指定する

## 最後に
要点としては、build.gradleでカバレッジをマージする、それをcircleciのjob間で共有してマージするというのがポイントでした。

思いつけば意外と簡単なのですが、なぜか私にはすぐに思いつくことができず「いったいどうやるんだ🤔❓」となったので同じような人もいるのではないでしょうか。
少ないかも知れませんが、そういった人たちが最短で答えに辿り着くための助けになれば幸いです。

それから、この記事は周辺要素のせいで迷いが生じないように限りなく要点のみに絞って記事を書いてみています。
そのため、逆にわかりにくい点があるかもしれません。
もしお気づきのことがあればコメントで教えて頂ければ加筆したいと思っていますので、ご連絡頂ければ幸いです。