📊

うわっ…私のカバレッジ、低すぎ…? - GitHub ActionsでRSpecのカバレッジレポートを出力する

2023/12/03に公開

はじめに

本記事はプロもくチャット Advent Calendar 2023の3日目です。

https://qiita.com/advent-calendar/2023/puromoku


RSpecのカバレッジ計測行うgemであるSimpleCovと、GitHub Actionでカバレッジレポートの出力を行うoctocovを組み合わせて、PR作成時にテストを実行してカバレッジを計測し、PRコメントにカバレッジレポートを出力します。

What is カバレッジ?

テストの網羅性を評価する指標で、コードのうちテストで実行される割合を示すものです。

この数値が100%に近づくほど、より多くのコードがテストによって検証されていると言えます。

カバレッジの種類として、line coverage, branch coverage, condition coverageなどがあります。

line coverage

コードの各行がテストで実行されたかどうかを測定します。

例えば、以下のようなメソッドをテストする場合に、処理Aを通るテストケースが1つあればカバレッジが100%となります。

def foo
  if condition_a
    # 処理A
  end
end

line coverageは「コードの各行が実行されたか」、つまり「分岐の中を通ったか」を示すものであり、分岐条件そのものについては評価されません。

branch coverage

コード内の条件分岐(if文, ループ, switch文など)をどれだけ網羅できているかを測定します。

上記のfooメソッドで言うと、condition_aがtrue, falseの2通りのテストケースがあればカバレッジ100%となります。

また、以下のbarメソッドの場合は、condition_b && condition_c, condition_d && condition_eの組み合わせが

  • [true, true]
  • [true, false]
  • [false, true]
  • [false, false]

の4通りのテストケースがあればカバレッジ100%となります(condition_bcondition_ccondition_dcondition_eは個々に考える必要はありません)。

def bar
  if condition_b && condition_c
    # 処理B
  end

  if condition_d && condition_e
    # 処理C
  end
end

なお、branch coverageではあくまで「分岐を網羅しているか」が関心事であり、line coverageのように「各行が実行されたか」には関与しません。

後述のSimpleCovでのbranch coverage計測においては、分岐のないメソッドの場合、実行されなくてもカバレッジ100%と判定されます。

condition coverage

これに対して、条件分岐内の個々の条件にも着目して網羅率を計測するのがcondition coverageです。

上記のbarメソッドにおいて、branch coverageではcondition_b && condition_cを1つの条件とみなしていましたが、condition coverageではcondition_bcondition_cを別個の条件とみなします。

つまり、condition_bcondition_eのそれぞれについてtrue/falseを検証するため、2^4 = 16通りのテストケースがあればカバレッジ100%となります。

Codecovではあかんのか?

カバレッジ計測ツールとしてはCodecovも有名です。

カバレッジ計測自体はもちろんのこと、

  • PRごとのカバレッジ差分を確認する
  • テストされた/されていない箇所を行単位で表示する

など、様々な分析が可能になります。

しかし、Codecovの料金体系は以下のようになっており、

Open Source Developer Pro Enterprise
料金 無料 無料 有料 有料
ユーザー数 無制限 1ユーザー カスタム カスタム
Publicリポジトリでの使用 無制限 無制限 無制限 無制限
Privateリポジトリでの使用 不可 無制限 無制限 無制限
料金 無料 無料 有料 有料

https://about.codecov.io/pricing/

  • Open SourceプランとDeveloperプラン以外は有料
  • Open Sourceプランだとユーザー数無制限だが、Privateリポジトリでは使えない
  • DeveloperプランだとPrivateリポジトリでも無制限だが、ユーザーは1人しか使えない

という特徴があります。
なので、複数人かつPrivateリポジトリでの開発の場合は有料プランを使うしかなく、導入ハードルが上がってしまいます。

そこで今回は、Codecovを使うことなく

  • PRごとのカバレッジ差分を確認する

こちらも含めたカバレッジレポートを無料で出力する方法を紹介していきます。
(ただしGitHub Actions自体にかかるコストは考えないものとする)

同じ値段でステーキを!?

というわけでやっていきます。

なお、本記事ではRSpec(Rails) + SimpleCov + octocovで構築していますが、octocov自体はSimpleCov以外にも様々なフォーマットに対応しているので、他の言語でも使用できます(公式ドキュメントではGoを使って説明しています)。

手順

全体の流れ

以下のような流れで設定していきます。

  1. SimpleCovを使ってローカルでカバレッジ計測できるようにする
  2. octocovをGitHub Actionsのワークフローに組み込んで、PRコメントでカバレッジレポートを出力するようにする
  3. PRマージ時に、デフォルトブランチでカバレッジ計測を行うようにする
  4. 2のカバレッジレポートで、3の計測結果との差分も出せるようにする

登場人物はざっくりこんな感じです。

