実践LESSON 11
この教材で学ぶこと unittestとargparseで、検証可能なcommand-line toolを作ります。PythonのテストとCLI
対象読者: Pythonの基本文法を学び、再利用できるcommand-line toolを作りたい人。
この教材でできるようになること
- pure functionと入出力を分離できる
- 標準libraryの
unittestで正常系・境界値・異常系を検証できる argparseで利用方法が分かるCLIを作れる- exit statusと標準エラーを使い分けられる
1. 先にcore logicを分ける
# study.py
def completion_rate(completed: int, total: int) -> float:
if total <= 0:
raise ValueError("total must be positive")
if not 0 <= completed <= total:
raise ValueError("completed must be between 0 and total")
return completed / total
引数だけで結果が決まり、画面表示やファイル操作を行わない関数はテストしやすくなります。
2. unittestを書く
# test_study.py
import unittest
from study import completion_rate
class CompletionRateTest(unittest.TestCase):
def test_returns_ratio(self) -> None:
self.assertEqual(completion_rate(3, 4), 0.75)
def test_accepts_zero_completed(self) -> None:
self.assertEqual(completion_rate(0, 4), 0.0)
def test_rejects_zero_total(self) -> None:
with self.assertRaisesRegex(ValueError, "total must be positive"):
completion_rate(0, 0)
def test_rejects_completed_over_total(self) -> None:
with self.assertRaises(ValueError):
completion_rate(5, 4)
if __name__ == "__main__":
unittest.main()
python -m unittest -v
実装の内部手順ではなく、外から観測できる振る舞いを検証します。
3. argparseでCLIを作る
# cli.py
import argparse
import sys
from study import completion_rate
def parser() -> argparse.ArgumentParser:
result = argparse.ArgumentParser(description="学習の完了率を計算します")
result.add_argument("completed", type=int, help="完了した教材数")
result.add_argument("total", type=int, help="教材の総数")
return result
def main(argv: list[str] | None = None) -> int:
args = parser().parse_args(argv)
try:
rate = completion_rate(args.completed, args.total)
except ValueError as error:
print(f"error: {error}", file=sys.stderr)
return 2
print(f"{rate:.0%}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
python cli.py 3 4
python cli.py --help
4. CLI自体もテストする
from contextlib import redirect_stderr, redirect_stdout
from io import StringIO
import unittest
from cli import main
class CliTest(unittest.TestCase):
def test_prints_rate(self) -> None:
output = StringIO()
with redirect_stdout(output):
status = main(["3", "4"])
self.assertEqual(status, 0)
self.assertEqual(output.getvalue(), "75%\n")
def test_reports_invalid_range(self) -> None:
error_output = StringIO()
with redirect_stderr(error_output):
status = main(["5", "4"])
self.assertEqual(status, 2)
self.assertIn("error:", error_output.getvalue())
5. filesystemを扱うとき
固定pathや利用者のhomeをテスト対象にせず、temporary directoryを使います。
from pathlib import Path
from tempfile import TemporaryDirectory
with TemporaryDirectory() as directory:
path = Path(directory) / "result.txt"
path.write_text("75%\n", encoding="utf-8")
assert path.read_text(encoding="utf-8") == "75%\n"
演習: 学習記録CLI
record.py add TITLE MINUTESとrecord.py totalを持つCLIを作ってください。
- 保存先pathは引数または環境変数で差し替えられる
- minutesは正の整数だけ許可
- JSONを一度temp fileへ書き、成功後に置換する
- core logic、file adapter、CLIを分離する
- 正常系・空data・壊れたJSON・不正入力をテストする
理解チェック
- core logicとI/Oを分けられる
- 正常系・境界値・異常系を列挙できる
- CLIのhelp、標準エラー、exit statusを設計できる
- temporary directoryで安全にfilesystem testを書ける
公式資料
次に読む
Python実践で、型・非同期処理・repository設計へ進みます。