Python講座 / unittestで正常値・境界値・失敗をテストする

自動テストは決めた入力に対する結果や例外を繰り返し確認します。利用者が必要とする動作、境界、失敗条件を検査すると、後の変更による不具合を見つけやすくなります。

確認する規則を先に決める

totalは非負の整数のリストを受け取り、空のリストなら0を返し、boolや文字列を拒否します。時計、ネットワーク、利用者のファイルに依存しない小さな計算から始めると、結果を再現しやすくなります。

unittestは標準ライブラリです。ファイル名とテストメソッド名にはtest_の接頭辞を付けます。表示を人が読むだけの場合と違い、assertEqualなどは不一致をプログラムが検出できる失敗へ変えます。

ファイルとDBを含む結合検証は、最終実習:SQLiteの支出管理アプリを作るで扱う規則とつながります。

関数とテストの全体を保存する

新しいフォルダーへexpense_math.pyを保存します。

def total(amounts):
    if any(type(x) is not int or x < 0 for x in amounts):
        raise ValueError("invalid amount")
    return sum(amounts)

同じ場所へtest_expense_math.pyを保存します。

import unittest
from expense_math import total

class TotalTests(unittest.TestCase):
    def test_regular(self):
        self.assertEqual(total([3500, 8000, 2500]), 14000)

    def test_empty(self):
        self.assertEqual(total([]), 0)

    def test_invalid(self):
        for values in [[-1], [True], ["8000"]]:
            with self.subTest(values=values):
                with self.assertRaises(ValueError):
                    total(values)

if __name__ == "__main__":
    unittest.main()

そのフォルダーで実行します。

python -m unittest -v

3件のテストと最後のOKを確認します。subTestは不正な入力の3条件を区別します。所要時間や表示の細部は環境によって異なります。

不具合を検出することを確認する

一時的に戻り値をsum(amounts) + 1へ変更し、通常の入力と空の入力のテストが失敗するか確認します。元の計算へ戻し、OKを再確認してください。

入力検証を外した場合も不正入力のテストが失敗するか調べ、元へ戻します。意図的な変更を検出できれば、テストが守る規則を確かめられます。壊した実装を最終状態として残さないでください。

単体テストと結合確認を分ける

合計のテストが成功しても、CSVの見出しやデータベース接続まで正しいとはいえません。ファイルやデータベースをつなぐ結合テストには、一時的な専用パスと管理したデータを用意し、本番記録を使わないようにします。

検証を記録するときはPythonの版、コマンド、入力と結果を残します。確認していないOSのインストール画面などへ結果を広げないでください。CSVレポート実習では、ファイルと計算をつないだ具体的な照合を行えます。

関連する公式リファレンス

単体テストは関数の規則、結合テストはコマンドライン・ファイル・終了値の連携を確認します。CSV実習のcsv_report.pyを下のcheck_csv.pyの隣に置いて実行します。一時フォルダーで重複IDの拒否と報告内容を確認するため、作業中の報告書は上書きせず、終了後に一時フォルダーを片付けます。

全体を check_csv.py に保存し、同じ練習フォルダーで python check_csv.py を実行します。 先に csv_report.py も同じ場所へ置きます。

import subprocess
import sys
import tempfile
from pathlib import Path
program = Path("csv_report.py").resolve()
with tempfile.TemporaryDirectory() as folder:
    work = Path(folder)
    source = work / "expenses-input.csv"
    source.write_text("id,category,amount\ne1,food,8000\ne1,food,8000\n", encoding="utf-8")
    result = subprocess.run([sys.executable, str(program)], cwd=work, capture_output=True, text=True)
    assert result.returncode == 1
    assert "food,8000" in (work / "report.csv").read_text(encoding="utf-8")
    assert "duplicate id" in (work / "rejected.csv").read_text(encoding="utf-8")
print("CSV integration checks passed")

予想結果:

CSV integration checks passed