🎉

pytestカバレッジをプルリクエストのコメントに表示する

2024/12/13に公開

導入経緯

Specteeでエンジニアをしている和山です。

プルリクエストのレビューにおいて、単体テストコードの妥当性を確認することは重要な観点の一つです。その際、カバレッジの確認が目安として活用されることがあります。
しかし、現状ではカバレッジを確認するために、ローカル環境で実行して出力を確認するか、GitHub Actionsの履歴を参照する必要があり、レビューに必要な手順や時間が増加するという課題が発生していました。

この問題を解決するために、 プルリクエストのコメントに pytest のカバレッジ結果を自動的に表示する仕組みを導入しました。 この仕組みにより、開発プロセスの透明性と効率性が向上し、レビューの負担を軽減することが期待されます。

本記事の結論

サードパーティ製アクションのPytest Coverage Comment を使用する。
カバレッジの出力ファイルをアクションに渡してコメントを行う。
https://github.com/marketplace/actions/pytest-coverage-comment

公式サンプルはこちら
name: pytest-coverage-comment
on:
  pull_request:
    branches:
      - '*'

permissions:
  contents: write
  checks: write
  pull-requests: write

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

        # 1.Pythonセットアップ
      - name: Set up Python 3.8
        uses: actions/setup-python@v2
        with:
          python-version: 3.8

      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install flake8 pytest pytest-cov
          if [ -f requirements.txt ]; then pip install -r requirements.txt; fi

        # 2.pytest実行
      - name: Build coverage file
        run: |
          pytest --junitxml=pytest.xml --cov-report=term-missing:skip-covered --cov=app tests/ | tee pytest-coverage.txt

        # 3.アクション起動
      - name: Pytest coverage comment
        uses: MishaKav/pytest-coverage-comment@main
        with:
          pytest-coverage-path: ./pytest-coverage.txt
          junitxml-path: ./pytest.xml
ryeと組み合わせたワークフローはこちら

ディレクトリ構成

.
└── .github
│   └── workflows
│       ├── ci.yml
│       └── cicd.yml
├── pyproject.toml
├── scripts
│   └── test_cov.sh
├── src              
└── tests  

pyproject.toml

pyproject.toml
[tool.rye.scripts]
ci = { chain = ["check", "test:cov"] }
check = { chain = ["check:ruff", "check:mypy"] }
"test:cov" = "./scripts/test_cov.sh"
"check:ruff" = "ruff check --config pyproject.toml src"
"check:mypy" = "mypy src --config-file=pyproject.toml"

scripts/test_cov.sh

scripts/test_cov.sh
#!/bin/bash
pytest tests/ --cov=src --cov-branch --junitxml=pytest.xml --cov-report=term-missing:skip-covered | tee pytest-coverage.txt

.github/workflows/cicd.yml

.github/workflows/cicd.yml
name: CI/CD Pipeline

on:
  push:
  pull_request:          # 重要:ワークフローの起動条件にプルリクエストを追加

permissions:
  pull-requests: write   # 重要:権限設定
  contents: read        

jobs:
  setup:
    runs-on: ubuntu-latest
    steps:
      - name: checkout repository
        uses: actions/checkout@v4

      - name: setup rye
        id: setup-rye
        uses: eifinger/setup-rye@v4
        with:
          enable-cache: true
          cache-prefix: rye-cache

      - name: install dependencies
        run: rye sync

  ci:
    needs: setup
    uses: ./.github/workflows/ci.yml  # ci.yml呼び出し

  # 以下ジョブは割愛

.github/workflows/ci.yml

.github/workflows/ci.yml
name: CI

on:
  workflow_call:

permissions:
  pull-requests: write # 重要:権限設定
  contents: read

