ふくふくHukuhuku Inc.
EP.09Testing 15分公開: 2026-09-09

GitHub Actions でのテスト自動化 ── 構成の基本形

ワークフローは、置き場所を間違えると待ち時間と費用の両方が膨らみます。ジョブの分け方、キャッシュ、並列度、失敗時の証跡まで、実際に使える構成の型をまとめます。

#テスト#CI#GitHub Actions#自動化
執筆 / 監修
松尾 亮合同会社ふくふく 代表社員

データ基盤・データパイプライン構築 / BI / 生成 AI 活用支援を専門とするエンジニア (28 年)。 本記事は AI 利用ポリシーに基づき、生成 AI の補助で執筆 → 人間が監修・編集して公開しています。

プロフィール詳細
シェア

ここまでは何をどう検証するかの話でした。ここからは、それを自動で回す側に移ります。 を題材にしますが、考え方は他の でもおおむね共通です。

重要なのは、構成の良し悪しがそのまま待ち時間と費用に直結するという点です。EP.3 で扱った実行時間の話は、ここで金額として現れます。

1. まず最小の形を置く

凝った構成を最初から作る必要はありません。プルリクエストで速い層だけを流すところから始めます。

.github/workflows/test.yml — 最小構成
YAML
name: test
on:  pull_request:  push:    branches: [main]
# 同じブランチで新しい push があれば、古い実行を打ち切るconcurrency:  group: test-${{ github.ref }}  cancel-in-progress: true
jobs:  fast:    runs-on: ubuntu-latest    timeout-minutes: 10    steps:      - uses: actions/checkout@v4      - uses: actions/setup-python@v5        with:          python-version: '3.12'          cache: pip      - run: pip install -r requirements.txt      - run: pytest tests/unit -q

短いですが、重要な設定が3つ入っています。`concurrency` による打ち切り`timeout-minutes` による上限依存のキャッシュです。この3つは最初から入れておく価値があります。ここで流しているのは EP.3 で言う高速層 ── だけです。重い層は後から足せばよく、最初から全部を載せようとすると立ち上がりません

最初から入れておきたい3つ
設定入れないとどうなるか
`concurrency` + `cancel-in-progress`連続 push のたびに古い実行が走り続け、待ち時間と費用が二重にかかる
`timeout-minutes`ハングしたジョブが上限まで回り続ける。最も無駄な費用
依存のキャッシュ毎回インストールから始まる。実行時間の大半を占めることがある
timeout は必ず入れる

既定の上限は非常に長く設定されています。待ち続けるだけのジョブが上限まで回ると、そのぶんがそのまま費用になります。想定時間の2〜3倍程度を上限として置いておくと、異常時の被害が止まります。

2. ジョブをどう分けるか

テストが増えたら分割を検討します。ただし分ければ速くなるとは限りません。ジョブごとにコンテナの起動、コードの取得、依存の復元が発生するためです。

分ける判断は、失敗をどれだけ早く知りたいかで決めます。書式チェックや型検査のように数秒で終わり、失敗すれば以降が無意味になるものは、先に独立して流す価値があります。

早く落ちるものを先に、重いものを後に
YAML
jobs:  lint:    runs-on: ubuntu-latest    timeout-minutes: 5    steps:      - uses: actions/checkout@v4      - uses: actions/setup-python@v5        with: { python-version: '3.12', cache: pip }      - run: pip install -r requirements-dev.txt      - run: ruff check .      - run: mypy src
  unit:    needs: lint          # lint が通ってから流す    runs-on: ubuntu-latest    timeout-minutes: 10    steps:      - uses: actions/checkout@v4      - uses: actions/setup-python@v5        with: { python-version: '3.12', cache: pip }      - run: pip install -r requirements.txt      - run: pytest tests/unit -q -n auto
  e2e:    needs: unit          # 単体が通ってから、重い層を流す    runs-on: ubuntu-latest    timeout-minutes: 20    steps:      - uses: actions/checkout@v4      - run: npx playwright install --with-deps chromium      - run: npx playwright test      - uses: actions/upload-artifact@v4        if: failure()    # 落ちたときだけ証跡を残す        with:          name: playwright-report          path: playwright-report/

`needs` で順序をつけると、手前が落ちた時点で後段は動きません。重い を無駄に流さずに済みます。一方で、直列にするぶん全体の完了は遅くなります。落ちる確率が高い順に前へ置くのが基本です。

最後の `if: failure()` が実務では効きます。EP.4 で書いた証跡の話で、落ちたときだけ記録を持ち帰る形にすると、保管の費用を抑えつつ原因調査ができます。

3. キャッシュを効かせる

