はじめに
こんにちは、Qiita 初投稿の sabakandayo です!
みなさんは、GitHub Actions使ってますか?
「Python × GitHub Actions = ビルド&デプロイ専用ツール」
というイメージでしたが、単体テストも出来るらしいとのことで
今回は、GitHub上で完全自動化に挑戦してみました!
GitHub Actionsの機能を自分なりに詳しい解説付きでサンプルをまとめたので、同じ疑問を持つ方の参考になればうれしいです!
この記事のゴール
- GitHub Actions で API の単体テストを “手軽に” 自動化する方法を知る
- pytest の結果を GitHub Actions の サマリー にきれいにまとめるワークフローを作る
- 失敗テストが一目で分かるダッシュボードを作り、チーム開発を少しハッピーにする
目的(メリット)
GitHub Actions を使って単体テストの CI/CD を構築することで、以下のメリットがあります。
- ビルドやデプロイと連携して自動実行できる
- ローカルにテスト環境を用意しなくてよい
- チーム全員でテスト状況を可視化できる
実行イメージ
ワークフローの説明
テスト実行ジョブとテスト結果をまとめるジョブに分け、API のテストは 並列 で、実行結果は 順序付けて 実装しました。
-
Matrix:api(並列で API テストを実行)
-
test_api_fail
:期待値を満たさないテスト(例:ステータスコード 500) -
test_api_pass
:期待値を満たすテスト(例:ステータスコード 200)
-
-
UnitTests-Result(全 API テスト結果を集計)
- テスト結果の Markdown 生成とサマリー出力
実行結果イメージ
テスト結果をジョブのサマリーに Markdown 形式で表示しています。
- UnitTest Result: テスト結果の一覧表
- UnitTest Detail: API リクエスト & レスポンス詳細
主な使用技術
- GitHub Actions
- pytest
- Markdown
GitHub Actions とは
GitHub Actions は、GitHub リポジトリの push/Pull Request などのイベントをトリガーに、テスト・ビルド・デプロイを自動化できる統合 CI/CD サービスです。
pytest とは
pytest は Python 向けの軽量で高機能なユニットテストフレームワークで、最小限のコードで柔軟な自動テストを記述・実行できます。
実装してみた
主な流れ
- 各 API ごとにテスト(pytest)を実行(matrix で並列化)
- テスト結果(JSON サマリー)をアーティファクトとして保存
- 全 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 install
→pytest
を独立して走らせてくれます。
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_result
がPassed
でない場合に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! 仕事でも趣味プロジェクトでも使い回せるので、ぜひ試してみてください。
ここまで読んでくださりありがとうございます! 誤りや改善案があればコメントいただけると嬉しいです 🙏