jobs:
  ci:
    runs-on: ubuntu-latest
    steps:
      - name: checkout repository
        uses: actions/checkout@v4

      - name: setup rye
        id: setup-rye
        uses: eifinger/setup-rye@v4
        with:
          enable-cache: true
          cache-prefix: rye-cache

      - name: install dependencies
        run: rye sync

      - name: run ci for python project # 重要:CIの実行(静的解析〜テスト)
        run: |
          rye run ci

      - name: comment coverage on pull request
        if: github.event_name == 'pull_request' # 重要:プルリクエストの時にテストカバレッジをコメントする
        uses: MishaKav/pytest-coverage-comment@main
        with:
          pytest-coverage-path: ./pytest-coverage.txt
          junitxml-path: ./pytest.xml
          title: Coverage Report(ServiceA)
          unique-id-for-comment: ServiceA # 同じコメントにテスト結果を追記するための一意なID
          create-new-comment: false # 既存のコメントを更新します。同じコメントにテスト結果を追記するため、新しいコメントは作成しません。

導入手順 パターン1(公式サンプル)

以下公式のワークフローのサンプルです。

name: pytest-coverage-comment
on:
  pull_request:
    branches:
      - '*'

permissions:
  contents: write
  checks: write
  pull-requests: write

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

        # 1.Pythonセットアップ
      - name: Set up Python 3.8
        uses: actions/setup-python@v2
        with:
          python-version: 3.8

      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install flake8 pytest pytest-cov
          if [ -f requirements.txt ]; then pip install -r requirements.txt; fi

        # 2.pytest実行
      - name: Build coverage file
        run: |
          pytest --junitxml=pytest.xml --cov-report=term-missing:skip-covered --cov=app tests/ | tee pytest-coverage.txt

        # 3.アクション起動
      - name: Pytest coverage comment
        uses: MishaKav/pytest-coverage-comment@main
        with:
          pytest-coverage-path: ./pytest-coverage.txt
          junitxml-path: ./pytest.xml

出力イメージは公式参照
ワークフローの流れは以下の通り

  1. Pythonのセットアップ
  2. pytestコマンド実行。カバレッジファイル出力
  3. アクションの引数にカバレッジファイルを指定する

重要なのはpytestの実行とアクションの実行です。

pytestコマンドでカバレッジを出力

pytestコマンドでテストの実行とカバレッジの出力を行います。

pytest --junitxml=pytest.xml --cov-report=term-missing:skip-covered --cov=app tests/ | tee pytest-coverage.txt

各オプションの説明

  • --junitxml=pytest.xml: テスト結果をJUnit形式のXMLファイルとして保存
  • --cov-report=term-missing:skip-covered: カバレッジレポートの形式と内容を指定
    • term-missing: ターミナルにカバレッジレポートを出力
    • skip-covered: 100%カバレッジのファイルや行をスキップし、未カバー部分だけ表示
  • --cov=app: コードカバレッジ計測対象ディレクトリ。例ではappディレクトリが対象
  • | tee pytest-coverage.txt: コマンドの出力を標準出力とファイルの両方に記録

実行すると2つのファイルが作成されます。

  • pytest.xml
  • pytest-coverage.txt

pytest-coverage-commentアクションを起動

アクションを起動します

- name: Pytest coverage comment
uses: MishaKav/pytest-coverage-comment@main
with:
    pytest-coverage-path: ./pytest-coverage.txt
    junitxml-path: ./pytest.xml
  • pytest-coverage-path: pytest-coverage.txt出力パス。カバレッジのパーセンテージを判断するために使用
  • junitxml-path: junitxmlの出力パス。テストの数(成功、失敗など)を判断するために使用

導入手順 パターン2(ryeのプロジェクトに組み込む)

先ほどの手順は公式サンプルを利用したものです。
私達のチームはryeでプロジェクト管理しています。
公式サンプルを元にryeが使いやすい形に修正しました。特殊なケースかもしれませんが紹介します。

ディレクトリ構成は下記の通り。

.
├── pyproject.toml
├── scripts
│   └── test_cov.sh  # カバレッジファイル出力用シェルスクリプト
├── src              # プロダクションコードが格納されているディレクトリ
└── tests            # テストコードが格納されているディレクトリ

手順1.pytestコマンド実行スクリプトの作成と各種設定

