Community라이팅 & 에디팅github.com

Flaky Test Detective

불안정한(비결정적) 테스트의 근본 원인을 찾아내고 결정적인 테스트로 다시 작성하는 Claude Agent Skill입니다. pytest · Jest · RSpec · Go · JUnit을 지원합니다.

Flaky Test Detective란 무엇인가요?

Flaky Test Detective는 불안정한(flaky) 테스트의 근본 원인을 분석하고 결정론적(hermetic) 테스트로 재작성하는 AI 에이전트 스킬입니다. 이 스킬은 테스트가 동일한 코드에서 통과와 실패를 반복하는 '플래키(flaky)' 문제를 해결하는 데 특화되어 있습니다. 플래키 테스트는 시간, 랜덤 시드, 테스트 실행 순서, 공유 데이터베이스 행, 실제 네트워크 호출, 맵의 반복 순서 등 테스트가 제어하지 못하는 숨겨진 입력에 의존하기 때문에 발생합니다. 이 스킬은 이러한 비결정적 입력을 식별하고, 그 이유를 설명하며, 제어된 입력을 사용하도록 테스트를 재작성합니다. 주요 기능으로는 플래키 테스트 탐지 및 재현, 근본 원인 분류(12가지 고빈도 원인 카탈로그 제공), 결정론적 재작성 코드 제시, 테스트 격리 및 시드 고정 등이 포함됩니다. 지원하는 테스트 프레임워크는 pytest, Jest/Vitest, RSpec/Minitest, Go testing, JUnit, PHPUnit 등이며, CI 환경에서의 병렬 실행, 무작위 순서, 시간대 변화 등 다양한 조건에서 발생하는 플래키 테스트를 체계적으로 해결할 수 있습니다. 스킬은 SKILL.md 문서를 기반으로 하며, 참조 파일로 root-causes.md, frameworks.md, detection.md, patterns.md를 제공하여 각 언어와 프레임워크에 특화된 깊이 있는 정보를 지원합니다.

지원 대상✓Claude Code~Codex CLI~Cursor
npx skills add windchillscalanthes-ship-it/flaky-test-detective

Installed? Explore more 라이팅 & 에디팅 skills: steipete/notion, langchain-ai/langchain, bytedance/podcast-generation · View all 6 →

즐겨 사용하는 AI에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

Flaky Test Detective은(는) 무엇을 하나요?

같은 코드에서 통과했다 실패했다 하는 테스트를 잡아내어 결정론적(deterministic)으로 다시 작성하세요. 빌드 실패를 무시하는 팀 문화가 생기기 전에.

플래키(flaky) 테스트는 테스트가 없는 것보다 나쁩니다. 한 번 실패하면 누군가 CI를 다시 실행하고, 통과하면 모두가 '빨간색이 곧 고장'을 의미하지 않는다는 것을 배웁니다. 곧 '그냥 다시 실행해'라는 말 뒤에 실제 회귀(regression)가 숨어들게 됩니다. 원인은 거의 항상 테스트 대상 코드가 아니라, 테스트가 통제하지 못하는 무언가에 대한 숨겨진 의존성입니다: 시계, 랜덤 시드, 테스트 실행 순서, 데이터베이스의 공유 행, 실제 네트워크 호출, 맵의 반복 순서. 이 스킬은 그 숨겨진 입력을 찾아내고, 왜 테스트를 비결정적으로 만드는지 설명하며, 테스트를 다시 작성하여 그 입력을 제어합니다.

사용 시점

  • 테스트가 간헐적으로 실패합니다 — 코드 변경 없이 한 번은 빨간색, 다음 번은 초록색.
  • 테스트가 로컬에서는 초록색이지만 CI에서는 빨간색(또는 그 반대)이거나, 병렬 실행이나 특정 순서에서만 실패합니다.
  • 누군가 "그냥 다시 실행해", "불안정해", "재시도하면 통과해" 라고 말합니다.
  • 테스트를 작성하거나 리뷰 중이며, 커밋되기 전에 비결정성을 잡아내고 싶습니다.
  • 테스트 스위트를 강화하고 싶습니다 — 숨겨진 불안정성이 발현되기 전에 찾아내려는 경우.

테스트가 매번 실패한다면, 그것은 플래키가 아닙니다 — 실제 실패 또는 진짜 버그입니다. 코드를 고치세요. 테스트를 안정화하지 마세요. 그리고 플래키 문제를 재시도나 sleep을 추가하여 '고치는' 일은 절대 하지 마세요 — 그것은 신호를 묻어버립니다. 목표는 결정론적인(deterministic) 테스트이지, 대개 초록색인 테스트가 아닙니다.

워크플로우

  1. 컨텍스트를 확립하세요. 언어 / 프레임워크 / 러너 (pytest, Jest/Vitest, RSpec/Minitest, Go testing, JUnit, PHPUnit, …)와 어디서 불안정한지 — 로컬, CI, 병렬에서만, 특정 순서에서만, 특정 시간대에만 — 식별하세요. 어디서 그리고 언제 불안정한지가 원인에 대한 가장 큰 단서입니다.

  2. 비결정성을 재현하세요. 요청해도 실패하지 않는 플래키 테스트는 고쳐진 것이 아니라 잠잠해진 것뿐입니다. 강제로 실패시키세요: 여러 번 실행하고, 무작위 순서로 실행하고, 격리 vs. 전체 스위트와 함께 실행하고, 하나의 워커에 고정시키거나, 시계/시간대를 이동시키세요. reference/detection.md에 프레임워크별 정확한 명령어가 있습니다.

  3. 근본 원인을 아래 카탈로그에 따라 분류하고 심각도를 할당하세요:

    • 🔴 Flaky(불안정) — 구체적인 비결정적 입력이 단언(assertion)에 도달합니다; 결국 실패할 것입니다. 반드시 결정론적으로 만들어야 합니다.
    • 🟡 Latent(잠재) — 원칙적으로는 비결정적이지만 현재는 가려져 있습니다 (우연히 안정적인 시드, 우연히 유지되는 순서, 충분히 빠른 CI). 어떤 조건이 깨뜨릴지 이름을 붙이세요.
    • 🟢 Deterministic(결정론적) — 테스트가 자신의 입력을 제어합니다. 변경 불필요.
  4. 근본 원인을 고치세요 — 덮지 마세요. 각 🔴/🟡에 대해 다음을 제시하세요:

    • 비결정적인 이유 — 정확히 어떤 통제되지 않은 입력이 단언에 도달하는지,
    • 결정론적 재작성을 실행 가능한 코드로 (프레임워크 관용구에 맞게),
    • 결과 (예: "순서 의존적 → 격리됨" 또는 "20번에 1번꼴 불안정 → 1000번 연속 초록").
  5. 보고서를 마지막의 출력 템플릿을 사용하여 작성하세요. 🔴부터 먼저 나열하세요.

언어 및 원인별 깊이 있는 내용을 보려면 현재 테스트에 관련된 참조 파일을 불러오세요:

  • 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 수정.>

항상 결정론적 재작성과 재현 명령어를 제시하세요 — "이것은 불안정합니다"라고만 말하고 제어된 입력 수정과 증명 방법을 제공하지 않는 리뷰는 어려운 부분을 다시 돌려주는 것입니다. 통제되지 않은 입력의 이름을 붙이는 것이 이 스킬이 제공하는 전문성입니다.

관련 스킬