정의
변경 영향 기반 테스트 선택(affected test selection) 은 이번 변경에 영향받는 테스트만 골라 돌리는 방식이에요. 파일들이 서로를 import로 가져다 쓰는 관계를 그린 import 그래프에서, 바뀐 파일을 출발점으로 거꾸로 따라가 그 파일에 닿는 테스트만 실행해요.
왜 필요한가
한 줄만 고쳐도 수백 개 파일의 테스트를 전부 돌리면, 푸시 직전 훅과 PR CI마다 같은 일이 반복돼요. 작업이 여러 개 병렬로 돌면(여러 작업 폴더, 여러 에이전트) 같은 전체 테스트가 동시에 CPU를 나눠 쓰면서 기다림이 길어져요.
선택 실행은 이 비용을 줄이지만 위험도 있어요. import 그래프가 불완전하면 돌려야 할 테스트를 빼먹어서 깨진 코드가 통과해요. 그래서 "줄이는 규칙"과 "불확실하면 전체로 되돌리는 규칙"을 같이 설계해야 해요.
동작 원리
변경 파일 수집(기준점 대비)
└→ 삭제/이름 변경 또는 전역 설정 변경? ── 예 ──→ 전체 테스트 실행
│ 아니오
└→ import 그래프를 역방향으로 따라 테스트 선택 → 선택된 테스트만 실행
| 요소 | 설명 | 주의점 |
|---|---|---|
| 기준점(base) | "무엇과 비교해 바뀐 것인가" | 추측하면 너무 적게 또는 너무 많이 잡아요 |
| 정적 그래프 | import 구문 기반 | 동적 import(), 문자열로 만든 경로는 놓칠 수 있어요 |
| 공유 모듈 | 많은 파일이 가져다 씀 | 관련 테스트가 사실상 전체에 가까워질 수 있어요 |
| 강제 재실행 | 특정 파일이 바뀌면 무조건 전체 | lockfile, tsconfig, 테스트 setup, 번역 JSON, 공용 자산 |
| 최종 게이트 | 병합·배포 후보의 전체 + E2E | 선택 실행의 누락을 마지막에 덮어요 |
기준점은 상황에서 받아요. PR CI는 플랫폼이 알려 주는 PR의 base 커밋을 쓰고, 푸시 훅은 Git이 훅에 넘겨 주는 "그 브랜치의 이전 원격 SHA"를 써요. 새 브랜치는 어느 브랜치에서 갈라졌는지 Git이 기록하지 않으므로 사람이 TEST_BASE 같은 입력으로 명시하게 해요.
실무 적용
도구가 제공하는 기능을 쓰고 직접 그래프를 만들지 않아요.
# Vitest: 기준 커밋 대비 변경분과 관련된 테스트만
vitest run --changed "$BASE" --maxWorkers=2 --passWithNoTests
# Jest 계열
jest --changedSince="$BASE"
// vitest.config.ts — 이 파일들이 바뀌면 선택 없이 전부 다시 돌린다
export default defineConfig({
test: {
forceRerunTriggers: [
"**/package.json", "**/pnpm-lock.yaml", "**/tsconfig*.json",
"**/vitest.setup.*", "messages/**", "public/**",
],
},
});
- 삭제·이름 변경은 전체로 보내요. 사라진 파일은 그래프에서 출발점이 되지 못해서, 그 파일에 의존하던 테스트를 찾을 수 없어요.
- 더러운 작업 트리나 현재 HEAD가 아닌 ref는 막아요(fail closed). 기준과 실제 내용이 어긋난 채 통과하는 것을 피해요.
- 훅 자체를 회귀 테스트로 고정해요: 푸시 범위, 첫 푸시 기준, 더러운 작업 트리, 없는 base, 삭제 파일 폴백.
- worker 수는 측정해서 정해요. 한 프로세스의 worker를 늘리는 것과 프로세스를 여러 개 동시에 띄우는 것은 달라요. 동시 실행 상황에서 실제로 재 본 뒤 정하고, 다른 환경에 일반화하지 않아요.
트레이드오프
| 얻는 것 | 잃는 것 |
|---|---|
| 푸시·PR 검증 시간과 CPU 감소 | 정적 그래프가 못 보는 경로의 누락 위험 |
| 병렬 작업 간 중복 부하 감소 | 훅·CI에 선택 로직과 폴백 규칙이 늘어요 |
| 작은 변경의 빠른 피드백 | 최종 전체 게이트라는 별도 단계가 필요해요 |
사용하면 안 되는 경우
- 테스트가 전부 몇 초 안에 끝나는 작은 프로젝트에서는 선택 로직이 복잡도만 더해요.
- 최종 병합·배포 후보에는 선택 실행만 쓰면 안 돼요. 전체와 E2E를 따로 돌려요.
- 동적 import나 코드 생성이 많은 코드베이스에서 정적 그래프만 믿으면 안 돼요.
흔한 실수
- 모르는 상황을 통과로 처리하기. "모르면 통과"가 아니라 "모르면 전체"여야 해요.
- 기준점을
main이나HEAD~1로 고정하기. 실제 PR 대상, 이전 원격 SHA와 어긋나요. - 공유 모듈 변경 때 선택 실행이 빠르다고 기대하기. 이런 변경은 관련 테스트가 전체에 가까워요.
- 병렬 환경에서 흔들리는 E2E를 sleep으로 덮기. 실패한 테스트만 단독으로 돌려서 환경 문제인지 먼저 가르세요(playwright-flaky-vs-failed-triage).
- 측정 없이 "빨라졌다"고 주장하기.
관련 개념
- playwright-flaky-vs-failed-triage — 병렬 실행에서만 흔들리는 실패를 가려내는 방법
- mutation-testing — 테스트가 실제로 결함을 잡는지 확인하는 다른 방법
- test-oracle — 선택된 테스트가 믿을 만한 정답 기준을 갖는가