Retty Tech Blog

実名口コミグルメサービスRettyのエンジニアによるTech Blogです。プロダクト開発にまつわるナレッジをアウトプットして、世の中がHappyになっていくようなコンテンツを発信します。

低予算でGoのコードカバレッジレポートをPull Requestにコメントする using CircleCI

ソフトウェアエンジニアの福井です。

コードカバレッジのパーセンテージを上げる(または保つ)ことを強制することは悪いプラクティスとされます。 そのためRettyではいくつかのプロジェクトで、パーセンテージによってmergeできないなど強制せず、カバレッジのパーセンテージのみを見える化していました。
しかしどのコードがカバーされてるか参照できるカバレッジレポートが身近にありませんでした。 どのコードがカバーされてるかを参照することで以下のメリットがあります。

  1. 条件が複雑でないコードのリファクタリング心理的安全性が高まる
  2. コード設計面での気づきが得られる

2つ目の"コード設計面での気づきが得られる"についての説明です。
飲食店の予算をチェックする関数(言語はGo)があるとします。 予算が¥1,000~¥50,000の範囲で¥1,000単位であることをチェックします。
(*説明のために仕様を単純化していて、コードも1行にまとめたりしていません)

package budget

func CheckBudget(b uint32) bool {
    if !checkRange(b) {
        return false
    }
    if !checkUnit(b) {
        return false
    }
    return true
}

func checkRange(b uint32) bool {
    return 1000 <= b && b <= 50000
}

func checkUnit(b uint32) bool {
    // A
    if b < 1000 {
        // B
        return false
    }
    return b%1000 == 0
}

Aでは入力が0だとtrueで判定してしまうためのチェックです。

これに対してテストコードを書きます。

package budget_test

import (
    "coverage/budget"
    "testing"
)

func TestCheckBudget(t *testing.T) {
    tests := []struct {
        budget uint32
        want   bool
    }{
        {budget: 1000, want: true},
        {budget: 50000, want: true},
        {budget: 49999, want: false},
        {budget: 0, want: false}, // C
    }

    for _, tc := range tests {
        if budget.CheckBudget(tc.budget) != tc.want {
            t.Error("unexpected result")
        }
    }
}

Cのbudgetが0のテストケースを実行してもBの行はカバレッジが通りません。 なぜなら前のcheckRange関数で0を弾いているからです。 この場合checkRangeとcheckUnit関数の責務を重複させないために、1つの関数にまとめるなど考えられるかもしれません。
このように関数の責務や粒度のようなコード設計に気づかされます。 Google Testing Blogでも"コードがカバーされてないことに意味がある"とあります。

GoのコードカバレッジレポートをPull Requestにコメントする

このようなメリットのあるコードカバレッジレポートをGoのプロジェクト開発で簡単に参照したく、Codecovなどのサービスを使わずに低予算でカバレッジレポートをPull Requestコメントし参照できるようにしました。
なおCIはCircleCIを使っており、Go標準のカバレッジレポートを使用します。

流れ

  1. GitHubでPull Request open
  2. カバレッジレポートを出力しCircleCI Artifactにアップロード
    CircleCI Artifactはジョブが完了した後もデータが保持され、ビルドプロセス出力を格納するストレージとして使用できます
  3. アップロードしたURLをPull Requestにコメント

実際のCircleCIコード

既存のCircleCIコードにworkflowを追加します。

jobs:

  # 他のjob

  post-coverage-report:
    executor:
      name: golang
    steps:
      - checkout
      - run:
          command: |
            go test -coverprofile=cover.out -coverpkg=./... ./... 
            go tool cover -html=cover.out -o cover.html
      - store_artifacts:
          path: cover.html
          destination: coverage-report
      - run:
          name: Post Artifact URL to GitHub PR
          command: |
            GITHUB_COMMENT_VERSION=6.0.3

            if [ -z "${CIRCLE_PULL_REQUEST}" ]; then
              echo "This step is not by a pull request. Skip posting coverage report."
              exit
            fi
            apt update && apt install -y jq
            ARTIFACT=$(curl -X GET "https://circleci.com/api/v2/project/github/${CIRCLE_PROJECT_USERNAME}/${CIRCLE_PROJECT_REPONAME}/${CIRCLE_BUILD_NUM}/artifacts" \
              -H "Accept: application/json" --header "Circle-Token: ${CIRCLE_TOKEN}" | jq -r '.items[0].url')

            PR_NUMBER=$(basename ${CIRCLE_PULL_REQUEST})
            COMMENT_BODY="Coverage report is available [here](${ARTIFACT})."
            echo $COMMENT_BODY

            curl -L https://github.com/suzuki-shunsuke/github-comment/releases/download/v${GITHUB_COMMENT_VERSION}/github-comment_${GITHUB_COMMENT_VERSION}_linux_amd64.tar.gz -o linux_amd64.tar.gz
            tar -zxvf linux_amd64.tar.gz
            ./github-comment hide --org ${CIRCLE_PROJECT_USERNAME} --repo ${CIRCLE_PROJECT_REPONAME} --pr ${PR_NUMBER} -condition 'Comment.HasMeta && Comment.Meta.TemplateKey == "default"'
            ./github-comment post --org ${CIRCLE_PROJECT_USERNAME} --repo ${CIRCLE_PROJECT_REPONAME} --pr ${PR_NUMBER} --template "${COMMENT_BODY}"


workflows:
  version: 2 

  # 他のworkflow

  post-coverage-report-workflow:
    jobs:
      - post-coverage-report:
          filters:
            branches:
              ignore: main

またCIrcleCI Environment Variablesに以下を設定します。

  • GITHUB_TOKEN: Pull RequestにWriteできるGitHub Personal access tokens
  • CIRCLE_TOKEN: CircleCI Personal API Token

Only build pull requestsは有効化してる前提です。

動作

  1. Pull Request openするとコメントされます。
  2. URLを選択するとGo標準カバレッジレポートを参照できます。
  3. コミットを積むと新しくコメントされ、前のコメントがhideされます。

解説

  • Pull Requestコメントにgithub-commentというCLIを使用してます。 2024年3月時点ではGitHub APIでPull RequestコメントをhideするAPIGraphQLでしか提供してないため、このCLIを使うのが便利です。

  • アップロードしたArtifact URLを取得するCircleCI API v2は現在Personal API Tokenのみをサポートしてます。 Personal API TokenはProject API Tokenのような権限設定はできないため管理に注意が必要です。

  • CircleCIとCircleCI Artifactの代わりにGitHub ActionsとGitHub Actions Artifactを使うことも考えられます。 ですが2024年3月時点でGtiHub Actions ArtifactはCircleCI ArtifactのようにHTMLファイルをブラウザで開けないので、対応が待たれます (issue)。

  • post-coverage-report-workflow は他workflowとは別に作成しています。 post-coverage-report-workflowをGitHubのBranch protections status checkから外すなどすれば、もしCI設定ミスでpost-coverage-report-workflowが失敗してもmergeできる設定にしやすいためです。

まとめ

(コミット積むたびにPull Requestにコメントしてしまいますが、)どのコードがカバーされてるか参照できるカバレッジレポートが身近になりました。コード設計面で気づきを得る機会が増えると思います。

以上です。