実践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設計へ進みます。