Community코딩 & 개발github.com

test-suite-speedup

느린 테스트 스위트나 CI 테스트 작업을 더 빠르게 만들고 그 효과를 입증합니다 — 여러 실행에 걸쳐 CI에서 시간이 소모되는 부분을 순위화하고, 파일별 재작성보다 구조적 수정(공유 설정, 병렬 파일, 매트릭스 레그)을 선호하며, 병렬 안전성을 검증하고, 측정된 CI 전/후 비교를 통해서만 변경을 수용합니다.

test-suite-speedup란 무엇인가요?

이 스킬은 개발자가 느린 테스트 스위트 또는 CI 테스트 작업을 체계적으로 최적화할 수 있도록 설계되었습니다. 핵심 원칙은 로컬 타이밍이 아닌 동일한 CI 경로에서 측정된 CI 전/후 비교를 유일한 인수 기준으로 삼는 것입니다. 이 스킬은 다음을 포함한 구조적 수정을 우선시합니다: 반복되는 무거운 설정을 세션 픽스처로 공유, CI 작업을 병렬 매트릭스 레그로 분할, 독립적인 테스트 파일을 병렬로 실행. 파일별 재작성은 마지막 수단으로만 권장됩니다. 지원 플랫폼은 pytest와 vitest/JavaScript를 포함한 주요 테스트 프레임워크를 포함하며, 다양한 CI 환경(GitHub Actions 등)에서 작동합니다. 프로세스는 7단계로 구성됩니다: 동일한 CI 경로에서 기준 수집, 예상 이득 추정, 전체 스위트에서 파일 순위화 및 분류, 구조적 수정 선택, 병렬 안전성 증명 및 러너 변경 구현, CI에서 검증(로컬 중복 실행 방지), 전/후 비교 보고서 작성. 각 단계에는 상세한 측정 지침, 함정 설명, 검증 조건이 포함되어 있어 사용자가 효과적이고 반복 가능한 최적화를 수행할 수 있습니다.

지원 대상~Claude Code✓Codex CLI~Cursor
npx skills add https://github.com/devseunggwan/praxis/tree/HEAD/skills/test-suite-speedup

Installed? Explore more 코딩 & 개발 skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

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

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

문서

test-suite-speedup은(는) 무엇을 하나요?

개요

느린 스위트는 두 가지 잘못된 접근을 유혹합니다: 가장 느려 보이는 파일을 수동으로 다시 작성하거나, 로컬 스톱워치 측정값을 성과로 보고하는 것입니다. 둘 다 본질을 놓칩니다. 시간은 일반적으로 몇 가지 구조적인 위치 — 반복되는 무거운 설정, 병렬로 실행될 수 있지만 직렬로 실행되는 파일들, 하나의 CI 작업이 두 작업을 수행하는 경우 — 에 있으며, 다른 작업이 실행되는 동안 측정된 로컬 타이밍은 노이즈에 불과합니다. 이 스킬은 먼저 CI에서 비용을 순위화하고, 가장 많은 파일을 움직일 수 있는 지렛대를 선택하며, CI 전/후 비교 테이블을 승인 테스트로 취급합니다.

핵심 원칙: 산출물은 동일한 CI 경로에서 측정된 CI 비교이지, 로컬 타이밍이나 녹색 체크 표시가 아닙니다.

참조 맵 — 단계가 실행될 때만 로드:

