理系学生日記

おまえはいつまで学生気分なのか

GitLabでJavaのソースコードのテストカバレッジを可視化する

テストを書こうと言っても、精神論ではなかなかその文化は育ちません。 これを行おうとすると、自分の記述したテストがどのようにプロジェクトやチームに貢献しているのかを可視化する必要があります。

今日のゴールは2つあり、1つはMerge Requestのファイルタブ上でテストのカバー範囲を可視化することです。

そしてもう1つは、Merge Request上でカバレッジが何%なのかを表示することです。

JaCoCoの組み込み

Javaにおいてカバレッジを解析してくれるライブラリにJaCoCoがあります。

JaCoCoについてはMaven Plug-inが存在しています。 このため、pom.xmlで設定すれば、カバレッジのレポートを取ることが可能になります。

以下がjacoco-maven-pluginを設定するpom.xmlです。

diff --git a/app/movie-uploader/pom.xml b/app/movie-uploader/pom.xml
index 07b9927..82e2305 100644
--- a/app/movie-uploader/pom.xml
+++ b/app/movie-uploader/pom.xml
@@ -180,6 +180,26 @@
           </dependency>
         </dependencies>
       </plugin>
+
+      <plugin>
+        <groupId>org.jacoco</groupId>
+        <artifactId>jacoco-maven-plugin</artifactId>
+        <version>0.8.7</version>
+        <executions>
+          <execution>
+            <goals>
+              <goal>prepare-agent</goal>
+            </goals>
+          </execution>
+          <execution>
+            <id>report</id>
+            <phase>test</phase>
+            <goals>
+              <goal>report</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
     </plugins>
   </build>

mvn testを実行すればカバレッジレポートが出力されるようになります。

$ mvn test
(略)
[INFO] --- jacoco-maven-plugin:0.8.7:report (report) @ movieuploader ---
[INFO] Loading execution data file /Users/kiririmode/src/github.com/kiririmode/hobby/app/movie-uploader/target/jacoco.exec
[INFO] Analyzed bundle 'movieuploader' with 6 classes

テストカバレッジの可視化

GitLabのTest Coverage Visualization機能

GitLabには、Merge Requestで表示されるソース行をテストがカバーしたのか否かを可視化してくれる機能があります。

この機能は、.gitlab-ci.ymlにてCovertura形式のファイルをartifacts:reports:coverage_reportで指定することにより有効になります。

JaCoCoからCovertura形式への変更

しかし冒頭に記載した通り、ぼくが使っているのはJaCoCoです。従って、JaCoCoからCovertura形式のカバレッジレポートへと変換しなければなりません。

JaCoCo形式からCovertura形式への変換機能を有するコンテナはGitLabやDockerHubでホストされているので、これを利用すれば良いでしょう。

Dockerイメージ上に含まれているのはcover2cover.pyで、引数は以下のようでした。

  1. JaCoCoのレポートへのパス
  2. Javaのソースルートへのパス

これをgitlab-ci.ymlに落とすと以下のcoverageジョブのようになります。

unittest:
  stage: test
  image: maven:3.8.4-openjdk-17-slim
  (略)
  script:
    - mvn ${MAVEN_CLI_OPTS} -f app/movie-uploader/pom.xml test
  (略)
  artifacts:
    paths:
      - app/movie-uploader/target/site/jacoco/jacoco.xml

coverage:
  stage: visualize
  image: registry.gitlab.com/haynes/jacoco2cobertura:1.0.8
  variables:
    jacocoreport: app/movie-uploader/target/site/jacoco/jacoco.xml
  script:
    - python /opt/cover2cover.py ${jacocoreport} ${CI_PROJECT_DIR}/app/movie-uploader/src/main/java/ > cobertura.xml
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
      changes:
        - "app/**/*"
  artifacts:
    reports:
      coverage_report:
        coverage_format: cobertura
        path: cobertura.xml

これを実行すると、きちんとテストのカバー範囲が表示されるようになりました。

Merge Requestへのカバレッジの表示

さらにMerge Request上で、現在のテストカバレッジが何%なのかという表示も行ってみます。

JaCoCoのXML形式のレポートには、末尾にC1やC2カバレッジの元となる数値が記録されています。

$ xmllint --format target/site/jacoco/jacoco.xml | tail -8
  </package>
  <counter type="INSTRUCTION" missed="266" covered="123"/>
  <counter type="BRANCH" missed="10" covered="4"/>
  <counter type="LINE" missed="71" covered="25"/>
  <counter type="COMPLEXITY" missed="23" covered="9"/>
  <counter type="METHOD" missed="18" covered="7"/>
  <counter type="CLASS" missed="4" covered="2"/>
</report>

これをxmllintで取り出せば、容易にC0やC1カバレッジが計算できるでしょう。ここでは、XPathで値を抽出し、C1カバレッジを計算することにしました。

$ jacocoreport=target/site/jacoco/jacoco.xml
$ covered=$(xmllint --xpath 'string(/report/counter[@type="BRANCH"]/@covered)' ${jacocoreport})
$ missed=$(xmllint --xpath 'string(/report/counter[@type="BRANCH"]/@missed)' ${jacocoreport})
$ coverage=$(awk -vmissed=$missed -vcovered=$covered 'BEGIN{ printf("%.1f\n", covered/(covered+missed)*100 ) }')
$ echo "C1 coverage=${coverage}%"
C1 coverage=28.6%

これを.gitlab-ci.ymlに組み込めば良いです。 GitLabの場合、カバレッジはジョブの標準出力に出力し、それをキャプチャする仕組みがありました。 最近はこのキャプチャ範囲を.gitlab-ci.ymlで書けるようになったようです。

coverage:
  stage: visualize
  image: registry.gitlab.com/haynes/jacoco2cobertura:1.0.8
  variables:
    jacocoreport: app/movie-uploader/target/site/jacoco/jacoco.xml
  before_script:
    # for xmllint
    - apk --no-cache add libxml2-utils
  script:
    - python /opt/cover2cover.py ${jacocoreport} ${CI_PROJECT_DIR}/app/movie-uploader/src/main/java/ > cobertura.xml
    - covered=$(xmllint --xpath 'string(/report/counter[@type="BRANCH"]/@covered)' ${jacocoreport})
    - missed=$(xmllint --xpath 'string(/report/counter[@type="BRANCH"]/@missed)' ${jacocoreport})
    - coverage=$(awk -vmissed=$missed -vcovered=$covered 'BEGIN{ printf("%.1f\n", covered/(covered+missed)*100 ) }')
    - echo "Test Coverage=${coverage}%"
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
      changes:
        - "app/**/*"
  coverage: '/Test Coverage=\d+\.\d+/'
  artifacts:
    reports:
      coverage_report:
        coverage_format: cobertura
        path: cobertura.xml

結果がこちら。