💨

Github ActionsでPyTest+LocalStackを動かしてテストを自動化する

に公開

概要

「機能追加とテストはセットである。テストのないコードはありえない」というお話を何回もしていたのですが「忘れたので別Issueで」とか、そもそもテストを書くという概念がない人が多くてめちゃくちゃ疲弊したので、自動でテストを回してPassしないとマージできないようにしました。
「テストを書きましょう」と個々の意識に任せるのではなく、システム化した方が確実です。

今回はPyTestとLocalStackをGithub Actions上で動かす必要があったのですがそのナレッジを今回ご紹介します。

リポジトリ

今回Github Actionsを検証するにあたり作成したリポジトリです。
https://github.com/DogFortune/pytest-github-actions

ワークフロー

まずはyml全体を書いておきます。

name: test

on:
  workflow_dispatch:
  push:
    branches:
      - main
  pull_request:
    branches:
      - main
permissions:
  checks: write
  pull-requests: write
jobs:
  test:
    runs-on: ubuntu-24.04
    timeout-minutes: 5
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: '3.12'
          cache: 'pipenv'
      - name: install pipenv
        run: |
          python -m pip install --upgrade pip
          python -m pip install pipenv
      - name: install packages
        run: pipenv sync --dev
      - name: Cache Docker images.
        uses: ScribeMD/docker-cache@0.5.0
        with:
          key: ${{ hashFiles('docker-compose.yml') }}
      - name: start docker compose
        run: docker compose up -d
      - name: run test
        run: pipenv run pytest -q --junit-xml pytest.xml
        continue-on-error: true
      - name: Publish Test Report
        uses: mikepenz/action-junit-report@v5
        if: success() || failure()
        with:
          report_paths: 'pytest.xml'

ポイント

他リポジトリをチェックアウトする時はトークンを発行する

自動的に生成されるGithubトークンはワークフローを起動したリポジトリしかアクセス権限がないので、他のリポジトリを参照したい場合、ランナーとなるマシンにソースコードを持ってくる(チェックアウト)時に認証情報を設定しないとエラーになります。

Github Appsを使って秘密鍵を使ってトークンを取得します。それを使ってチェックアウトします。
https://docs.github.com/ja/enterprise-cloud@latest/apps/creating-github-apps/authenticating-with-a-github-app/making-authenticated-api-requests-with-a-github-app-in-a-github-actions-workflow

Appsを作成したらAPP IDと秘密鍵をリポジトリに登録します。アプリIDはVariablesに、秘密鍵はSecretsに登録しましょう。設定画面から登録できます。
今回の場合、アプリIDはAPP_ID、秘密鍵はAPP_PRIVATE_KEYをキーにして登録しています。

設定したらyml上から参照できるようになります。

env:
  TARGET_REPO: "first-repo, second-repo"

jobs:
  test:
    runs-on: ubuntu-24.04
    timeout-minutes: 5
    steps:
      - name: create token
        id: create-token
        uses: actions/create-github-app-token@v1
        with:
          app-id: ${{ vars.APP_ID }} # アプリIDはここ。
          private-key: ${{ secrets.APP_PRIVATE_KEY }} # 秘密鍵はここ。
          repositories: ${{ env.TARGET_REPO }} # チェックアウトしたいリポジトリ
      - uses: actions/checkout@v4
        with:
          token: ${{ steps.create-token.outputs.token }}
          persist-credentials: false

プライベートリポジトリをパッケージとしてインストールする(pip)

上記の方法で生成したトークンを使う事でインストールできます。TARGET_REPOに対象のリポジトリを含めるのを忘れないように。

pip install git+https://x-access-token:${{ steps.create-token.outputs.token }}@github.com/owner/private-repo.git

例えばプライベートリポジトリで配布されているライブラリに依存しているコードをテストしたい時とかに必要になります。

参考:https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/authenticating-as-a-github-app-installation

JUnit形式のサマリーを出す

