Skip to content

Latest commit

 

History

History
85 lines (72 loc) · 5.57 KB

07_testing.md

File metadata and controls

85 lines (72 loc) · 5.57 KB

07. Testing Harness — 테스트 하네스 계약

책임: 유닛 + 통합/E2E를 결정적으로 돌리는 픽스처·관례. 이음새(worker) 검증을 보장. 해소: 리뷰 D1·D2·D8·A5. 기여 방침은 CONTRIBUTING.md.

★ 핵심 결함(D1): 현 통합테스트가 ASGITransport(app=app)만 써서 lifespan 미기동 → worker 미동작 → 가짜green. 본 스펙이 이를 금지·교정한다.


디렉터리

tests/
├─ conftest.py              # 공통 픽스처(임시DB·TARGETS·lifespan)
├─ unit/                    # 부품(GT-store, GT-queue, GT-config, GT-1 일부)
├─ integration/             # 합류(GT-2,3,loss,seed,status)
├─ e2e/                     # Playwright(GT-E2E, GT-real)
└─ fixtures/
    ├─ real_page.html
    ├─ nate_rank_list.html  # m.news.nate.com/rank/list 1회 캡처
    └─ nate_article.html

★ lifespan 기동 픽스처 (D-test-lifespan, 필수)

통합/E2E는 반드시 lifespan을 띄워 worker가 돌게 한다:

# 권장(무의존): starlette TestClient — 컨텍스트 매니저로 쓰면 lifespan(init_db+worker) 실행됨
from fastapi.testclient import TestClient
with TestClient(app) as c:           # ← lifespan 기동(worker 동작)
    c.put("/api/v1/collect", json=...)
# (선택) async가 필요하면 asgi-lifespan(LifespanManager) 추가 — 기본은 TestClient로 의존성 추가 없이
  • 금지: lifespan 없이 collect→stored 단언(워커가 없어 영원히 received).
  • 적재 대기: 폴링(await until stored, timeout=2s)으로 단언(고정 sleep 지양).
  • ✅ 종료 경합(★ D-worker-safe-cancel, v1.2.1에서 근본 수정됨): PUT /collect 직후 곧바로 with TestClient(app) 블록을 빠져나가면, 워커가 소비해 run_in_executor로 넘긴 sqlite 스레드가 아직 in-flight인 채 lifespan shutdown(워커 task cancel)이 진행될 수 있었다. asyncio.Task.cancel()은 이미 던져진 스레드풀 작업을 멈추지 못하고(스레드는 계속 실행), asyncio.shield()로도 못 막는다(asyncio.Runner.close()의 _cancel_all_tasks()가 루프의 모든 태스크를 강제 취소 — shield된 태스크도 예외 없이 포함). 그 상태에서 get_db_session_ctx의 finally: conn.close()가 같은 커넥션을 다른 스레드가 여전히 쓰는 도중 실행돼(check_same_thread=False) C레벨 레이스로 세그폴트했다(반복 PUT+즉시 종료로 재현 확인, exit 139). 수정: main.py의 두 백그라운드 워커(background_queue_worker·background_retention_worker)가 DB 작업 구간을 asyncio.Event(_queue_worker_idle/_retention_worker_idle)로 감싸고, lifespan shutdown은 worker_task.cancel() 전에 그 이벤트를 기다린다(_wait_worker_idle, 최대 5s) — 워커가 안전한 지점(큐 대기 중)으로 돌아온 뒤에만 취소한다. 재현 스크립트로 800회 연속 무크래시 확인. 이제 테스트는 별도 sleep/폴링 없이 PUT 직후 바로 with TestClient(app) 블록을 빠져나가도 안전하다.

픽스처 데이터

  • 임시 DB: settings.DB_URL = sqlite:///<tmp>.db, 테스트 전후 .db/.db-wal/.db-shm 정리.
  • TARGETS: 테스트용 등록(example.com + 캡처 nate). 실타겟은 별도(아래).

마커

  • @pytest.mark.live : 실타겟(m.nate.com) 실제 수집. CI 기본 skip, 사람 승인 시 -m live로만 실행(specs/08).
  • @pytest.mark.e2e : Playwright 필요.

GT-id 라벨 규약 (verify 게이트 추적성)

  • 테스트 함수명 또는 도크스트링에 GT-id 명시: def test_..._GT_seed(): 또는 """GT-seed: ...""".
  • 게이트(scripts/verify.py)가 GT 라벨 존재를 검사(문자열). → 올바른 테스트에 붙여야 추적성이 진짜다.

★ GT-id ↔ 테스트 정본 매핑 (라벨 오부착 금지)

GT 의미 붙일 테스트(정본)
GT-1 payload 발행(command) test_api.py::test_command_api_registered_target
GT-2 수집 수신·멱등 test_api.py::test_collection_flow_and_query (또는 store upsert 멱등)
GT-3 2단계(action↔script) 일치·조회 2-stage 수집 후 stage별 일치 단언 테스트
GT-loss write-ahead 무손실 test_api.py write-ahead 적재 테스트
GT-seed worker→stored 이음새 test_gt_seed.py
GT-loader/register/correlate 식별 test_identity.py 해당
GT-E2E 브라우저 풀 사이클 tests/e2e/test_e2e_cycle.py

GT-3 정본이 없으면 2-stage 일치 테스트를 신설해 붙인다(가짜 라벨 금지).

swipe 액션 테스트(D8)

  • 픽스처(jsdom/정적 HTML): swipe는 scroll과 동등 처리로 검증.
  • 실브라우저(Playwright): 터치 스와이프(page.touchscreen/마우스 드래그)로 1-50위 로드 확인.

Negative

  • ❌ lifespan 없이 합류 단언 ❌ assert True/항상통과 ❌ 외부 네트워크 의존(live 제외) ❌ 테스트 간 DB 누수.

★ 게이트 정적 적발(D1 함정, scripts/verify.py::check_no_asgitransport_stored_fakegreen)

같은 테스트 함수가 TestClient(/LifespanManager(lifespan 기동 경로) 없이 httpx.ASGITransport(app=app)만 쓰면서 status == "stored" 등가비교를 단언하면 게이트가 FAIL로 적발한다(D1 함정 재발 방지 — 위 규칙을 사람이 매번 기억하지 않아도 됨). assert "stored" in data처럼 키 존재만 확인하는 in 비교는 대상이 아니다.

Golden Test 매핑

  • 본 하네스가 GT-seed/GT-E2E/GT-real을 실제로 합류 검증하도록 보장(tests/e2e·tests/integration).