파일내용
references/ci-measurement.md여러 실행에 걸친 순위화 단계, pytest -v 로그에서 얻은 파일별 순위, 러너 유형 노이즈, 전/후 보고서, 지속 시간-샤드 갱신
references/pytest-slow-test-causes.md느린 pytest 파일을 단계 및 시그니처별로 분류; 이스케이프된 sleep; 도달할 수 없는 제품 코드 대기; 스위트 전체에 걸친 은폐 패치(클라우드 클라이언트, 라이브러리 유휴 sleep)와 가드 테스트; 작업 스트림의 단일 프로세스 프로파일링; create_all 런타임 범위 좁히기
references/pytest-shared-setup.md호출자로부터 핫 팩토리의 공유 가능한 부분 크기 측정; 비용이 많이 드는 pytest 객체를 안전하게 공유: 전후 증명, 최소 변경 배선, 변경 선택 상호 작용
references/vitest-isolation.mdJS/vitest 스위트: 대기 vs 계산 파일 분류, CI 전용 느린 파일, import 중심 비용, 모듈별 import 프로파일링, 배럴 라이브러리 사전 번들링, 격리 vs 공유 풀, 누수 프로브, 셔플 검사
references/parallel-runner.md병렬 안전성 프로브, 직렬 파티션, 러너 구현 규칙, CI 매트릭스 분할
references/ci-redundant-runs.md발생하지 않아야 할 실행: 트리별 백머지/푸시 기록 분류, 동일한 트리 결과 재사용, 실제 최신 실행으로 대체(Actions API, 메시지 파싱 아님), 작업별 동시성, 코드로 if: 게이트 테스트, 커밋 텍스트에 건너뛰기 마커 유지 금지
scripts/ci_leg_series.sh특정 날짜 이후 모든 푸시 실행의 레그별 테스트 단계 초(TSV) — 단계 변경 위치를 파악하기 위한 시리즈; 백그라운드에서 실행
scripts/ci_durations_diff.py각 단계 변경 전후 실행 쌍에 대해: 수집된 카운트, pytest 요약, 새롭거나 증가한 --durations 항목
scripts/boto_call_counter.py테스트당 실제 botocore 호출을 계산하는 pytest 플러그인 — cProfile이 botocore 아래에서 SSL/소켓 대기를 보여줄 때 실행

사용 시기

  • 테스트 스위트 또는 CI 워크플로의 테스트 작업이 너무 느려서 사용자가 더 빠르게 만들고자 할 때.
  • 사용자가 이전 속도 향상 후 "또 무엇이 느린가?"라고 물을 때.
  • 타이밍 목표 없이 복사/중복 제거 정리를 위한 것이 아님 — 재사용을 위한 테스트 구조 변경은 속도 향상이 아닙니다(test-suite-consolidation 참조).

프로세스

1단계: 동일한 CI 경로에서 기준 수집

기준은 로컬 실행이 아닌 CI에서 가져옵니다. 기준은 후속 실행과 동일한 CI 경로를 실행해야 합니다: PR 베이스가 경로를 선택할 때(예: prod는 매트릭스 레그로 분할된 전체 스위트를 실행하고 dev는 변경 선택과 함께 하나의 레그를 실행), 동일한 베이스에 대한 PR의 최신 성공적인 실행을 가져오고, 시간을 비교하기 전에 통과/건너뜀/선택 해제된 카운트가 일치하는지 확인합니다. 해당 실행의 단계별 타이밍을 가져오고, 러너의 섹션 배너로 하나의 큰 단계를 분할하며, 워크플로 파일의 주석을 읽습니다 — 유지 관리자가 이미 의도된 수정 사항을 적어 두었을 때가 많습니다. 그런 다음 하나의 실행이 아닌 여러 실행에 걸쳐 단계를 순위화하고, 러너의 실제 vCPU 수를 구성된 작업자 수와 비교합니다. 명령어 → references/ci-measurement.md.

무엇이든 제안하기 전에, 기존 속도 향상 이슈(닫힌 이슈 및 실험용 PR 포함)를 검색합니다 (gh pr list --state all --search "xdist", --search "test_durations"): 측정되고 거부된 대안과 거부 조건이 기록되어 있습니다 — 설정 비용이 지배적일 때 반박된 지렛대는 CPU 바운드 테스트가 지배하게 되면 효과를 볼 수 있습니다.

완료 조건: 일치하는 CI 경로의 기준 실행 ID가 명명되고, 해당 테스트 카운트가 기록되었으며, 몇 초가 넘는 모든 단계에 대해 여러 실행에 걸친 중앙값과 90번째 백분위수가 있습니다.

2단계: 지렛대를 선택하기 전에 예상 이득 크기 측정

이득 ≈ (배치가 차지하는 스위트 시간 비율) × (해당 하위 집합에서 측정된 파일당 절감). 스위트의 2%인 배치는 파일당 -60%에서도 약 1-2%를 제공합니다 — CI 노이즈 미만이며, 사용자는 "거의 움직이지 않았다"고 읽을 것입니다. 수백 개의 파일을 한 번에 움직이는 지렛대(공유 테스트 헬퍼, 풀 분류기, 러너 유형)를 선호하고 파일별 마이그레이션은 긴 꼬리로 제시합니다. 예상 이득을 미리 말합니다.