mikepenz/action-junit-reportを使うと見やすいレポートとどこでテストが失敗したかが確認しやすくなります。PyTestを実行する際、JUnit形式のレポートを出力させて読み込ませることでサマリーを出すことができます。

      - name: run test
        run: pipenv run pytest -q --junit-xml pytest.xml # `--junit-xml`オプションをつけて結果を出力。
        continue-on-error: true # テスト失敗時でも先に進むようにする。テストが失敗するとここで止まってしまうのでサマリーが出ない。
      - name: Publish Test Report
        uses: mikepenz/action-junit-report@v5
        if: success() || failure() # 成功しても失敗しても実行する。
        with:
          report_paths: 'pytest.xml' # レポートを指定する。

結果はこんな感じ。

https://github.com/DogFortune/pytest-github-actions/pull/2/checks?check_run_id=35199107370

使用の際はパーミッションの設定が必要ですので注意して下さい。
https://github.com/mikepenz/action-junit-report#pr-run-permissions

TimeOutを設定する

何かしらの理由でランナーがスタックした場合、無駄に時間枠が消費されてしまうので、タイムアウトは必ず設定しておきましょう。デフォルトはなんと6時間!
1つあたりのジョブは1分くらいで終わるとしても、並列で100ジョブ実行すると100分になるので、「短いジョブだから別にいいか」と思わず設定しておきましょう。

jobs:
  test:
    runs-on: ubuntu-24.04
    timeout-minutes: 5 # 5分
    steps:
      - run: npm run test

キャッシュを効かせる

基本的にはActionsの時間削減ですが、LocalStack用のDockerイメージもキャッシュを効かせることでPull回数制限に引っかからないようにします。

jobs:
  test:
    runs-on: ubuntu-24.04
    timeout-minutes: 5
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: '3.12'
          cache: 'pipenv' # ここ
      - name: install pipenv
        run: |
          python -m pip install --upgrade pip
          python -m pip install pipenv
      - name: install packages
        run: pipenv sync --dev

actions/setup-pythonがキャッシュのサポートをしているので自分の環境にあった設定を行います。
https://github.com/actions/setup-python#caching-packages-dependencies

Dockerイメージのキャッシュも行います。認証なしユーザーだとPull回数に制限があるのでそこに引っかからないようにキャッシュします。プラススピードアップも狙えます。

jobs:
  test:
    runs-on: ubuntu-24.04
    timeout-minutes: 5
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: '3.12'
          cache: 'pipenv'
      - name: install pipenv
        run: |
          python -m pip install --upgrade pip
          python -m pip install pipenv
      - name: install packages
        run: pipenv sync --dev
      - name: Cache Docker images. # これでDockerイメージをキャッシュできます。
        uses: ScribeMD/docker-cache@0.5.0
        with:
          key: ${{ hashFiles('docker-compose.yml') }}
      - name: start docker compose
        run: docker compose up -d

原理としてはDockerイメージをsaveしておいて、keyが一致したらそれをloadする事でPullせずにDockerイメージを使う事ができます。

keyは各環境に合わせて変更お願いします。
https://github.com/marketplace/actions/docker-cache

動作検証はPublicリポジトリで

Privateリポジトリは時間枠が決まっているので、あれこれ設定を弄ってはActionsを動かしてを繰り返していると枠を消費します。
なので動作検証はPublicリポジトリでやって、ある程度骨組みができたらPrivateの方に持ってきて、最後はそっちで微調整するという流れがおすすめです。

リポジトリのルールセットについて

ルールセットにステータスチェックを追加するには1回はActionsを回さないといけないのがちょっと注意です。

ポイントとしては、最新コードを含めるチェックを有効にすることです。

最初はここを無効にしていたのですが、featureブランチの状態ではPassしていたのですが、mainブランチにマージした時に失敗する現象が発生しました。
すべてのfeatureブランチがマージした状態のテストが動かないので、最新のコードをfeatureで取り込んできちんとテストがPassすることが確定してからマージするようにします。

まとめ

ここで書いたことは普段からActionsを回している人からすれば新鮮な情報ではないかもしれませんが、何かしら参考になれば幸いです。

Discussion