実行時間の中で、依存のインストールが占める割合は想像より大きいものです。キャッシュが効いているかどうかで体感が大きく変わります。

  • キーはロックファイルのハッシュで作る — 依存が変わったときだけ作り直される
  • 復元用のキーを段階的に用意する — 完全一致がなくても近いものを使える
  • 保存対象を正しく指定する — 対象がずれていると毎回不命中になる
  • ログで命中を確認する — 効いているつもりで効いていない、が最も多い
キーの作り方(ロックファイルの内容で決める)
YAML
- uses: actions/cache@v4  with:    path: ~/.cache/pip    key: pip-${{ runner.os }}-${{ hashFiles('requirements*.txt') }}    restore-keys: |      pip-${{ runner.os }}-

`restore-keys` があると、完全一致がない場合でも接頭辞が一致する直近のキャッシュを使えます。依存を1つ追加しただけで全部インストールし直す、という事態を避けられます。

ブラウザや大きなツールも対象になる

E2E で使うブラウザ本体のように、サイズが大きく変更頻度が低いものはキャッシュの効果が特に大きい領域です。毎回ダウンロードしている場合、そこだけで実行時間の相当部分を占めていることがあります。

4. マトリクスは絞って使う

は、複数のバージョンや環境を並列で検証できる便利な仕組みです。ただし組み合わせの数だけ実行時間と費用が増えます

全組み合わせではなく、意味のある組み合わせだけを流す
YAML
strategy:  fail-fast: false      # 1つ落ちても他は最後まで流す  matrix:    python: ['3.11', '3.12']    os: [ubuntu-latest]    include:      # 最新版だけ macOS でも確認する(全組み合わせは作らない)      - python: '3.12'        os: macos-latest

`include` を使うと、全組み合わせを展開せずに必要な組だけ追加できます。`fail-fast: false` は、1つ落ちたときに他を打ち切らない設定です。どの組み合わせで落ちるかを一度に知りたい場合は false、費用を優先するなら既定のままにします。

5. 秘密情報の扱い

テストで外部サービスを使う場合、鍵の扱いが問題になります。基本はリポジトリのシークレット機構に置き、ログに出さないことです。加えて、押さえておくべき前提があります。

  • 外部からのプルリクエストにはシークレットが渡らない — 既定の挙動。これに依存したジョブは失敗する
  • ログへの出力に注意する — 環境変数を一覧表示する処理が混ざると漏れる
  • 権限は最小に絞る — ワークフローに与えるトークンの権限は既定を見直す
  • 外部サービスを使わない構成も検討する — EP.7 で扱ったとおり、決定的な層は呼び出し不要

1番目は設計に影響します。外部からの貢献を受け付けるリポジトリでは、鍵が要るテストは流れない前提で組む必要があります。鍵が要る層を分離しておくと、この制約と自然に整合します。EP.7 で扱った「非決定的な範囲を狭く閉じ込める」設計は、ここでも効いてきます。 のようにモデルの鍵を必要とする層を切り離しておけば、外部貢献でも大半のテストは流れる状態を保てます。

6. 結果を読める形で返す

最後に、失敗をどう伝えるかです。ログを最後まで遡らないと原因が分からない状態だと、確認そのものが億劫になります。

実行結果の要約を、まとめ画面に出す
YAML
- name: テスト結果を要約する  if: always()  run: |    echo '## テスト結果' >> $GITHUB_STEP_SUMMARY    python -c "    import xml.etree.ElementTree as ET    r = ET.parse('report.xml').getroot()    total = int(r.get('tests', 0)); fails = int(r.get('failures', 0))    print(f'- 実行 {total} 件 / 失敗 {fails} 件')    " >> $GITHUB_STEP_SUMMARY

`if: always()` を付けるのが要点です。失敗したときこそ要約が要るので、成功時のみ実行される既定のままでは意味がありません。

CI の実行状況そのものを継続的に見る方法については、エンジニアリング・ダッシュボード EP.05「CI/CD コストの可視化」 が参考になります。次回はその費用を下げる具体策を扱います。

ここまでのまとめ

最初から入れるべきは打ち切り・上限・キャッシュの3つ。ジョブは落ちる確率が高い順に前へ。証跡は失敗時だけ残す。マトリクスは `include` で必要な組だけ。次回は費用の削り方を具体的に扱います。

シェア

この記事の感想を教えてください

あなたの 1 クリックで、本当にこの記事は更新されます。「もっと詳しく」「続編希望」が一定数集まった記事は、 ふくふくが 実際に内容を拡充したり続編記事を公開 します。 送信したリアクションはお使いのブラウザに記録され、再カウントされません。

シリーズの外も探す:

まずは、現状を聞かせてください。

要件が固まっていなくて大丈夫です。現状診断と方針提案までを無料でお手伝いします。

無料相談フォームへ hello [at] hukuhuku [dot] co [dot] jp