├── .github
│   └── workflows
│       ├── ci.yml # PR作成時に実行するCIのワークフロー
│       └── rspec_on_pr_merge.yml # PRマージ時に実行するCIのワークフロー
│
├── spec
│   └── spec_helper.rb # RSpecの設定ファイル(ここにカバレッジ計測の設定を記述する)
│
│── .octocov.yml # PR作成時に実行するカバレッジレポート出力の設定ファイル
└── .octocov.base.yml # PRマージ時に実行するカバレッジレポート出力の設定ファイル

カバレッジ計測の設定をする

まずはSimpleCovを使ったカバレッジ計測の設定を行います。

  • GemfileにSimpleCovを追加してbundle installを実行する

    Gemfile
    gem 'simplecov', require: false, group: :test
    
  • spec/spec_helper.rbでSimpleCovを読み込む

    spec/spec_helper.rb
    require 'simplecov'
    SimpleCov.start
    
  • 必要に応じて設定を追加する

    spec/spec_helper.rb
    require 'simplecov'
    SimpleCov.start do
      enable_coverage :branch
      add_filter '/spec/'
      add_group 'Services', 'app/services'
    end
    
    • enable_coverage: 指定したカバレッジを有効にする(デフォルトではline coverageのみ有効になっている)(後述)
    • add_filter: 指定したファイルが計測対象から除外される(例の場合、specディレクトリ以下のファイルが除外)
    • add_group: 指定した内容をグループ化して出力する(後述)

この状態でRSpecを実行すると、coverageディレクトリにカバレッジ計測結果が出力されます。
coverage/index.htmlを開くと、画像のようなページが表示されます。

SimpleCov計測結果

上述のenable_coverage :branchの設定により、branch coverageの計測結果が表示されています(画像赤枠部分)。

また、add_group: 'Services', 'app/services'の設定により、app/servicesのファイルの計測結果がグループ化され、タブが追加されています(画像青枠部分)。
なお、Controllers, Channels, Models, Mailers, Helpers, Jobs, Librariesはデフォルトでグループ化されています。

ファイル毎の計測結果もあり、どの行が実行された/されてないかも確認することができます。

octocovの設定をする

次に、SimpleCovでの計測結果から、octocovを使ってPRコメントにカバレッジレポートを出力します。
前提として、GitHub ActionsでCIを構築済で、RSpecの実行までできるようになっているものとします。

  • GitHub Actionsのワークフローに、カバレッジレポートを出力するoctocov-actionを追加

    .github/workflows/ci.yml
    name: Rspec
    
    on:
      pull_request:
        branches:
          - main
    
    jobs:
      rspec:
        runs-on: ubuntu-latest
        timeout-minutes: 15
        steps:
          # Ruby 3.2.2, Node 18の環境をセットアップ
          - uses: actions/checkout@v3
          - uses: ruby/setup-ruby@v1
            with:
              ruby-version: 3.2.2
          - uses: actions/setup-node@v4
            with:
              node-version: 18
          # RSpecを実行するための環境をセットアップ
          - run: gem install bundler:2.4.22
          - run: ./bin/bundle install
          - run: ./bin/yarn install
          - run: ./bin/rails db:create
          - run: ./bin/rails db:migrate
          - name: Exec rspec
            run: ./bin/rspec
    +     # octocovによりカバレッジレポートをPRコメントで表示
    +     - name: Coverage Report by octocov (in this pull request)
    +       uses: k1LoW/octocov-action@v0
    
  • 設定ファイル.octocov.ymlを作成する

    .octocov.yml
    coverage: # カバレッジ計測の設定
      paths:
        - coverage/.resultset.json # ここで指定した計測結果ファイルを読み込んで分析を行う
      acceptable: 60% # カバレッジがこの数値を下回るとCIがfailする
    testExecutionTime: # テスト実行時間
      steps:
        - Exec rspec # この名前に対応するGitHub Actionsのstepの実行時間を計測する
      acceptable: 3min # 実行時間がこの時間を上回るとCIがfailする
    codeToTestRatio: # Code to Test Ratio(実装コードの行数を1とした場合のテストコード行数の比率)の設定
      code:
        - "app/**/*.rb"
      test:
        - "spec/**/*_spec.rb"
      acceptable: 1:1.2 # この数値を下回るとワークフローがfailする
    comment: # レポート内容をコメント投稿設定
      if: is_pull_request # PRの場合のみコメントを投稿する
      hideFooterLink: false # フッターのoctocovのリンクを非表示にしない
    report: # レポートの出力設定
      datastores:
        - artifact://${GITHUB_REPOSITORY} # レポートの出力先(GitHub ActionsのArtifactsに保存する)
    

この設定でCIを回すとこんな感じのPRコメントが投稿されます。

octocovコメント

カバレッジを含むテストの情報が表示できましたが、まだこのPR単体での結果のみです。

続いて、このカバレッジレポートを、デフォルトブランチでのカバレッジレポートと比較するようにします。

