失敗をテストとして固定する
目標
欠陥のあるログローテーションのツールを、pytestで再現して直します。純粋関数のテスト、tmp_path、失敗するテストを先に、parametrize、capsys、monkeypatch、JUnitレポートです。
なぜ重要なのか
直したという言葉は、証拠ではありません。バグを再現するテストは、直す前は失敗し、直したあとは通り、そのあとは、同じバグが戻ってくるのを防ぎます。ファイル・時間・出力に依存する運用ツールは、tmp_path・monkeypatch・capsysで、その依存をテストの中に閉じ込めます。このラボの採点は、皆さんのテストファイルを元の欠陥のあるコードに対しても実行して、テストが本当にバグを見つけるかを、両方向から確認します。
材料: /opt/fixtures/pyops/buggy/rotate.py。rotate.py DIR --keep N [--dry-run] [--older-than-days D]です。plan(files, keep)は、削除するファイルのリストを返す純粋関数で、main(argv)は整数を返します。欠陥は2つです。(1)keepより1つ多く残してしまうこと、(2)--dry-runなのに削除してしまうことです。
ステップ
mkdir -p /root/pyops/test && cp /opt/fixtures/pyops/buggy/rotate.py /root/pyops/test/でコピーして、/root/pyops/test/test_rotate.pyに、plan()を直接呼び出すテストを1つ書いてください。keepがファイル数より大きければ、空のリストでなければなりません。pytest -qが通ります。- tmp_pathに.logファイルを5つ作って、
rotate.main([str(tmp_path), "--keep", "3"])のあとに3つ残るかを確認するテストを書いてください。関数名にkeepが入っている必要があります。このテストは、今のコードで失敗しなければなりません(再現)。 - rotate.pyのplan()を直して、keep個だけ残るようにしてください。
pytest -qがすべて通ります。 - tmp_pathにファイルを2つ作って、
--keep 0 --dry-runのあとでも2つがそのままかを確認するテストを書いてください(関数名にdryを含める)。元のコードでは失敗しなければならず、rotate.pyを直して通します。 - keepを0・1・3・10に変えながら、残るファイルの数がmin(5, keep)かを確認するテストを、
@pytest.mark.parametrizeで書いてください。pytest --collect-only -qに、[が付いたIDが4つ以上見える必要があります。 - capsysで、
delete <파일이름>(プレースホルダーはファイル名です)が標準出力に出力されるかを確認するテストを書いてください(関数名にoutputを含め、capsysを使う)。 - monkeypatchで、
rotate.time.timeを固定したうえで、--older-than-days 7が8日前のファイルだけを削除して、1日前のファイルは残すかを確認するテストを書いてください(関数名にolderを含め、monkeypatchを使う)。ファイルの更新時刻は、os.utimeで合わせます。 pytest -q --junitxml=/root/pyops/test/report.xmlで、レポートを残してください。テストが7つ以上で、failuresとerrorsが0である必要があります。
参考
- テストファイルは、
/root/pyops/test/の中で、pytest -qで実行します。rotate.pyと同じディレクトリにあれば、import rotateができます。 - 8日前の時刻:
os.utime(path, (t, t))に、t = FIXED - 8 * 86400を入れます。FIXEDは、monkeypatchでtime.timeが返すようにした値です。 - よくあるミスは、本物の時間を待つsleep、固定パスの/tmpの使用、直す前に失敗するかを確認しないことです。
純粋関数からテストする
rotate.pyを/root/pyops/test/にコピーして、/root/pyops/test/test_rotate.pyに、plan()を直接呼び出すテストを1つ書いてください。keepがファイル数より大きければ、空のリストでなければなりません。pytest -qが通ります。
import rotateのあとで、rotate.plan(["a.log", "b.log"], keep=10) == []をassertすれば済みます。テスト関数の名前は、test_で始まらないと、pytestが見つけません。
失敗するテストで再現する
tmp_pathに.logを5つ作って、rotate.main([str(tmp_path), "--keep", "3"])のあとに3つ残るかを確認するテスト(名前にkeepを含める)を書いてください。今のコードでは、失敗しなければなりません。
(tmp_path / f"app-{i}.log").write_text("x")で作って、list(tmp_path.glob("*.log"))の長さを見ます。欠陥のせいで4つ残って、テストが赤になるのが正常です。
直して、緑を確認する
rotate.pyのplan()を直して、keep個だけ残るようにしてください。pytest -qがすべて通ります。
スライスの終わりが、len(files) - keepでなければなりません。直したあとで、ステップ2のテストが通り、ステップ1のテストも、引き続き通る必要があります。
2つ目の欠陥も、同じ順序で
tmp_pathにファイルを2つ作って、--keep 0 --dry-runのあとでも2つがそのままかを確認するテスト(名前にdryを含める)を書いてください。元のコードで失敗しなければならず、rotate.pyを直して通します。
main()が、os.removeを呼び出す前に、args.dry_runを見る必要があります。出力(delete ...)は、dry-runでもそのまま出力するのが慣例です。
複数の入力を、1つのテストで
keepを0・1・3・10に変えながら、残るファイルの数がmin(5, keep)かを確認するテストを、@pytest.mark.parametrizeで書いてください。pytest --collect-only -qに、[が付いたIDが4つ以上出ます。
@pytest.mark.parametrize("keep", [0, 1, 3, 10])を関数の上に付けて、引数としてkeepを受け取ります。IDは、test_名前[0]、test_名前[1]のように付きます。
出力も契約
capsysで、delete <ファイル名>が標準出力に出力されるかを確認するテスト(名前にoutputを含め、capsysを使う)を書いてください。
関数の引数にcapsysを入れて、rotate.main(...)のあとで、capsys.readouterr().outに"delete app-0.log"があるかをassertします。残したファイルの名前は、出力に含まれていてはいけません。
時間を差し替える
monkeypatchで、rotate.time.timeを固定したうえで、--older-than-days 7が8日前のファイルだけを削除して、1日前のファイルは残すかを確認するテスト(名前にolderを含め、monkeypatchを使う)を書いてください。
monkeypatch.setattr(rotate.time, "time", lambda: FIXED)で固定して、os.utime(path, (t, t))で、ファイルの時刻をFIXED - 886400、FIXED - 186400に合わせます。
機械が読むレポート
pytest -q --junitxml=/root/pyops/test/report.xmlで、レポートを残してください。テストが7つ以上で、failures・errorsが0である必要があります。
--junitxmlは、testsuite要素に、tests・failures・errorsの属性を残します。失敗があれば、終了コードも1です。