第117回 実務で使えるPython基礎:ユニットテストとCIで作る信頼性チェックワークフロー

現場でPythonスクリプトやAI連携処理を運用していて、ふと「本当にこれを信頼して実行してよいか」と不安になったことはありませんか?小さなスクリプトでも、想定外の入力や外部APIの変化で業務に影響が出ます。本記事はその不安に寄り添い、最低限必要なユニットテスト&CIワークフローを「リポジトリにそのまま追加できる」形で示します。

なぜテストが必要か(実務リスクの観点)

短いスクリプトほど「動いているから大丈夫」と放置しがちですが、次のようなリスクがあります。

  • 入力データの形式変化(CSV列の順序や欠損)
  • 外部AIプロバイダの応答変更やレート制限
  • 想定外の例外で処理が中断し、後続バッチが止まる

目的は「完璧なカバレッジ」ではなく、現場で重要な失敗モードを再現・検出できる仕組みを作ることです。

ユニットテストの基本(pytest紹介と実例)

pytestは構文が簡潔で導入しやすく、pytest.iniやtoxでCI連携しやすいです。ここでは「CSVを読み変換する関数」と「AIプロバイダ呼び出しのラッパー」を想定します。

想定する最小コード(例)

transform.py: CSVを読み、特定列を正規化して辞書リストを返す関数。

実装メモ: コード例は環境に合わせて調整してください。例: # transform.py

ai_client.py: 実際の呼び出しはrequests経由だが、テストではモックする設計。

実装メモ: コード例は環境に合わせて調整してください。例: # ai_client.py

pytestテスト例(fixturesとtmp_pathの活用)

CSVのテストは一時ファイルを使い、AI呼び出しはモックでネットワークを張らないようにします。

実装メモ: コード例は環境に合わせて調整してください。例: # tests/test_transform.py

実装メモ: コード例は環境に合わせて調整してください。例: # tests/test_ai.py

ファイル・CSV処理のテスト例(一時ファイル、tmp_path使用)

tmp_pathはpytest組み込みのfixtureで、一時的なディレクトリを提供します。重要なのはテストデータを最小限にして失敗モードを確実に検出することです。

ケース 目的 入力例 検証方法
正常系 基本変換が動くか 標準CSV(名前の前後に空白あり) 正規化された文字列か
欠損列 指定列がない場合の挙動 列が欠けたCSV 空文字が入る、例外を出すか確認
エンコーディング UTF-8以外の検証 非UTF-8ファイル(必要なら外部で検証) 明確なエラーメッセージを期待

CLI/引数をテストする方法(argparseの例)

CLIは内部ロジックを関数化しておき、引数パース部分だけを短いテストで検証します。

実装メモ: コード例は環境に合わせて調整してください。例: # cli.py

実装メモ: コード例は環境に合わせて調整してください。例: # tests/test_cli.py

外部API/AIプロバイダのモックと契約テスト

実務では外部APIに直接アクセスするテストは避けます。2種類のテストを分けると運用が楽になります。

  • 契約テスト(ユニット):プロバイダの期待するレスポンス構造をモックで固定し、入力→期待構造検証を行う。
  • 統合テスト(任意):実際のプロバイダに対して行うテスト。頻度を限定(nightlyや手動)し、APIコストを管理する。

モックの例:unittest.mockでrequests.postを置き換える方法は前述の通りです。さらに細かいHTTP挙動を検証したい場合はrequests-mockやresponsesを使う選択肢があります。

非決定性の扱い(スナップショット/閾値)

生成結果が毎回変わる場合、完全一致チェックは現実的ではありません。実務的には次のどちらかで扱います。

  • スナップショット検査:出力構造や重要フィールドだけを固定化して比較する(部分比較)。
  • 閾値検査:出力にスコアや確信度があれば閾値を設け、閾値以上を合格とする。

flakyテスト・時間依存処理の扱い

flakyテスト(たまに失敗するテスト)はCIの信頼性を損ないます。対策の実務ルールを示します。

  • 外部に依存するテストはモック化する。
  • 時間依存処理は時刻注入(引数でnowを渡す)か、freezegunのようなライブラリで固定化する。
  • 再試行は最終手段。なぜflakyになったかの原因調査を優先する。

CI連携(GitHub Actionsでのテスト自動化)

PRごとにpytestを実行する最小構成の例です。重たい統合テストは別ジョブやnightlyに切り分けます。

実装メモ: コード例は環境に合わせて調整してください。例: # .github/workflows/ci.yml

運用上の工夫:

  • 依存キャッシュ(pip cache)やテスト分割で実行時間を短くする。
  • heavyな統合テストは “integration” ラベルで分離し、nightlyで実行する。
  • PRでの失敗はマージ禁止にし、必須チェックに設定する。

運用ルールとチェックリスト

現場で使える最小限のルールとチェックポイントを表で示します。

項目 説明 実務判断
テスト分離 ユニットは常時、統合は頻度を限定 ユニットはPR必須、統合はnightly
外部呼び出し モックでネットワーク接続を無効化 ユニット=モック、統合=実環境(限定)
重要な失敗モード 想定外のCSV、空応答、HTTP 5xxなど 各モードに1つ以上のテストを用意
フレーク対策 タイムアウト管理・時刻注入・固定乱数 原因不明な再試行は禁止
テストデータ管理 最小サンプル、ダミー優先、実データは匿名化 fixturesディレクトリで管理

成果物:貼り付けて使えるテンプレート(付録)

以下は記事本文からそのままリポジトリに追加できる最小構成のコード例です。必要に応じてプロジェクトに合わせて調整してください。

ファイル構成の例

パス 役割
transform.py CSV読み取り・変換ロジック
ai_client.py 外部AIプロバイダラッパー(requests使用)
tests/ pytestテスト(fixtures, tmp_pathを使用)
.github/workflows/ci.yml GitHub Actionsでpytestを実行

(上のコードブロックをそのままリポジトリに置けば、最小限のテストが動きます。)

まとめ

本記事では、実務で使う小さなPythonスクリプトに対して「信頼できる」状態を作るための実践的な方法を示しました。ポイントを整理します。

  • 目的は「重要な失敗モードを検出すること」。数値的なカバレッジ目標に依存しない。
  • 外部APIはユニットでは必ずモックにする。統合テストはコスト管理の下で分離する。
  • CI(GitHub Actions)でPRごとに自動テストを回し、統合テストは別スケジュールにする。
  • flaky対策、時刻注入、テストデータ管理など運用ルールを明文化する。

次の一歩:記事付録のテンプレートをリポジトリに追加して、まずは1つの機能(CSV変換やAI呼び出し)に対してテストを1つ書くことをお勧めします。次回は「運用と点検」軸で、テスト結果の自動通知やアラート設定、テスト失敗時の担当フローについて掘り下げます。

付録リンク案:実際に使えるリポジトリテンプレート(例) — https://manageai.online/repo-templates/python-test-ci-template