1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

GitHub Actions と pytest で API 単体テストを自動化してみた

Last updated at Posted at 2025-07-04

はじめに

こんにちは、Qiita 初投稿の sabakandayo です!
みなさんは、GitHub Actions使ってますか?

「Python × GitHub Actions = ビルド&デプロイ専用ツール」
というイメージでしたが、単体テストも出来るらしいとのことで

今回は、GitHub上で完全自動化に挑戦してみました!

GitHub Actionsの機能を自分なりに詳しい解説付きでサンプルをまとめたので、同じ疑問を持つ方の参考になればうれしいです!

この記事のゴール

  • GitHub Actions で API の単体テストを “手軽に” 自動化する方法を知る
  • pytest の結果を GitHub Actions の サマリー にきれいにまとめるワークフローを作る
  • 失敗テストが一目で分かるダッシュボードを作り、チーム開発を少しハッピーにする

目的(メリット)

GitHub Actions を使って単体テストの CI/CD を構築することで、以下のメリットがあります。

  • ビルドやデプロイと連携して自動実行できる
  • ローカルにテスト環境を用意しなくてよい
  • チーム全員でテスト状況を可視化できる

実行イメージ

image.png

ワークフローの説明

テスト実行ジョブとテスト結果をまとめるジョブに分け、API のテストは 並列 で、実行結果は 順序付けて 実装しました。

  • Matrix:api(並列で API テストを実行)

    • test_api_fail:期待値を満たさないテスト(例:ステータスコード 500)
    • test_api_pass:期待値を満たすテスト(例:ステータスコード 200)
  • UnitTests-Result(全 API テスト結果を集計)

    • テスト結果の Markdown 生成とサマリー出力

実行結果イメージ

image.png

テスト結果をジョブのサマリーに Markdown 形式で表示しています。

  • UnitTest Result: テスト結果の一覧表
  • UnitTest Detail: API リクエスト & レスポンス詳細

主な使用技術

  • GitHub Actions
  • pytest
  • Markdown

GitHub Actions とは

GitHub Actions は、GitHub リポジトリの push/Pull Request などのイベントをトリガーに、テスト・ビルド・デプロイを自動化できる統合 CI/CD サービスです。

pytest とは

pytest は Python 向けの軽量で高機能なユニットテストフレームワークで、最小限のコードで柔軟な自動テストを記述・実行できます。


実装してみた

主な流れ

  1. 各 API ごとにテスト(pytest)を実行(matrix で並列化)
  2. テスト結果(JSON サマリー)をアーティファクトとして保存
  3. 全 API 分のサマリーを集計し、GitHub Actions のサマリーに表形式で出力

ワークフローの構成

  • .github/workflows/api_test.yml — API 単体テスト自動化ワークフロー

今回は 手動実行 で実装しています。

on:
  workflow_dispatch:

他のトリガーイベントについては公式ドキュメントを参照してください。
参考:https://docs.github.com/ja/actions/reference/events-that-trigger-workflows#workflow_call

GitHub Actions(yml)のワークフロー内容


ジョブ間でパラメータを共有する(改善の余地あり)

ジョブ 1 で生成した pytest の JSON を アーティファクト としてアップロードし、ジョブ 2 でダウンロードして解析しています。


サマリー生成の流れ

pytest 実行(抜粋)

