테스트 가이드
테스트는 1급 시민입니다. PR 머지 게이트는 **변경된 코드의 line coverage ≥ 80 %**이며, Playwright E2E 스위트는 PR에서 실행되지 않습니다(아래 커버리지 게이트 참고). 이 페이지는 레이아웃, 하네스 패턴, 정적 분석으로는 못 잡는 버그를 잡는 적대적 입력 규칙을 다룹니다.
모든 컨트리뷰터. apps/backend/나 apps/frontend/를 건드리는 모든 PR에 적용.
Backend — pytest
테스트는 apps/backend/tests/ 아래에 있으며 세 계층으로 분리됩니다.
apps/backend/tests/
├── unit/ # 순수 함수 테스트, DB·네트워크 없음
├── integration/ # FastAPI TestClient + Postgres (testcontainers)
└── e2e/ # 백엔드 단독 블랙박스 흐름; Playwright 스위트와 다름
각 계층의 conftest.py는 적절한 fixture를 노출합니다. 최상위 conftest.py는 계층 간 공용 헬퍼(factory, time freezing)를 제공합니다.
집중된 셋만 실행
cd apps/backend
# 전체 스위트
pytest -q
# 단일 계층
pytest -q tests/unit
# 키워드로
pytest -q -k "api_key and revoke"
# 단일 테스트 + print
pytest -s tests/integration/test_api_key_endpoints.py::test_revoke_immediate
git worktree 안에서 작업할 때
세 가지가 예상과 다릅니다.
tools/ 아래 저장소 도구가 저장소 루트를 찾는 방식이 서로 다릅니다. tools/em-dash/lint.mjs는 git에게 물으므로 실행 위치를 따릅니다. tools/ko-style/lint.mjs는 자기 파일 위치에서 루트를 계산하므로, 메인 체크아웃의 복사본을 절대 경로로 부르면 워크트리 안에서 실행하든 워크트리 파일을 인자로 주든 메인 트리를 검사합니다. 워크트리 자기 복사본을 쓰십시오. 린트가 내 파일을 읽고 있는지 확인하려면 그중 하나에 위반을 심어 정확한 줄에서 잡히는지 보고 되돌립니다. 검사한 파일 수는 내 브랜치가 파일을 추가했을 때만 달라지므로 그것만으로는 알 수 없습니다.
내 앞 리비전이 아직 다른 사람 브랜치에 남아 있어 alembic upgrade head가 풀리지 않을 때는 자리표시자 마이그레이션을 만들되, 파일 이름에 _local_stub을 넣고 .git/info/exclude에는 그 정확한 경로를 적습니다. 글롭이 아닙니다. 그 파일은 git 공통 디렉터리에 하나뿐이라 모든 워크트리에 적용되므로, 0081_*.py라고 적으면 동료의 진짜 0081_finding_assignment.py가 그쪽 git status에서 사라지고 마이그레이션이 빠진 채로 PR이 열립니다. 그리고 진짜 리비전이 main에 들어오는 즉시, 리베이스하기 전에 자리표시자를 지우십시오. 같은 revision 문자열을 선언한 파일이 둘이면 alembic이 아예 멈춥니다. 그때 .git/info/exclude의 해당 줄도 함께 지웁니다. 남은 줄은 무해하지 않습니다. 공용 파일이고, 이제는 아무도 볼 수 없는 경로를 가리키며, 다음 사람이 그 줄이 무엇을 막고 있었는지 알아내야 합니다.
리베이스한 결과에 낯선 파일이 없는지 볼 때 git diff --name-only origin/main을 쓰는 것도 예상과 다릅니다. 이 명령은 "무엇이 다른가"에 답하므로 내 변경과 브랜치를 딴 뒤 main이 얻은 것을 함께 보여 줍니다. 그래서 남이 머지한 파일이 내가 지운 파일처럼 보입니다. 병합 기준과 비교하거나, 리베이스한 뒤에 보십시오. 낯선 이름이 나왔을 때 main이 움직인 것인지 내가 움직인 것인지는 git log HEAD..origin/main이 가릅니다. 이것이 헷갈리는 것으로 끝나지 않는 이유는 소음입니다. 확인할 때마다 낯선 이름이 나오면 목록을 훑고 넘기게 되고, 진짜 사고는 그 목록 안에 있습니다.
자리표시자가 닿은 데이터베이스는 버린다
자리표시자 마이그레이션을 적용하면 DDL은 하나도 실행하지 않은 채 그 리비전만 적용됨으로 기록됩니다. 나중에 진짜 리비전이 들어오면 alembic이 alembic_version에서 그 id를 보고 건너뛰고, alembic upgrade head를 몇 번 돌려도 복구되지 않습니다. 데이터베이스는 최신 리비전이라고 보고하면서 그 리비전이 추가한 컬럼이 없는 상태로 남습니다. 없는 컬럼 때문에 테스트 하나가 실패하기 전까지 이상해 보이는 것이 없고, 그 실패는 지금 손에 든 코드의 결함처럼 읽힙니다. 여기서 실제로 그랬고, 기록된 리비전과 실제 스키마를 대조하기 전에 이미 엉뚱한 곳을 뒤지기 시작한 뒤였습니다.
그러니 자리표시자가 필요한 작업에는 버릴 데이터베이스를 하나 두고 끝나면 지우십시오. 사슬을 잇는 것이 자리표시자의 목적이라 기록 자체는 피할 수 없습니다. 피할 수 있는 것은 재사용뿐입니다.