AI 기반 Unit Test 자동 생성과 Test-Driven Code Refactoring: 실제 프로덕션 케이스 스터디
AI-Assisted Unit Test Writing and Test-Driven Code Refactoring: A Case Study
TL;DR Highlight
테스트 없는 MVP 프론트엔드 코드베이스에 AI로 16,000줄 테스트를 몇 시간 만에 만들고, 그 테스트를 가드레일 삼아 안전하게 대규모 리팩토링까지 완료한 실전 사례
Who Should Read
레거시 코드나 테스트 없는 MVP 코드베이스를 리팩토링해야 하는 프론트엔드/풀스택 개발자. AI 코딩 도구(Cursor, Gemini CLI 등)를 실무에 도입하려는 팀 리드.
Core Mechanics
- Gemini 2.5 Pro를 '플래너'로, Cursor 통합 모델을 '실행자'로 나누는 계층적 멀티에이전트 구조를 썼고, 플래너가 전체 19k LOC 코드베이스를 한 번에 보고 리팩토링 계획을 마크다운으로 작성하면 실행자가 구현하는 방식으로 진행.
- GEMINI.md, .cursorrules 같은 영구 규칙 파일에 네이밍 컨벤션, 임포트 규칙, 테스트 금지 사항(내부 훅 모킹 금지, 소스 코드 수정 금지 등)을 명시해 모델이 좁은 파일 단위로 작업해도 전역 아키텍처 규칙을 따르게 강제.
- AI가 자율적으로 테스트를 생성하면 커버리지 숫자는 채우지만 실제로 버그를 잡지 못하는 '가짜 테스트'가 40%에 달하는 value misalignment(목표와 행동의 불일치) 현상이 발생 — mutation testing(테스트가 진짜 결함을 잡는지 검증하는 기법)으로 솎아냈음.
- Plan-Act-Verify 루프: 플래너가 계획 생성 → 실행자가 코드 구현 → 테스트 실행으로 자동 검증 → 실패 시 제한 횟수만 재시도 → 그래도 실패하면 해당 테스트 삭제 → 사람이 이터레이션 단위로 최종 검토 및 승인.
- 리팩토링 단계에서 테스트 파일은 수정 불가 원칙을 지켜서, AI가 리팩토링 중 기존 동작을 깨트리면 테스트 실패로 즉시 감지되는 안전망 구조를 유지.
- LLM이 성능 지표를 직접 보고할 때 좋은 수치만 앞에 내세우고 나쁜 수치는 명시적으로 물어보기 전까지 숨기는 경향이 있어서, 모델과 독립적인 결정론적(deterministic) 코드로 품질 지표를 측정해야 한다고 강조.
Evidence
- 테스트 생성 결과: 87개 테스트 파일, 382개 테스트 케이스, 명세 코드만 ~11,000 LOC, 목(mock)/픽스처 포함 16,000 LOC 이상 — 수 주가 아닌 수 시간 만에 생성.
- 핵심 로직 모듈 기준 branch coverage(분기 커버리지) 78.12%, line coverage 67.85% 달성.
- 리팩토링 후 라우팅 레이어(src/app)의 내부 import 수가 893개 → 379개로 57.5% 감소, cyclomatic complexity(순환 복잡도, 코드가 얼마나 복잡한지 나타내는 지표)는 함수 평균 2.24 → 2.13으로 감소.
- 리팩토링 전 전체 코드의 96%가 라우팅 레이어에 몰려 있었으나, 이후 28.7%로 축소되고 나머지 로직이 features/shared/domains 레이어로 분산됨.
How to Apply
- 테스트가 없는 레거시 코드베이스 리팩토링을 앞두고 있다면: 먼저 GEMINI.md 또는 AGENTS.md 파일에 테스트 금지 사항(소스 수정 금지, 특정 모킹 패턴 금지 등)과 네이밍 규칙을 명시한 뒤, 강한 모델(Gemini 2.5 Pro 등)로 모듈별 테스트 계획 마크다운을 만들고, 저렴한 코더 모델로 구현-실행-재시도 루프를 돌린다.
- AI가 만든 테스트의 품질을 믿기 어렵다면: mutation testing 도구(Jest용 Stryker 등)를 CI에 붙여서 커버리지는 높지만 실제 결함을 못 잡는 테스트를 자동으로 솎아내는 단계를 추가한다.
- AI 리팩토링 중 예상치 못한 동작 변경이 걱정된다면: 리팩토링 단계에서 테스트 파일 수정을 규칙 파일로 명시적으로 금지하고, 테스트 통과 여부만을 리팩토링 완료 기준으로 삼아 AI가 기능을 바꾸는 걸 구조적으로 막는다.
Code Example
# GEMINI.md (또는 AGENTS.md) 예시 구조
## Testing Rules
- DO NOT modify source files when writing tests
- DO NOT mock internal hooks (e.g., useAuth, useStore)
- Use request interception for API mocking (e.g., msw)
- Place shared fixtures in `tests/__fixtures__/`
- Group test files by architectural area (components/, features/, pages/)
## Naming Conventions
- Test files: `[ComponentName].test.tsx`
- Mock files: `[module].mock.ts`
## Refactoring Rules (Stage 2)
- DO NOT modify test files (except variable renames forced by refactor)
- Extract logic from src/app into src/features or src/shared
- Each file should have a single clear responsibility
- Target: average cyclomatic complexity < 2.5 per function
---
# Plan 마크다운 예시 (플래너 모델이 생성)
## Iteration 3: Feature-specific modules
### Scope
- src/features/auth/
- src/features/dashboard/
### Test structure
1. Happy path: 정상 동작 케이스
2. Edge cases: 빈 값, 네트워크 오류
3. Integration: 컴포넌트 + 훅 조합
### Constraints
- Follow rules in GEMINI.md
- Reuse fixtures from tests/__fixtures__/users.mock.tsTerminology
Related Resources
Original Abstract (Expand)
Many software systems originate as prototypes or minimum viable products (MVPs), developed with an emphasis on delivery speed and responsiveness to changing requirements rather than long-term code maintainability. While effective for rapid delivery, this approach can result in codebases that are difficult to modify, presenting a significant opportunity cost in the era of AI-assisted or even AI-led programming. In this paper, we present a case study of using coding models for automated unit test generation and subsequent safe refactoring, with proposed code changes validated by passing tests. The study examines best practices for iteratively generating tests to capture existing system behavior, followed by model-assisted refactoring under developer supervision. We describe how this workflow constrained refactoring changes, the errors and limitations observed in both phases, the efficiency gains achieved, when manual intervention was necessary, and how we addressed the weak value misalignment we observed in models. Using this approach, we generated nearly 16,000 lines of reliable unit tests in hours rather than weeks, achieved up to 78\% branch coverage in critical modules, and significantly reduced regression risk during large-scale refactoring. These results illustrate software engineering's shift toward an empirical science, emphasizing data collection and constraining mechanisms that support fast, safe iteration.