완료 조건: 각 후보 지렛대에 대해 입력값과 함께 작성된 추정치가 있고, CI 노이즈 미만의 추정치는 그렇게 레이블이 지정됩니다.

3단계: 파일 순위화 및 각 상위 파일 분류

pytest -v를 기록하는 전체 스위트 CI 실행에서 전체 스위트에 걸쳐 파일 순위를 매깁니다(변경 선택 실행은 모든 테스트를 다루지 않습니다). 먼저 알려진 답이 있는 실행에서 순위를 검증합니다 — 수정 전 실행은 수정한 파일을 먼저 순위화해야 합니다. 그런 다음 수정을 제안하기 전에 각 상위 파일을 단계(setup / call / teardown)와 형태별로 분류합니다:

파일별 로컬 타이밍은 형태 분석용입니다: 각 파일을 백그라운드에서 실행하고, 왜 느린지 살펴봅니다(케이스당 프로세스 생성, 실제 sleep — 테스트의 입력 문자열 내부의 sleep은 지연이 아닙니다), 다른 스위트가 실행되는 동안 측정된 숫자는 절대 보고하지 않습니다. 각 파일에 대해 CPU(user+sys)를 벽시계 시간 옆에 기록합니다. 벽시계 시간에 비해 CPU를 적게 사용하는 파일은 실제 시간(제한기, 백오프, 폴링)을 기다리는 중이며, 수정은 테스트 측 가짜 시간입니다; 더 많은 작업자나 import 감소는 도움이 되지 않습니다. CI에서는 느리지만 로컬에서 CI의 작업자 수와 환경에서 빠른 파일은 CI의 OS 및 런타임 메이저(CI의 vCPU로 제한된 node:<major> 컨테이너)에서 재현이 필요하며, 추측이 아닙니다; 해당 파일 아래 CI 로그에 이미 인쇄된 느린 테스트 라인을 먼저 읽습니다 → references/vitest-isolation.md. pytest의 경우, 파일별 CI/로컬 비율을 스위트의 일반적인 비율과 비교합니다. 몇 배 이상 높은 파일은 CI 전용 비용이 있지만, 그 원인은 PR 자체의 CI 실행이 해당 파일을 움직일 때까지 가설입니다: Linux 컨테이너 A/B(예: sqlite synchronous=OFF가 스케줄러 시뮬레이션을 약 35% 감소)가 GitHub 러너로 이어지지 않아 동일한 테스트가 --durations를 유지했습니다. 첫 번째 PR 실행에서 측정 가능한 목표와 함께 그러한 지렛대를 제공합니다 → references/pytest-slow-test-causes.md.

느린 스위트의 원인은 일반적으로 조합입니다: 직렬 작업자, 대기 파일, 무거운 파일별 import. 하나의 테이블에서 지렛대 순위를 매깁니다: 원인 · 증거(측정 vs 추정) · CI 초 · 수정 · 예상 절감.

--durations 이름에서 추측된 원인("아마도 DAG를 다시 파싱할 것")은 2-3개의 느린 테스트에 대한 cProfile cumtime 실행이 그것을 보여줄 때까지 가설입니다; 원인이 계획이나 이슈에 들어가기 전에 프로파일링하고, 프로파일이 그렇지 않다고 말할 때 보고서에서 철회합니다.

완료 조건: 각 최상위 파일은 분류 테이블의 원인이 있거나, 명시적으로 "확인 필요"로 표시됩니다.

