Flaky Test Detective은(는) 무엇을 하나요?
같은 코드에서 통과했다 실패했다 하는 테스트를 잡아내어 결정론적(deterministic)으로 다시 작성하세요. 빌드 실패를 무시하는 팀 문화가 생기기 전에.
플래키(flaky) 테스트는 테스트가 없는 것보다 나쁩니다. 한 번 실패하면 누군가 CI를 다시 실행하고, 통과하면 모두가 '빨간색이 곧 고장'을 의미하지 않는다는 것을 배웁니다. 곧 '그냥 다시 실행해'라는 말 뒤에 실제 회귀(regression)가 숨어들게 됩니다. 원인은 거의 항상 테스트 대상 코드가 아니라, 테스트가 통제하지 못하는 무언가에 대한 숨겨진 의존성입니다: 시계, 랜덤 시드, 테스트 실행 순서, 데이터베이스의 공유 행, 실제 네트워크 호출, 맵의 반복 순서. 이 스킬은 그 숨겨진 입력을 찾아내고, 왜 테스트를 비결정적으로 만드는지 설명하며, 테스트를 다시 작성하여 그 입력을 제어합니다.
사용 시점
- 테스트가 간헐적으로 실패합니다 — 코드 변경 없이 한 번은 빨간색, 다음 번은 초록색.
- 테스트가 로컬에서는 초록색이지만 CI에서는 빨간색(또는 그 반대)이거나, 병렬 실행이나 특정 순서에서만 실패합니다.
- 누군가 "그냥 다시 실행해", "불안정해", "재시도하면 통과해" 라고 말합니다.
- 테스트를 작성하거나 리뷰 중이며, 커밋되기 전에 비결정성을 잡아내고 싶습니다.
- 테스트 스위트를 강화하고 싶습니다 — 숨겨진 불안정성이 발현되기 전에 찾아내려는 경우.
테스트가 매번 실패한다면, 그것은 플래키가 아닙니다 — 실제 실패 또는 진짜 버그입니다. 코드를 고치세요. 테스트를 안정화하지 마세요. 그리고 플래키 문제를 재시도나 sleep을 추가하여 '고치는' 일은 절대 하지 마세요 — 그것은 신호를 묻어버립니다. 목표는 결정론적인(deterministic) 테스트이지, 대개 초록색인 테스트가 아닙니다.
워크플로우
-
컨텍스트를 확립하세요. 언어 / 프레임워크 / 러너 (pytest, Jest/Vitest, RSpec/Minitest, Go
testing, JUnit, PHPUnit, …)와 어디서 불안정한지 — 로컬, CI, 병렬에서만, 특정 순서에서만, 특정 시간대에만 — 식별하세요. 어디서 그리고 언제 불안정한지가 원인에 대한 가장 큰 단서입니다. -
비결정성을 재현하세요. 요청해도 실패하지 않는 플래키 테스트는 고쳐진 것이 아니라 잠잠해진 것뿐입니다. 강제로 실패시키세요: 여러 번 실행하고, 무작위 순서로 실행하고, 격리 vs. 전체 스위트와 함께 실행하고, 하나의 워커에 고정시키거나, 시계/시간대를 이동시키세요.
reference/detection.md에 프레임워크별 정확한 명령어가 있습니다. -
근본 원인을 아래 카탈로그에 따라 분류하고 심각도를 할당하세요:
- 🔴 Flaky(불안정) — 구체적인 비결정적 입력이 단언(assertion)에 도달합니다; 결국 실패할 것입니다. 반드시 결정론적으로 만들어야 합니다.
- 🟡 Latent(잠재) — 원칙적으로는 비결정적이지만 현재는 가려져 있습니다 (우연히 안정적인 시드, 우연히 유지되는 순서, 충분히 빠른 CI). 어떤 조건이 깨뜨릴지 이름을 붙이세요.
- 🟢 Deterministic(결정론적) — 테스트가 자신의 입력을 제어합니다. 변경 불필요.
-
근본 원인을 고치세요 — 덮지 마세요. 각 🔴/🟡에 대해 다음을 제시하세요:
- 비결정적인 이유 — 정확히 어떤 통제되지 않은 입력이 단언에 도달하는지,
- 결정론적 재작성을 실행 가능한 코드로 (프레임워크 관용구에 맞게),
- 결과 (예: "순서 의존적 → 격리됨" 또는 "20번에 1번꼴 불안정 → 1000번 연속 초록").
-
보고서를 마지막의 출력 템플릿을 사용하여 작성하세요. 🔴부터 먼저 나열하세요.
언어 및 원인별 깊이 있는 내용을 보려면 현재 테스트에 관련된 참조 파일을 불러오세요:
reference/root-causes.md— 8가지 근본 원인 패밀리에 대한 심층 분석: 각각이 어떻게 테스트를 불안정하게 만드는지, 탐지 방법, 표준 수정.reference/frameworks.md— 프레임워크별 메커니즘: pytest, Jest/Vitest, RSpec/Minitest, Go, JUnit, PHPUnit에서 시계 제어, RNG 시드 고정, 테스트 순서 무작위화/고정, 상태 격리, 네트워크 모킹.reference/detection.md— 요청 시 불안정성을 재현하는 방법: 재실행 루프, 순서 무작위화, 격리/이분법, 병렬 실행, CI 신호 읽기.reference/patterns.md— 결정론적 테스트 원칙: 격리성(hermeticity), 시계/RNG/네트워크/순서 제어, 테스트별 격리, 탐지→수정→격리 생명주기.
현재 작업 중인 프레임워크에 대한 참조를 읽고 추측하지 마세요 — 메커니즘이 다릅니다 (pytest vs. Jest vs. Go에서 시간 고정, 시드, 순서 제어 방식이 완전히 다릅니다).
근본 원인 카탈로그 (고빈도 원인)
"통제되지 않은 입력" = 테스트 결과가 의존하지만 테스트가 고정하지 않은 모든 것. 불안정성은 항상 통제되지 않은 입력이 단언에 도달하는 것입니다.
| # | 근본 원인 | 비결정적인 이유 | 결정론적 수정 |
|---|---|---|---|
| 1 | 테스트 순서 의존성 / 공유 가변 상태 | 테스트가 다른 테스트가 남긴 상태에 의존 (전역 변수, 클래스 속성, 모듈 싱글턴, 롤백되지 않은 행). 한 순서에서는 통과, 다른 순서에서는 실패. | 격리: 테스트마다 새 fixture, 해제(teardown)에서 전역 변수 재설정, 각 테스트를 트랜잭션 롤백으로 감싸기. CI에서 순서를 무작위화하여 잡아내세요. |
| 2 | 시간 및 시간대 | 테스트나 코드 내의 now()/Date.now()/today() — 자정, 월/년 경계, 일광 절약 시간제, 또는 CI의 시간대에서 실패. | 시계를 고정하세요 (freezegun/time-machine, Jest fake timers, timecop, Go 주입 가능 시계); TZ 고정; 실제 벽시계에 대해 단언하지 마세요. |
| 3 | 시드 고정되지 않은 무작위성 | random(), uuid4(), faker, 셔플된 데이터 — 드문 값이 N번 실행 중 한 번 단언을 깨뜨림. | RNG 시드를 결정론적으로 테스트별로 고정; ID/가짜 객체 주입; 특정 랜덤 값 대신 속성에 대해 단언. |
| 4 | 비동기 경쟁 조건 / sleep 기반 대기 | sleep(100) 후 단언 — CI가 느려지면 작동 실패; 처리되지 않은 프로미스; 실행 전에 단언된 콜백; 이벤트 순서. | 조건을 기다리세요. 지속 시간이 아닌 (waitFor/실제 신호 await); 모든 프로미스 await; 가짜 타이머를 사용하여 스케줄링 제어. |
| 5 | 실제 네트워크 / 외부 서비스 | 실제 HTTP/API/DNS 호출 — 지연 시간, 속도 제한, 다운타임, 또는 변경된 데이터로 실패. | 경계에서 모킹 (responses/respx, nock/msw, VCR/WireMock); 단위 테스트에서 실제 네트워크에 접근하지 마세요. |
| 6 | 순서 없는 컬렉션 단언 | set/dict/map, ORDER BY 없는 DB 쿼리, os.listdir, 또는 동시 실행 결과에서 특정 순서를 단언. | 비교 전 정렬, 집합 동등성/포함 단언 사용, 또는 명시적 ORDER BY 추가. 우연한 순서에 의존하지 마세요. |
| 7 | 샌 글로벌 상태 / 리소스 누출 | 환경 변수, 싱글턴, 모듈 캐시, CWD, 열린 연결/파일/포트가 변경되고 복원되지 않음 — 이후 테스트를 오염시킴. | 각 테스트 전후로 저장/복원 (fixtures/beforeEach/t.Cleanup); 모든 리소스 닫기; 자동 실행 취소와 함께 monkeypatch. |
| 8 | 너무 빡빡한 타임아웃 / 성능 가정 | 빠른 노트북에서는 유효하지만 부하가 많은 CI 실행기에서는 유지되지 않는 하드코딩된 타임아웃 또는 assert elapsed < X. | 정확성 테스트에서 벽시계 성능 단언 제거; 타임아웃이 필요하면 관대하고 조건 기반으로 만들고, 빡빡한 상수로 두지 마세요. |
| 9 | 부동소수점 정확한 동등성 | 연산 후 assert result == 0.3 — 반올림으로 인해 때때로 불일치 발생. | 허용 오차와 함께 단언 (pytest.approx, toBeCloseTo, 엡실론 비교). |
| 10 | 로케일 / 환경 가정 | 시스템/CI에 따라 다른 숫자/날짜/문자열 형식, 대소문자 변환, 경로 구분자, 또는 파일 시스템 대소문자 구분. | 테스트에서 로케일/인코딩 고정; 로케일 독립적인 형식 사용; OS별 경로 단언하지 않음. |
| 11 | 공유 fixture / DB 격리되지 않음 | 테스트가 데이터베이스, 캐시, 또는 임시 디렉토리를 공유하고 병렬 실행에서 서로 간섭. | 테스트별 트랜잭션/롤백, 워커별 고유 스키마/네임스페이스, 테스트별 tmp_path; 공유 쓰기 가능 fixture 없음. |
| 12 | 테스트 대상 코드의 숨겨진 동시성 | 단언에 대해 예측 불가능하게 완료되거나 완료되지 않는 백그라운드 스레드, 타이머, 또는 비동기 작업. | 경계를 await 가능/주입 가능하게 만들기; 결정론적으로 큐를 비우기/플러시; 제어 가능한 실행기/시계 주입. |
🟡 계층은 불안정성이 발현되기 전에 숨어 있는 곳입니다: 우연히 마지막에 실행되는 순서 의존적 테스트, 현재 안정적인 시드, 현재 충분히 긴 sleep. 🔴로 바뀌는 조건을 이름 붙이세요 — 그것이 기다리고 있는 수정입니다.
지침 원칙
- 불안정성은 통제되지 않은 입력이지, 나쁜 운이 아닙니다. 모든 플래키 테스트는 고정하지 않은 것에 의존합니다: 시계, 시드, 순서, 네트워크, 공유 행. 그 입력을 찾아서 제어하세요 — 고정, 시드, 격리, 모킹.
- 고치기 전에 재현하세요. 요청해도 실패하게 만들 수 없다면 (재실행 루프, 무작위 순서, 격리), 원인을 찾은 것이 아니라 숨긴 것입니다.
- 테스트는 자신의 상태를 소유해야 합니다. 단독으로, 어떤 순서로든, 병렬로도, 천 번을 통과해야 합니다. 설정(Setup)은 필요한 것을 만들고; 해제(Teardown)는 건드린 것을 복원합니다.
- 네 가지 주요 용의자를 제어하세요. 시계, 무작위성, 네트워크, 순서가 대부분의 불안정성의 원천입니다 — 각각을 기본적으로 결정론적으로 만드세요.
- 조건을 기다리세요, 시간을 기다리지 마세요.
sleep(n)은 느린 실행기에서 질 경주입니다. 실제 신호를 await하거나 넉넉한 상한으로 조건을 폴링하세요. - 재시도로 불안정성을 고치지 마세요. 재시도와 무차별적인
sleep은 눈에 보이는 결함을 조용한 결함으로 바꾸고 스위트의 신뢰성을 파괴합니다. - 격리하세요, 삭제하지 마세요. 플래키 테스트를 즉시 고칠 수 없다면 추적 이슈와 함께 표시(건너뛰기/격리)하여 차단을 막으세요 — 하지만 유지하고, 고장났다는 신호를 유지하세요.
- 적절한 크기를 유지하세요. 진정으로 순서 독립적이고 격리된 테스트는 이것이 전혀 필요하지 않습니다 — 이미 입력을 제어하는 테스트에 결정론적 장식을 덧붙이지 마세요.
출력 템플릿
## 플래키 테스트 리뷰
**스택:** <언어 / 프레임워크 / 러너> · **불안정 발생:** <CI 전용 / 병렬 / 무작위 순서 / 시간 X> · **리뷰한 테스트:** <n개>
### 🔴 <파일::테스트 이름> — FLAKY(불안정)
**근본 원인:** <순서 의존성 / 시간 / 무작위성 / 비동기 경쟁 / 네트워크 / 순서 / 전역 상태 / 타임아웃 / …>
**비결정적인 이유:** <정확한 통제되지 않은 입력과 단언에 도달하는 방식>
**재현:** <요청 시 실패하게 만드는 명령어 — 무작위 순서, 재실행 루프, 격리, 시계 이동>
**결정론적 재작성:**
<실행 가능한, 프레임워크 관용구 코드>
**결과:** <예: "순서 의존적 → 격리됨; 1000회 무작위 순서 실행에서 초록">
### 🟡 <파일::테스트> — LATENT(잠재)
**근본 원인 / 현재 가려진 이유 / 🔴로 바뀌는 조건 / 권장 사항**
### 🟢 <파일::테스트> — DETERMINISTIC(결정론적)
<한 줄: 입력을 제어함 — 변경 불필요>
---
**요약:** <n개 불안정, n개 잠재.> <가장 심각한 통제되지 않은 입력 한 줄.> <결론: 안정적으로 병합 가능, 또는 먼저 테스트 X 수정.>
항상 결정론적 재작성과 재현 명령어를 제시하세요 — "이것은 불안정합니다"라고만 말하고 제어된 입력 수정과 증명 방법을 제공하지 않는 리뷰는 어려운 부분을 다시 돌려주는 것입니다. 통제되지 않은 입력의 이름을 붙이는 것이 이 스킬이 제공하는 전문성입니다.