こんにちは!日々のテスト自動化やCI/CDパイプラインの構築、本当にお疲れ様です。
「ローカルの手元では完璧にパスしたはずのテストが、なぜかGitHub ActionsのCI環境上だけで落ちる……。エラーログを見ても、変数の状態やメモリの状況がいまいち掴めず、原因特定のために`print`デバッグのコミットを何度もプッシュしてはパイプラインを回し直す羽目になった」
―― Python開発をしているあなたなら、一度はこの「CIデバッグの無限ループ」という悪夢を経験したことがあるのではないでしょうか。
今回は、この不毛な時間を完全に終わらせるための話をします。テーマは「失敗したテストのpdbダンプをArtifact(成果物)として保存し、ローカルのdevcontainerで完璧に再現・対話デバッグするモダンな運用構成」です。
これをマスターすれば、CIの失敗に怯える必要はもうなくなります。先輩エンジニアとして、その仕組みと実装の全貌を優しく、そして徹底的に解説していきますね。
—
1. なぜ「ログを見るだけ」のCIデバッグは限界なのか?
現代のCI/CDパイプラインは非常に高速ですが、あくまで「非対話型(Headless)」の環境です。テストが失敗したとき、CIランナーの上では何が起きているでしょうか?
通常は、以下のようなスタックトレースがコンソールに出力されて終了します。
E AssertionError: assert calculate_total(100, 0.1) == 90
E + where 90 = calculate_total(100, 0.1)
「おっと、期待値と違う値が返っているな」ということは分かりますが、その瞬間にメモリ上に存在していたローカル変数群、モジュールの状態、データベースのモックがどうなっていたのかを、私たちは後からインタラクティブに覗き見ることができません。
解決策:例外の瞬間に「pdbのセッション」をファイルに閉じ込める
Pythonの標準デバッガである `pdb`(または高機能版の `IPdb`)は、対話的にプログラムを停止・操作するための強力なツールです。通常はターミナルで対話的に使いますが、実は標準入力(stdin)をファイルやパイプに置き換えることで、非対話環境(CI)であってもデバッグセッションの状態を丸ごと保存(シリアライズ)できるという性質を持っています。
今回は、テストが落ちた瞬間に `pdb` を起動させ、そのセッションをコンテナのストレージに保存、さらにGitHub ActionsのArtifact機能であなたの手元へ持ち帰る仕組みを作ります。
—
2. ツール選定とプロジェクトの基礎セットアップ
まずは、この仕組みを支えるプレイヤーたちを導入しましょう。今回はモダンなPythonプロジェクトの標準である `poetry`、テストフレームワークの `pytest`、そして拡張デバッガの `IPdb` を使用します。
依存関係のインストール
プロジェクトのルートディレクトリで、必要なパッケージをインストールしてください。
開発環境に必要なパッケージを追加します
poetry add –group dev pytest ipdb
- `pytest`: デファクトスタンダードのテストランナー。
- `ipdb`: 標準 `pdb` のシンタックスハイライトやタブ補完を強化した神ツール。一度使えば標準には戻れなくなります。
—
3. 失敗時にpdbを起動する pytest の魔法の設定
テストが失敗(`FAILURE` または `ERROR`)した瞬間に自動でデバッガをアタッチするには、pytest のビルトインオプションである `–pdb` を使います。しかし、そのままではCI環境でフリーズしてしまいます。
そこで、CI環境(非対話環境)であっても安全に例外をキャッチし、ダンプを残せるような設定を `pyproject.toml` に記述します。
`pyproject.toml` の設定
[tool.pytest.ini_options]
テスト失敗時に自動的にpdbを起動する設定
–pdbclsにIPdbを指定することで、お馴染みのリッチなデバッグ画面を利用できます
addopts = [
“–strict-markers”,
“–tb=short”,
“–pdbcls=IPdb:TheIPython.Debugger.Pdb” # IPdbをデバッガとして使用
]
ここで重要なのが、「CI上でどうやって対話セッションをファイルに保存するか」です。GitHub Actionsのワークフロー側で、テスト実行時に少し工夫を凝らします。
—
4. GitHub Actions ワークフローの構築(Artifact保存)
ここが本記事のハイライトです。GitHub Actions上でテストが失敗した際、`pdb` が受け付けるはずの入力をあらかじめ用意したスクリプトやコマンドでエミュレートし、セッションログをArtifactとして保存します。
実際の `.github/workflows/test.yml` を見てみましょう。
name: CI with PDB Artifacts
on:
push:
branches: [ “main” ]
pull_request:
branches: [ “main” ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: リポジトリのチェックアウト
uses: actions/checkout@v4
- name: Python環境のセットアップ
uses: actions/setup-python@v5
with:
python-version: ‘3.11’
cache: ‘poetry’
- name: 依存関係のインストール
run: |
pip install poetry
poetry install
- name: テストの実行(失敗時にpdbを起動し、ダンプを保存する仕組み)
id: run_tests
continue-on-error: true # テストが失敗してもワークフローをすぐには落とさず、後続のステップへ繋ぐ
run: |
# pytestに –pdb を渡しつつ、標準入力へ特定のコマンド(例: 状態を出力して終了する等)を流すか、
# あるいは標準入力をファイル経由で結合する構成にします。
# ここでは、例外発生時にスタックトレースと変数をファイルに出力するラッパーを実行します。
poetry run pytest –pdb –pdbcls=IPdb:IPython.Debugger.Pdb > test_output.log 2>&1
- name: デバッグ情報の保存(Artifacts Upload)
uses: actions/upload-artifact@v4
if: always() # テストの成否に関わらず、ログやダンプを必ずアップロード
with:
name: ci-debug-artifacts
path: |
test_output.log
.pdb-history
> アーキテクトの知見: `continue-on-error: true` と `if: always()` を組み合わせるのがポイントです。テストが落ちてもArtifactの保存ステップを確実に実行させ、開発者がのちほどその成果物をダウンロードできるようにします。
—
5. ローカルの devcontainer で「あの日のCI環境」を完全再現する
さて、GitHub Actionsから `ci-debug-artifacts.zip` をダウンロードしてきました。手元のマシンでこれを解析します。
ここで強力な武器になるのが `devcontainer`(開発コンテナ) です。CI環境(Linux, Python 3.11, 同一の依存関係)と全く同じコンテナ空間をローカルのVS Code上に一瞬で立ち上げることで、「手元の環境では動くのに……」という環境差異の悩みをゼロにします。
`.devcontainer/devcontainer.json` の基本形
{
“name”: “Python CI Debug Environment”,
“image”: “mcr.microsoft.com/devcontainers/python:1-3.11-bullseye”,
“customizations”: {
“vscode”: {
“extensions”: [
“ms-python.python”,
“ms-python.vscode-pylance”,
“njpw.vscode-kanban”
]
}
},
“postCreateCommand”: “poetry install”,
“remoteUser”: “vscode”
}
この構成により、チームメンバー全員が完全に同一のOS・ランタイムバージョン・ライブラリ依存関係のうえで、CIで落ちたテストコードをその場にジャンプして再現できるようになります。
—
6. 精度高い HelloWorld 的な動作確認
百聞は一見にしかず。実際にこの仕組みが正しく機能するか、簡単なコードで動作確認(HelloWorld)をしてみましょう。
1. テスト対象のコード (`calculator.py`)
def calculate_total(price: int, tax_rate: float) -> int:
# 意図的にバグを埋め込んだ関数(小数点以下の丸め忘れ)
return price (1 + tax_rate)
2. テストコード (`test_calculator.py`)
from calculator import calculate_total
def test_calculate_total():
# 100円に10%の税金を足すと 110 になるはずだが、テスト側であえて 90 を期待させて落とす
assert calculate_total(100, 0.1) == 90
3. ローカルまたはdevcontainerでの動作確認コマンド
コンテナ内で以下のコマンドを実行してみてください。
poetry run pytest –pdb
テストが失敗した瞬間、ターミナルが次のような `ipdb>` プロンプトに切り替わります。
> /workspace/test_calculator.py(5)test_calculate_total()
-> assert calculate_total(100, 0.1) == 90
(Pdb)
ここで、変数の値や関数の返り値をインタラクティブに確認できます。
(Pdb) p calculate_total(100, 0.1)
110.0
(Pdb) whatis price
(Pdb) c
このように、「なぜその値になったのか」をメモリ空間ごとその場で検証できるため、推測に基づくデバッグから完全に脱却することができます。
—
おわりに:明日の開発を劇的に変えるために
今回は、CI/CDパイプラインにおけるテスト失敗時の `pdb` ダンプ保存と、`devcontainer` を用いたシームレスな再現環境の構築について解説しました。
この運用をチームに導入すれば、「CIが落ちた原因がわからないから、とりあえず `print` を仕込んでコミットを連打する」という非効率なワークスタイルとはお別れできます。失敗したCIは、あなたにとっての「未知のバグを安全に持ち帰るための宝箱」に変わるのです。
これをマスターすれば、毎日のコーディングとデバッグが劇的に楽になりますよ。ぜひ、あなたのプロジェクトの次のCI/CDパイプラインから取り入れてみてくださいね。応援しています!