It failed, but the exit code was 0
Nail the failure down as a test
한국어 원문으로 표시합니다.
목표
결함이 있는 로그 회전 도구를 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) 는 정수를 돌려준다. 결함 두 개: (1) keep 보다 하나 더 남긴다, (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()을 직접 부르는 시험 하나를 쓴다. 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일 된 파일만 지우고 하루 된 파일은 남기는지 확인하는 시험을 쓴다(함수 이름에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() 을 직접 부르는 시험 하나를 쓴다. 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단계 시험도 여전히 통과해야 합니다.
두 번째 결함도 같은 순서로
tmp_path 에 파일 2개를 만들고 --keep 0 --dry-run 뒤에도 2개가 그대로인지 확인하는 시험(이름에 dry 포함)을 쓴다. 원본 코드에서 실패해야 하고, rotate.py 를 고쳐 통과시킨다.
main() 이 os.remove 를 부르기 전에 args.dry_run 을 봐야 합니다. 출력(delete ...)은 dry-run 에서도 그대로 찍는 것이 관례입니다.
입력 여러 벌을 한 시험으로
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일 된 파일만 지우고 하루 된 파일은 남기는지 확인하는 시험(이름에 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 입니다.