前提としてryeでコマンド実行する場合はryeのスクリプト機能を使うと便利です。
pyproject.tomlに設定したスクリプトをrye run <script>で動かすことができます。

test:covで動作するようにpyproject.tomlを設定します

pyproject.toml
[tool.rye.scripts]
"test:cov" = "pytest tests/ --cov=src --cov-branch --junitxml=pytest.xml --cov-report=term-missing:skip-covered | tee pytest-coverage.txt"

rye run test:covを実行します

rye run test:cov
=================================================== test session starts ====================================================
platform darwin -- Python 3.12.2, pytest-8.3.2, pluggy-1.5.0
rootdir: <テスト対象ディレクトリ>
plugins: cov-5.0.0, anyio-4.4.0
collected 0 items                                                                                                          

---------- generated xml file: <プロジェクトルート>/pytest.xml -----------
================================================== no tests ran in 0.01s ===================================================
ERROR: file or directory not found: |

パイプ|がうまく渡せないようです。
シェルスクリプトファイル経由で動かすように修正します

script/test_cov.sh
#!/bin/bash
pytest tests/ --cov=src --cov-branch --junitxml=pytest.xml --cov-report=term-missing:skip-covered | tee pytest-coverage.txt

スクリプトファイルに実行権限を付与します。

chmod 755 scripts/test_cov.sh

pyproject.tomlファイルの修正を行います

pyproject.toml
[tool.rye.scripts]
"test:cov" = "./scripts/test_cov.sh"

これで実行できるようになりました。

rye run test:cov
============================= test session starts ==============================
platform darwin -- Python 3.12.2, pytest-8.3.2, pluggy-1.5.0
rootdir: <テスト対象ディレクトリ>
plugins: cov-5.0.0, anyio-4.4.0
collected 41 items

# 以下テスト結果が表示される

この後の手順でワークフローを実装します。
CI上でRuffやmypyによる静的解析も行いたい場合はチェインを使うと良いです

pyproject.toml
[tool.rye.scripts]
ci = { chain = ["check", "test:cov"] }
check = { chain = ["check:ruff", "check:mypy"] }
"test:cov" = "./scripts/test_cov.sh"
"check:ruff" = "ruff check --config pyproject.toml src"
"check:mypy" = "mypy src --config-file=pyproject.toml"

ローカル環境でrye run ciで静的解析とテストが行われます

pytest.xmlとpytest-coverage.txtファイルが出力されれば成功です。
この時点でのディレクトリ構成は以下の通り

.
├── pytest.xml
├── pytest-coverage.txt
├── pyproject.toml
├── scripts
│   └── test_cov.sh  # 追加
├── src              
└── tests  

pytest.xmlとpytest-coverage.txtは.gitignoreに追記し、コミット対象外にすると良いです。

手順2. ワークフローファイル整備

本例はcicd.ymlファイルがci.ymlを呼び出す形にします。

.
└── .github
   └── workflows
       ├── ci.yml
       └── cicd.yml

まずはcicd.ymlの設定

cicd.yml
name: CI/CD Pipeline

on:
  push:
  pull_request:          # 重要:ワークフローの起動条件にプルリクエストを追加

permissions:
  pull-requests: write   # 重要:権限設定
  contents: read

jobs:
  setup:
    runs-on: ubuntu-latest
    steps:
      - name: checkout repository
        uses: actions/checkout@v4

      - name: setup rye
        id: setup-rye
        uses: eifinger/setup-rye@v4
        with:
          enable-cache: true
          cache-prefix: rye-cache

      - name: install dependencies
        run: rye sync

  ci:
    needs: setup
    uses: ./.github/workflows/ci.yml  # ci.yml呼び出し

  # 以下ジョブは割愛

ryeのセットアップはsetup-ryeを使用します。
詳細はRyeとキャッシュ機能で快適なCI環境を整備してみたを参照ください。

https://zenn.dev/spectee/articles/spectee-rye-ci-cache

