시험 하나마다 시간 예산을 두고, 러너가 강제한다
순수 로직 0.5초, 백엔드 1초, E2E 3초. 넘으면 통과한 시험도 실패로 바꾸고, 예산을 늘리는 대신 로직과 준비를 고친다.
시험 스위트는 한 번에 크게 느려지지 않는다. 시험 하나하나가 조금씩 느려지다가 어느새 CI 한 번에 몇 분이 걸린다. 느려진 시험을 아무도 알아채지 못하는 것이 원인이다.
층마다 예산을 정하고 러너가 지키게 한다. 순수 함수는 0.5초, HTTP·DB를 거치는 백엔드 시험은 1초, 브라우저 E2E는 3초. 한 시험이 두 층에 걸치면 바깥 층으로 친다. 예산을 넘으면 통과한 시험도 실패로 바꾼다.
재는 범위는 시험 본문과 그 시험만을 위한 함수 범위 준비다. 세션·모듈 범위 준비는 세지 않는다. 그래서 무거운 준비를 공통 픽스처로 옮기는 것이 곧 최적화가 된다.
# conftest.py
import time
import pytest
BUDGET = 1.0
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_call(item):
start = time.process_time()
yield
item.user_properties.append(("cpu_seconds", time.process_time() - start))
@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item, call):
outcome = yield
report = outcome.get_result()
if report.when != "call" or item.get_closest_marker("slow"):
return
spent = dict(item.user_properties).get("cpu_seconds", 0.0)
if report.passed and spent > BUDGET:
report.outcome = "failed"
report.longrepr = f"시간 예산 {BUDGET}s 초과: {spent:.2f}s"
user_properties에 넣은 시간은 junit 보고서에 남아 느린 시험을 찾는 데 쓴다. Playwright는 timeout: 3000, expect.timeout: 3000, retries: 0으로, node --test는 --test-timeout으로 같은 일을 한다.
예외는 slow 표지 하나만 두고, 표지가 붙은 시험은 정기 빌드에서만 돌린다. 예산 늘리기, 시험 쪼개기, 단언 줄이기, skip으로 넘기지 않는다.