jobs:
  api:
    name: ${{ matrix.api }}
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        api:
          - test_api_pass
          - test_api_fail
    env:
      API_NAME: ${{ matrix.api }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.11"
      - run: pip install -r tests/requirements.txt

      - name: Run API Unit Tests
        run: |
          pytest -v -s --log-cli-level=INFO tests -k "${{ matrix.api }}" --disable-warnings > pytest_output.log || true
          cat pytest_output.log

pytest コード実装例

try:
    assert response.status_code == expected_status, f"Expected {expected_status}, got {response.status_code}"
    test_result = 'Passed'
except AssertionError as e:
    print(f"AssertionError: {e}", flush=True)
    test_result = 'Failed'
finally:
    results = []
    result_item = {
        "api_name": ep.get('name'),
        "expected_status_code": expected_status,
        "method": ep.get('method', 'GET'),
        "status_code": response.status_code,
        "request_query": params,
        "request_body": None,
        "title": "テスト用にたたくAPI",
        "test_result": test_result
    }
    if response.headers.get("Content-Type", "").startswith("application/json"):
        try:
            result_item["response"] = response.json()
        except:
            result_item["response"] = response.text
    else:
        result_item["response"] = response.text
    results.append(result_item)
    print("=== TEST_RESULT_JSON_START ===", flush=True)
    print(json.dumps(results, ensure_ascii=False), flush=True)
    print("=== TEST_RESULT_JSON_END ===", flush=True)
  • 並列実行 (strategy.matrix)
    api_test.yml では下記のように書くだけで、GitHub Actions が 2 つのランナーを自動で確保 し、それぞれで pip installpytest を独立して走らせてくれます。
strategy:
 matrix:
    api:
      - test_api_pass
      - test_api_fail
  • print で JSON を埋め込む → sed + jq で抽出
    pytest のテストコード内で
print("=== TEST_RESULT_JSON_START ===")
print(json.dumps(results, ensure_ascii=False))
print("=== TEST_RESULT_JSON_END ===")

標準出力に JSON を直接書き出し ています。ワークフローでは

JSON_CONTENT=$(sed -n '/=== TEST_RESULT_JSON_START ===/,/=== TEST_RESULT_JSON_END ===/p' pytest_output.log | grep -v "TEST_RESULT_JSON")
echo "$JSON_CONTENT" | jq . > summary_${API_NAME}.json

のようにマーカーで囲まれた部分だけを切り出し、jq でパース→保存することで出力結果の取得をしています。

工夫ポイント

  • pytestのprint出力を=== TEST_RESULT_JSON_START ===などのマーカーで囲むことで、ログからの抽出を確実にしています。
  • pytestコマンドの後ろに|| trueを付けているのは、テスト失敗時でも必ずログを出力し、後続の処理で失敗判定を行うためです。

テスト結果を JSON 化・ジョブ失敗判定

- name: Process test results
  id: process_results
  run: |
    JSON_CONTENT=$(sed -n '/=== TEST_RESULT_JSON_START ===/,/=== TEST_RESULT_JSON_END ===/p' pytest_output.log | grep -v "TEST_RESULT_JSON" || true)
    SUMMARY_FILE=summary_${API_NAME}.json
    echo "$JSON_CONTENT" | jq . > $SUMMARY_FILE
    echo "summary_json=$(jq -c . $SUMMARY_FILE)" >> $GITHUB_OUTPUT

- name: Fail job if test failed
  run: |
    if [ -f summary_${API_NAME}.json ]; then
      LEN=$(cat summary_${API_NAME}.json | jq 'length')
      for i in $(seq 0 $((LEN-1))); do
        RESULT=$(cat summary_${API_NAME}.json | jq -r ".[$i].test_result")
        if [ "$RESULT" != "Passed" ]; then
          echo "Test $RESULT" >&2
          exit 1
        fi
      done
    fi

- name: Upload summary artifact
  if: always()
  uses: actions/upload-artifact@v4
  with:
    name: summary-${{ matrix.api }}
    path: summary_${{ matrix.api }}.json
  • echo "$JSON_CONTENT" | jq . > $SUMMARY_FILE:JSON ファイルを生成
  • echo "summary_json=$(jq -c . $SUMMARY_FILE)" >> $GITHUB_OUTPUT:ファイル内容を出力変数(summary_json)にセット
  • Fail job if test failed ステップでは test_resultPassed でない場合に exit 1 を返し、ワークフロー UI を赤く表示して失敗を可視化
  • actions/upload-artifact@v4:生成した JSON ファイルを アーティファクト として保存・ダウンロード可能にする

サマリー Markdown の自動生成

UnitTests-Result:
  needs: api
  if: always()
  runs-on: ubuntu-latest
  steps:
    - name: Download all summary artifacts
      uses: actions/download-artifact@v4
      with:
        pattern: summary-*
        path: ./summaries

    - name: Collect and Generate All API Summary
      run: |
        echo "# API UnitTest Summary" >> $GITHUB_STEP_SUMMARY
        echo "## 📊UnitTest Result" >> $GITHUB_STEP_SUMMARY
        echo '| API名 | Title | Method | Result | Status | Expected |' >> $GITHUB_STEP_SUMMARY
        echo '|-------|-------|--------|--------|--------|----------|' >> $GITHUB_STEP_SUMMARY
        for f in $(find ./summaries -name 'summary_*.json'); do
          jq -c '.[]' "$f" | while read ITEM; do
            API=$(echo "$ITEM" | jq -r '.api_name')
            TITLE=$(echo "$ITEM" | jq -r '.title')
            METHOD=$(echo "$ITEM" | jq -r '.method')
            STATUS=$(echo "$ITEM" | jq -r '.status_code')
            EXPECTED=$(echo "$ITEM" | jq -r '.expected_status_code')
            RESULT=$(echo "$ITEM" | jq -r '.test_result')
            if [ "$RESULT" = "Passed" ]; then ICON="✅"; else ICON="❌"; fi
            echo "| $API | $TITLE | $METHOD | $ICON | $STATUS | $EXPECTED |" >> $GITHUB_STEP_SUMMARY
          done
        done
  • 構造化したテスト結果のファイル出力にて出力を行ったJSON ファイルをすべてダウンロードします。

  • actions/download-artifact@v4:保存されたアーティファクトをダウンロードするアクション

  • ダウンロードしたJSON ファイルをforループで読み取っていき各値に割り当てMarkdown形式で出力しました。

他言語・フレームワークへの応用

この仕組みは Python/pytest 以外にも、Node.js(Jest)、Go(testing)、Java(JUnit)などのテストフレームワークでも同様に適用できます。CI/CD のテンプレート化にも便利です。


所感

Python で実装しましたが、構造とフローを共通化 しておけば言語を問わず応用が可能です。特に GitHub Actions のサマリー機能は、「CI の状態を誰が見ても理解しやすい」のが Good! 仕事でも趣味プロジェクトでも使い回せるので、ぜひ試してみてください。

ここまで読んでくださりありがとうございます! 誤りや改善案があればコメントいただけると嬉しいです 🙏


参考リンク

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?