4단계: 구조적 수정 선택, 다음 순서로

  1. 핫 파일 내부의 반복되는 비용이 많이 드는 설정 제거(호출당 동일한 무거운 객체 — DAG 백, 앱, 스키마 — 를 재구축하는 테스트). 캐시를 발명하기 전에 저장소의 기존 세션 픽스처를 재사용하고, 공유가 안전함을 먼저 증명한 다음, 베이스와 브랜치 복사본의 파일이 동일한 프로덕션 돌연변이에서 실패함을 증명합니다. 모듈/세션 캐시는 하나의 pytest 프로세스 내에서만 중복을 제거합니다: 샤딩된 CI에서는, 각 소비자가 실행되는 레그를 절감 계산 전에 확인합니다. 별도의 테스트가 매개변수화된 하나의 케이스의 비용이 많이 드는 결과만 다시 읽는 경우, 캐싱 대신 해당 케이스로 어설션을 접고(샤드 독립적), 이동된 하나의 어설션을 깨뜨려 여전히 실행됨을 증명합니다 → references/pytest-shared-setup.md.
  2. CI 진입점 내에서 독립적인 테스트 파일을 병렬로 실행합니다.
  3. CI 작업을 매트릭스 레그로 분할하여 절반이 나란히 실행되도록 합니다.
  4. 그런 다음에만 가장 느린 파일의 케이스별 생성을 다시 작성합니다 — 넓은 변경, 적당한 이득. 제안하되 기본값으로 하지 마십시오.

비용의 대부분이 발생할 필요가 없는 전체 실행(봇 백머지, 다른 브랜치에서 이미 테스트된 동일한 트리, 곧 대체될 푸시)인 경우, 해당 지렛대가 위의 모든 것보다 우선합니다: 트리별로 실행 기록을 분류하고 전체 실행을 건너뛰거나 재사용합니다 → references/ci-redundant-runs.md.

완료 조건: 선택된 지렛대가 2단계 추정치와 함께 명명되고, 건너뛴 모든 상위 순서 지렛대에 대해 한 줄 이유가 있습니다.

5단계: 병렬 안전성 증명 및 러너 변경 구현

모든 파일을 하나의 임시 상태 디렉토리 아래에서 병렬로 실행하고, 각각 자체 출력 및 종료 코드 파일로, 실패를 나열하고, 각 실패를 단독으로 다시 실행하여 동시성 버그와 환경을 구분합니다. 작업 트리를 편집하는 파일을 이미 가지고 있는 마커를 키로 하는 직렬 그룹으로 분할합니다. 그런 다음 러너와, 선택된 경우 CI 매트릭스 분할을 구현합니다. 절차 및 규칙 → references/parallel-runner.md.

완료 조건: 모든 병렬 프로브 실패가 분류되고(동시성 / 환경 / 사전 존재), 직렬 그룹이 수동 목록이 아닌 grep에서 파생되었으며, 작업 이름 변경 전에 필요한 상태 검사가 확인되었습니다.

6단계: 로컬 실행에 앉아 있지 말고 CI에서 검증

변경된 테스트 코드는 제품 코드와 마찬가지로 저장소 자체의 코딩 지침을 따르도록 유지합니다 — 통과 래퍼 금지, 중복 추출, 새로운 헬퍼에 대한 돌연변이 증명. 수정된 파일과 린트만 로컬에서 실행하고, 푸시한 다음, 턴을 잡고 있지 말고 백그라운드에서 PR의 검사를 지켜봅니다. 확인 노트를 작성하기 전에 PR의 리뷰 스레드를 읽습니다: 봇 리뷰어는 푸시 후 몇 분 안에 게시합니다.

로컬 환경에서만 테스트가 실패하는 경우(선택적 종속성 venv, 로컬 가짜 서비스), 변경 사항을 건드리기 전에 수정되지 않은 베이스에서 임시 분리 작업 트리에서 해당 테스트 하나를 실행합니다: 동일한 실패가 거기에서도 발생하면 환경이지 회귀가 아닙니다. 커밋의 Not-tested: 트레일러와 PR 본문에 이름을 지정하고, 해당 종속성을 설치하는 CI 작업(원시 로그: 요약 줄 + grep -c FAILED = 0)에서 종료하고, 로컬 실행에서는 종료하지 않습니다.

로컬 전체 스위트 베이스-대-브랜치 비교는 재현된 범위까지만 "새로운 실패 없음"을 지원합니다: 브랜치 전용 실패가 하나의 환경 오류를 공유하는 경우, "N개의 브랜치 전용 실패, 모두 <오류>; <파일>이 베이스에서 재현됨"이라고 말하고 CI 실행이 나머지를 종료하도록 합니다 — "0개의 새로운 실패"는 과장이며 도달한 모든 곳에서 수정되어야 합니다.

