🍞sales_data_appにpytest×GitHub Actionsを導入して、実際につまずいたこと
はじめに
現役のトラックドライバーとして物流の現場を走りながら、Pythonを独学しているtosane932です。
前回、Dockerのマルチステージビルドを実際に試した記事を書きました。あの記事の最後で「次はpytestとCI/CDについて書く」と予告していたので、今回はその実践編です。
先に結論を書いておくと、pytestを書いて満足していたら、実は肝心なところをすり抜けていたという失敗をやらかしました。そこから学んだことをそのまま書きます。
今のデプロイの流れと、その危うさ
sales_data_appは、git pushするとRenderが自動でデプロイしてくれる仕組みになっています。便利なのですが、裏を返すと、バグが入ったコードでもそのまま本番に反映されてしまう、テストなしの直行便でした。
これは物流で言うと、出発前点検をせずにそのままトラックを出しているようなものです。今回はここに、pytestとGitHub Actionsで「出発前点検ゲート」を作ってみることにしました。
まずはpytestでテストを書いてみる
sales_data_appには、Gemini APIに渡すプロンプトを組み立てるbuild_sales_promptという関数があります。まずはここから、シンプルなテストを書いてみました。
# test_prompts.py
from prompts import build_sales_prompt
def test_build_sales_prompt_includes_sales_summary():
sample_data = "月曜日: メロンパン50個、あんぱん30個"
result = build_sales_prompt(sample_data)
assert sample_data in result
def test_build_sales_prompt_includes_key_instructions():
result = build_sales_prompt("テストデータ")
assert "3点" in result
assert "箇条書き" in result
def test_build_sales_prompt_returns_string():
result = build_sales_prompt("テストデータ")
assert isinstance(result, str)
実行してみると、3つとも問題なくPASSEDでした。
test_prompts.py::test_build_sales_prompt_includes_sales_summary PASSED
test_prompts.py::test_build_sales_prompt_includes_key_instructions PASSED
test_prompts.py::test_build_sales_prompt_returns_string PASSED
一見、これで安心できそうに見えました。
わざと壊してみたら、テストの穴が見つかった
# prompts.py
def build_sales_prompt(sales_summary: str) -> str:
return f"""
あなたは食品業界のトレンドと消費者行動に精通した、経営コンサルタントです。
以下のベーカリーの販売数量データをもとに、現場で即実践できる具体的な提案を3点挙げてください。
↑ここが「上」の指示
【販売データ】
{sales_summary}
【分析の観点】
- 近年の食トレンドや競合業態の動向(例:ドーナツ専門店の減少など)
- ターゲット層別のニーズ(若年層・子育て世代・年配層)
- SNSでの訴求可能性(見た目・素材・ストーリー性)
- 曜日・季節・祝日パターンから読み取れる潜在需要
【出力形式】
箇条書き3点、各1〜2文で簡潔に。
↑ここが「下」の指示
"""
実験に使ったのは、このプロンプト生成関数です。
この「3点」が登場する2箇所を、それぞれ書き換えて実験しました。
「本当にこのテスト、ちゃんと機能してるのか?」を確かめるために、prompts.pyの中身をわざと壊してみることにしました。プロンプトの中には「提案を3点挙げてください」という指示と、「箇条書き3点」という指示の、2箇所に同じ数字が登場します。
① 下の「箇条書き3点」だけを5点に変更(3-5)
→ 結果:PASSED(素通り)
② 上の「提案を3点挙げて」も5点に変更(両方とも5点に)(5-5)
→ 結果:FAILED(検知できた)
③ 上だけ5点のまま、下は3点に戻す(5-3)
→ 結果:PASSED(素通り)
この3パターンを試して分かったのは、テストコードの
assert "3点" in result
という書き方は、「3点」という文字列がプロンプト全体のどこかに1つでも残っていれば、それだけで満足してしまうということでした。上下どちらか一方にでも3点が残っていれば、 もう一方が食い違っていても気づけない、 という穴があったのです。
「テストを書いた」 ことと、 「テストが本当に守りたいものを守れている」 ことは、別物なのだと実感しました。
テストを直す
原因が分かったので、文脈ごと確認するように書き直しました。
def test_build_sales_prompt_includes_key_instructions():
result = build_sales_prompt("テストデータ")
assert "提案を3点挙げて" in result
assert "箇条書き3点" in result
改めて同じ3パターンを試したところ、今度はどちらのケースも正しくFAILEDになりました。
AssertionError: assert '箇条書き3点' in '...箇条書き5点...'
エラーメッセージも「箇条書き3点を探したが実際は箇条書き5点だった」と分かりやすく出るようになり、テストとして意味のある状態になりました。
GitHub Actionsで自動点検の仕組みを作る
テストができたので、次はgit pushのたびに自動でこのテストを実行してくれる仕組みを作りました。.github/workflows/test.ymlというファイルを作成します。
name: Run Tests
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.12'
- name: 依存パッケージをインストール
run: |
pip install -r requirements.txt
- name: pytestを実行
run: |
pytest test_prompts.py -v
これで、pushされるたびにGitHubの仮想環境上で自動的にテストが走るようになるはずでした。
1回目の実行は失敗だった
期待してgit pushしたところ、結果はFailure。エラーコードはexit code 127でした。
これは「コマンドが見つからない」ときに出るエラーです。原因はすぐに分かりました。pytestを、ローカル環境に直接pip installしただけで、requirements.txtには追記していなかったのです。
GitHub Actionsは毎回まっさらな環境からpip install -r requirements.txtを実行するだけなので、ローカルにだけ入っていたpytestは存在しないものとして扱われていました。「ローカルで動く」と「どこでも動く」は違う、という当たり前だけど見落としがちなことを、実際につまずいて実感しました。
echo "pytest" >> requirements.txt
これを追記してgit pushし直したところ、無事にSuccess(緑のチェックマーク)になりました。
まとめ
今回の作業で得られたことを整理します。
- pytestを書いただけでは、テストが本当に何を検知できているかは分からない。わざと壊してみて初めて、テストの実力が見える
- 「同じ数字が2箇所にある」ようなケースでは、文字列の存在確認だけでなく、文脈ごと確認しないと見落としが起きる
- ローカル環境で動くことと、CI環境で動くことは別問題。依存関係は必ずrequirements.txtに反映させる
- 1回で成功するとは限らない。
exit code 127のようなエラーメッセージから原因を推測し、直していくプロセスそのものが学びになる
今回、actions/checkoutまわりで「Node.js 20 is deprecated」という警告も出ていました。テストの合否には影響していませんが、今後のメンテナンス課題として頭に置いておこうと思います。
前回のDockerfileの記事、今回のpytest×GitHub Actionsの記事と、2本続けて「実際にやってみたら、思っていたのと違った」という結果になりました。事前に想像していた通りにいかないからこそ、書く意味のある記録になっている気がしています。
(この記事は実際の作業ログに基づいて書いています。)
sales_data_app 整備記録シリーズ
「本番出走前・車検三部作」
🚚「出走後メンテナンスシリーズ」
🍞