ポイントは2点
1点目は起動条件onキーにpull_requestを追加することです。
プルリクエスト時にコメントする機能を実現するので必須となります。

on:
  push:
  pull_request:          # 重要:ワークフローの起動条件にプルリクエストを追加

2点目は権限設定permissionsキーに適切な書き込み権限を付与することです。
pull-requestsの権限をwriteにします。
カバレッジをプルリクエストに表示するだけであればcontentの権限はreadで大丈夫です。

permissions:
  pull-requests: write   # 重要:権限設定
  contents: read

次にci.ymlの実装を行います。

ci.yml
name: CI

on:
  workflow_call:

permissions:
  contents: write
  pull-requests: write

jobs:
  ci:
    runs-on: ubuntu-latest
    steps:
      - name: checkout repository
        uses: actions/checkout@v4

      - name: setup rye
        id: setup-rye
        uses: eifinger/setup-rye@v4
        with:
          enable-cache: true
          cache-prefix: rye-cache

      - name: install dependencies
        run: rye sync

      - name: run ci for python project # 重要:CIの実行(静的解析〜テスト)
        run: |
          rye run ci

      - name: comment coverage on pull request
        if: github.event_name == 'pull_request' # 重要:プルリクエストの時にテストカバレッジをコメントする
        uses: MishaKav/pytest-coverage-comment@main
        with:
          pytest-coverage-path: ./pytest-coverage.txt
          junitxml-path: ./pytest.xml
          title: Coverage Report(ServiceA)
          unique-id-for-comment: ServiceA # 同じコメントにテスト結果を追記するための一意なID
          create-new-comment: false # 既存のコメントを更新します。同じコメントにテスト結果を追記するため、新しいコメントは作成しません。

ポイントは2点。CI実施のステップとテストカバレッジのコメント処理です。

1点目はCIの実施です。このステップでpytest-coverage.txtとpytest.xmlが出力されます。

- name: run ci for python project
  run: |
    rye run ci

2点目はカバレッジのコメント処理です。
今回のワークフローはプルリクエスト以外にプッシュ時でも動作する仕組みになっています。
プルリクエストの時だけステップ処理を行いたいのでifキーでイベントを制限します。

- name: comment coverage on pull request
  if: github.event_name == 'pull_request' # 重要:プルリクエストの時にテストカバレッジをコメントする
  uses: MishaKav/pytest-coverage-comment@main
  with:
    pytest-coverage-path: ./pytest-coverage.txt
    junitxml-path: ./pytest.xml
    title: Coverage Report(ServiceA)
    unique-id-for-comment: ServiceA 
    create-new-comment: false

公式サンプルからいくつかオプションを追加しています。

  • title:コメントタイトル
  • unique-id-for-comment: 同じコメントにテスト結果を追記するための一意なID。
    1回のCIで復数のカバレッジを表示したいモノレポ構成の場合に指定しておくと、サービスごとにカバレッジコメントがでてきます
  • create-new-comment: プルリクエスト更新時のコメント更新挙動です
    • true: 新しくコメントする
    • false: 既存のコメントを更新する

最終的なディレクトリ構成は以下の通りです。

.
└── .github
│   └── workflows
│       ├── ci.yml
│       └── cicd.yml
├── pyproject.toml
├── scripts
│   └── test_cov.sh
├── src              
└── tests  

手順3.動作確認

ワークフローの実装まで終わったらコードをプッシュしてプルリクエストを作成します。

プルリクエスト作成後にワークフローが動き始めます。
しばらくしてプルリクエストにカバレッジコメントが追加されたら成功です。

PRコメント

> Coverage Repostのトグルを開くと未カバー部分を確認することができます。

PRカバレッジ詳細

まとめ

今回はpytestカバレッジをプルリクエストのコメントに表示する仕組みを紹介しました。
プルリクのコメントに表示されることでテストを確認する意識付けが向上したなと思いました。
レビューで開発プロセスの透明性と効率性を向上も期待できそうです!

参考

GitHubで編集を提案
Spectee Developers Blog

Discussion