스위트 전체 스텁이 병합 안전하다고 말하기 전에, 결과를 변경할 수 있는 방법을 열거하고 각각에 대한 프로브를 포함합니다: (1) 그것을 우회하는 모듈 수준 바인딩(git grep -nE "from <mod> import <fn>" — import 시 바인딩된 이름은 실제 함수를 유지합니다); (2) 스텁의 예외를 잡지 못하는 프로덕션 호출자와, 어떤 테스트가 이를 패치되지 않은 상태로 실행하는지(이전 실행의 테스트별 호출 카운트가 보여줍니다); (3) 실제 경로를 어설션하는 테스트 — 세션 스텁 내에 중첩되어 스스로 패치해야 합니다; (4) 패치된 이름을 이동시키는 진행 중인 라이브러리 업그레이드(pytest-slow-test-causes.md의 버전 허용 규칙 참조). 각각을 확인된 것으로 보고하거나, 확인할 수 있는 내용과 함께 추론으로 보고합니다.

봇 리뷰어는 짧은 폴링보다 더 오래 실행될 수 있습니다: Codex 커넥터는 리뷰하는 동안 PR을 👀로 표시하고, 발견 사항 없이 완료되면 👍로 바꾸며, 때로는 20분 이상 걸립니다. 대기를 제한한 다음, 폴링을 연장하는 대신 병합 브리핑 전에 한 번 반응과 스레드를 다시 읽습니다. 현재 👍가 최신 푸시에 속하는지 확인하려면, 푸시 시간에 대해 타임스탬프를 읽습니다(gh api graphql → pullRequest{reactions(first:20){nodes{content createdAt user{login}}}}) — 푸시보다 오래된 👍는 이전 라운드의 판결입니다.

pull_request 실행은 해당 시점의 베이스 팁으로의 헤드 병합을 테스트하며, PR 자체의 베이스 커밋이 아닙니다: 작업 로그에서 베이스 sha를 읽고(HEAD is now at <m> Merge <head> into <base>) 해당 베이스 커밋의 푸시 실행과 비교합니다.

파일별 메트릭은 기준과 PR 실행 사이에 스위트가 성장했을 때의 사후 측정입니다: 전체 레그 초는 다른 커밋의 테스트를 혼합합니다(먼저 collected N items 카운트를 비교). 두 실행에서 수정된 파일의 CI 초를 결과로 보고하고 레그 시간을 컨텍스트로 보고합니다. 베이스를 PR에 병합한 후, 이전 대역이 아닌 동일한 기간의 다른 PR 실행에서 비교 대역을 재구축합니다: 베이스 자체의 스위트는 계속 성장하므로, 이전 대역은 실제 이득을 회귀로 보이게 만들 수 있습니다. 대역 자체가 이동했을 때 보고서에 명시합니다.

완료 조건: 기준과 동일한 경로의 브랜치 CI 실행이 완료되었고, 모든 열린 리뷰 발견 사항에 답변되었습니다.

7단계: 전/후 보고

두 실행 ID가 포함된 테이블: 작업 벽시계 시간(현재 = 가장 느린 레그), 이전에는 느렸던 단계, 변경되지 않은 절반, 레그별 테스트 요약 줄. 여러 PR에 걸친 이득의 경우, 하나의 테이블: 기준 → 각 병합 단계 → 현재, 각 PR이 기여한 비율 포함. PR이 실행하지 않은 CI 경로를 미검증으로 명명하고, 각각 하나의 실행(러너 분산 측정 불가)이라고 밝히며, 이전 단계가 새로운 작업자를 내부적으로 추가하거나 테스트를 이동했는지 메모합니다(레그당 초는 배타적이지 않음). 다음 단계(2차 PR 또는 별도 이슈)로 남은 상위 느린 파일을 기록합니다.

완료 조건: 전/후 테이블이 작성되었고, 각 행에 실행 ID가 있으며, 남은 작업이 기록되었습니다.

참조

Individual skills in this repo

This repo contains 2 individual skills — each has its own dedicated page.

관련 스킬