TT Lab
はじめる
学ぶ 学習パス コース

失敗したのに終了コードは 0 だった

失敗をテストとして固定する

TT Labで続きを見る

目標

欠陥のあるログローテーションのツールを、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なのに削除してしまうことです。

ステップ

  1. 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が通ります。
  2. tmp_pathに.logファイルを5つ作って、rotate.main([str(tmp_path), "--keep", "3"])のあとに3つ残るかを確認するテストを書いてください。関数名にkeepが入っている必要があります。このテストは、今のコードで失敗しなければなりません(再現)。
  3. rotate.pyのplan()を直して、keep個だけ残るようにしてください。pytest -qがすべて通ります。
  4. tmp_pathにファイルを2つ作って、--keep 0 --dry-runのあとでも2つがそのままかを確認するテストを書いてください(関数名にdryを含める)。元のコードでは失敗しなければならず、rotate.pyを直して通します。
  5. keepを0・1・3・10に変えながら、残るファイルの数がmin(5, keep)かを確認するテストを、@pytest.mark.parametrizeで書いてください。pytest --collect-only -qに、[が付いたIDが4つ以上見える必要があります。
  6. capsysで、delete <파일이름>(プレースホルダーはファイル名です)が標準出力に出力されるかを確認するテストを書いてください(関数名にoutputを含め、capsysを使う)。
  7. monkeypatchで、rotate.time.timeを固定したうえで、--older-than-days 7が8日前のファイルだけを削除して、1日前のファイルは残すかを確認するテストを書いてください(関数名にolderを含め、monkeypatchを使う)。ファイルの更新時刻は、os.utimeで合わせます。
  8. pytest -q --junitxml=/root/pyops/test/report.xmlで、レポートを残してください。テストが7つ以上で、failuresとerrorsが0である必要があります。

参考

純粋関数からテストする

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です。