大まかな流れは、「PRマージ時にRSpecを実行してカバレッジレポートを出力する → 別のPR作成時にそのレポートと比較する」という感じです。

PRマージ時に、デフォルトブランチでRSpecを実行してカバレッジレポートを出力する

  • PRマージ時のカバレッジレポート用のoctocov設定ファイルを作成する

    .octocov.base.yml
    coverage:
      paths:
        - coverage/.resultset.json
    testExecutionTime:
      steps:
        - Exec rspec
    codeToTestRatio:
      code:
        - "app/**/*.rb"
      test:
        - "spec/**/*_spec.rb"
    report:
      datastores:
        - artifact://${GITHUB_REPOSITORY}/base # .octocov.ymlとは違う保存先を指定する
    
  • PRマージ時にRSpecを実行するワークフローを作成する

    .github/workflows/rspec_on_pr_merge.yml
    name: RSpec on PR merge
    
    on:
      push:
        branches:
          - main
    jobs:
      rspec:
        runs-on: ubuntu-latest
        timeout-minutes: 15
        steps:
          - uses: actions/checkout@v3
          - uses: ruby/setup-ruby@v1
            with:
              ruby-version: 3.2.2
          - uses: actions/setup-node@v4
            with:
              node-version: 18
          - run: gem install bundler:2.4.22
          - run: ./bin/bundle install
          - run: ./bin/yarn install
          - run: ./bin/rails db:create
          - run: ./bin/rails db:migrate
          - run: mv .octocov.base.yml .octocov.yml # 重要
          - name: Exec rspec
            run: ./bin/rspec
    

このワークフローでは、run: mv .octocov.base.yml .octocov.ymlの行が肝になります。
基本的に.octocov.ymlがoctocovの設定ファイルとして認識されるため、.octocov.base.ymlをリネームして上書きする必要があります。

他のステップは.github/workflows/ci.ymlと同じなので、composite actionを使って共通化するとbetterでしょう。

octocovでデフォルトブランチとの比較を行う

前項で作成したデフォルトブランチのカバレッジレポートとの比較を行うため、.octocov.ymlの設定を追加します。

.octocov.yml
coverage: # カバレッジ計測の設定
  paths:
    - coverage/.resultset.json # ここで指定した計測結果ファイルを読み込んで分析を行う
-  acceptable: 60% # カバレッジがこの数値を下回るとCIがfailする
+  acceptable: current >= 60% && diff >= 0% # カバレッジが60%以上かつ比較対象との差分が+0%以上でなければCIがfailする
testExecutionTime: # テスト実行時間
  steps:
    - Exec rspec # この名前に対応するGitHub Actionsのstepの実行時間を計測する
-  acceptable: 3min # 実行時間がこの時間を上回るとCIがfailする
+  acceptable: current <= 3min && diff <= 1min # 実行時間が3min以内かつ比較対象との差分が1min以内でなければCIがfailする
codeToTestRatio: # Code to Test Ratio(実装コードの行数を1とした場合のテストコード行数の比率)の設定
  code:
    - "app/**/*.rb"
  test:
    - "spec/**/*_spec.rb"
-  acceptable: 1:1.2 # この数値を下回るとワークフローがfailする
+  acceptable: current >= 1.2 && diff >= 0 # Code to Test Ratioが1.2以上かつ比較対象との差分が0以上でなければCIがfailする
comment: # レポート内容をコメント投稿設定
  if: is_pull_request # PRの場合のみコメントを投稿する
  hideFooterLink: false # フッターのoctocovのリンクを非表示にしない
report: # レポートの出力設定
  datastores:
    - artifact://${GITHUB_REPOSITORY} # レポートの出力先(GitHub ActionsのArtifactsに保存する)
+diff:
+  datastores:
+    - artifact://${GITHUB_REPOSITORY}/base # .octocov.base.ymlのreport.datastoresで設定した保存先を指定する

coverage, testExecutionTime, codeToTestRatioの各項目では、今回の実行結果の数値をcurrent、比較対象の数値をpref、その差分をdiffという値で用いることができます

この状態でCIを回すと、無事以下のように差分を含んだカバレッジレポートが出力されるようになりました🎉
(画像ではデフォルトブランチをmainではなくdevelopとしています)

カバレッジレポート(差分付き)

まとめ

以上の方法で、Codecovを使用せずとも差分を含むカバレッジレポートを出力することができました。

PRコメントとして結果を出すようにすることで、「うわ…私(たち)のカバレッジ、低すぎ…?」という気付きを共有しやすくもなりますし、レビューで「テスト書きましょうね〜」とも言いやすくなります。

もちろんカバレッジのみがテスト指標の全てではありませんが、こういう見える化がテストを書く意識付けにも繋がれば素晴らしいですね!

最後までご覧いただきありがとうございました。

参考

https://github.com/simplecov-ruby/simplecov

https://github.com/k1LoW/octocov

Discussion