테스트 코드 작성이 중요한 건 알지만 매번 손으로 다 짜기엔 시간이 너무 아까웠다. 그래서 AI를 활용해서 Git 커밋 기반으로 자동으로 테스트 코드를 생성하는 시스템을 만들어봤음. 생각보다 잘 동작해서 공유해보려고 함.
전체적인 워크플로우는 이렇게 돌아감:
핵심은 변경된 코드에 대해서만 테스트를 생성한다는 점임. 전체 프로젝트를 다 분석하는 게 아니라 실제로 수정된 부분만 타겟팅하니까 효율적임.
macOS에서 시스템 Python이 보호되어 있어서 가상 환경이 필수임:
# 프로젝트 루트에서 실행
python3 -m venv venv
source venv/bin/activate
pip install google-generativeai
local.properties 파일에 Gemini API 키를 추가함:
sdk.dir=/Users/username/Library/Android/sdk
gemini.api.key=YOUR_GEMINI_API_KEY_HERE
보안상 이 파일은 절대 커밋하면 안 됨. .gitignore에 이미 들어가 있겠지만 확인해보는 게 좋음.
scripts/gemini_test_gen.py 파일을 만들어서 실제 AI 테스트 생성 로직을 구현했음:
import sys
import os
import google.generativeai as genai
from pathlib import Path
class GeminiTestGenerator:
def __init__(self):
api_key = self.get_api_key()
if not api_key:
raise ValueError("API 키를 찾을 수 없습니다!")
genai.configure(api_key=api_key)
self.model = genai.GenerativeModel('gemini-1.5-pro')
def get_api_key(self):
# 환경 변수에서 먼저 확인
api_key = os.getenv('GEMINI_API_KEY') or os.getenv('GOOGLE_API_KEY')
if api_key:
return api_key
# local.properties에서 읽기
script_dir = Path(__file__).parent.parent
local_properties_path = script_dir / 'local.properties'
if local_properties_path.exists():
with open(local_properties_path, 'r') as f:
for line in f:
line = line.strip()
if line.startswith('gemini.api.key='):
return line.split('=', 1)[1].strip()
return None
def generate_test(self, file_path):
with open(file_path, 'r', encoding='utf-8') as f:
code = f.read()
# UI 컴포넌트가 있는지 체크해서 테스트 타입 결정
is_ui = 'Activity' in code or 'Fragment' in code or 'Composable' in code
prompt = f"""
Android 코드에 대한 테스트를 생성해주세요.
파일: {file_path}
코드:
```kotlin
{code}
```
요구사항:
{'- Espresso UI 테스트 포함' if is_ui else ''}
- JUnit4 사용
- Mockito로 의존성 모킹
- 한글 테스트 메서드명 (백틱 사용)
- @Test 어노테이션 필수
- Given-When-Then 패턴
"""
response = self.model.generate_content(prompt)
return response.text
def save_test(self, original_path, test_code):
# main 디렉토리를 test로 바꿔서 테스트 파일 경로 생성
test_path = original_path.replace('/main/', '/test/')
test_path = test_path.replace('.kt', 'Test.kt')
Path(test_path).parent.mkdir(parents=True, exist_ok=True)
with open(test_path, 'w', encoding='utf-8') as f:
f.write(test_code)
return test_path
코드를 보면 UI 컴포넌트 여부를 자동으로 감지해서 Espresso 테스트를 포함할지 결정하는 로직이 들어가 있음. 이런 세부사항들이 실제 사용할 때 중요함.
app/build.gradle.kts에 커스텀 태스크들을 추가해서 명령어 한 번으로 전체 프로세스가 돌아가도록 만들었음:
// Python 환경 자동 설정
tasks.register("setupPythonEnv") {
group = "ai testing"
description = "Setup Python virtual environment"
doLast {
val venvDir = file("${project.rootDir}/venv")
if (!venvDir.exists()) {
println("Python 가상 환경 생성 중...")
exec {
commandLine("python3", "-m", "venv", "venv")
workingDir = project.rootDir
}
exec {
commandLine("${project.rootDir}/venv/bin/pip", "install", "google-generativeai")
}
println("Python 환경 설정 완료!")
}
}
}
// 최근 커밋 변경사항 기반 테스트 생성
tasks.register("aiTestLastCommit") {
group = "ai testing"
description = "Generate AI tests for files in recent commits"
dependsOn("setupPythonEnv")
doLast {
// API 키 읽어오기
val localProperties = java.util.Properties()
val localPropertiesFile = rootProject.file("local.properties")
var apiKey: String? = null
if (localPropertiesFile.exists()) {
localProperties.load(localPropertiesFile.inputStream())
apiKey = localProperties.getProperty("gemini.api.key")
}
if (apiKey == null) {
throw GradleException("Gemini API 키가 필요합니다!")
}
val pythonCmd = "${project.rootDir}/venv/bin/python"
val scriptPath = "${project.rootDir}/scripts/gemini_test_gen.py"
// 분석할 커밋 개수 (기본값: 3개)
val commitCount = (project.findProperty("commits") as? String)?.toIntOrNull() ?: 3
println("최근 ${commitCount}개 커밋 분석 중...")
// Git 명령 실행 헬퍼 함수
fun runGitCommand(vararg args: String): String {
val process = ProcessBuilder("git", *args)
.directory(project.rootDir)
.redirectOutput(ProcessBuilder.Redirect.PIPE)
.start()
return process.inputStream.bufferedReader().readText().trim()
}
// 변경된 파일 목록 가져오기
val changedFiles = runGitCommand("diff", "--name-only", "HEAD~${commitCount}", "HEAD")
.lines()
.filter { it.endsWith(".kt") || it.endsWith(".java") }
.filter { it.isNotBlank() }
.filter { file("${project.rootDir}/$it").exists() }
if (changedFiles.isEmpty()) {
println("변경된 Kotlin/Java 파일이 없습니다.")
return@doLast
}
println("\n변경된 파일:")
changedFiles.forEach { println(" $it") }
var successCount = 0
var failCount = 0
// 각 파일에 대해 테스트 생성
changedFiles.forEach { filePath ->
val fullPath = "${project.rootDir}/$filePath"
println("\n테스트 생성 중: ${file(filePath).name}")
val processBuilder = ProcessBuilder(pythonCmd, scriptPath, fullPath)
.directory(project.rootDir)
.redirectOutput(ProcessBuilder.Redirect.PIPE)
.redirectError(ProcessBuilder.Redirect.PIPE)
// 환경 변수로 API 키 전달
val env = processBuilder.environment()
env["GEMINI_API_KEY"] = apiKey
val process = processBuilder.start()
val exitCode = process.waitFor()
if (exitCode == 0) {
successCount++
println("완료!")
} else {
failCount++
println("실패!")
val error = process.errorStream.bufferedReader().readText()
if (error.isNotEmpty()) {
println("에러: $error")
}
}
}
println("\n결과: 성공 $successCount 개, 실패 $failCount 개")
// 생성된 테스트 실행
if (successCount > 0) {
println("\n테스트 실행 중...")
ProcessBuilder("./gradlew", "app:test")
.directory(project.rootDir)
.inheritIO()
.start()
.waitFor()
}
}
}
편의를 위해 자주 쓰는 패턴들을 별도 태스크로 만들어뒀음:
// 최근 1개 커밋만
tasks.register("aiTest1") {
group = "ai testing"
dependsOn("aiTestLastCommit")
doFirst {
project.ext.set("commits", "1")
}
}
// 최근 5개 커밋
tasks.register("aiTest5") {
group = "ai testing"
dependsOn("aiTestLastCommit")
doFirst {
project.ext.set("commits", "5")
}
}
설정이 끝나면 사용법은 정말 간단함:
# 초기 설정 (딱 한 번만)
./gradlew setupPythonEnv
# 최근 3개 커밋의 변경사항에 대해 테스트 생성
./gradlew aiTestLastCommit
# 최근 1개 커밋만
./gradlew aiTest1
# 최근 5개 커밋
./gradlew aiTest5
# 특정 개수 직접 지정
./gradlew aiTestLastCommit -Pcommits=10
Android Studio에서도 쓸 수 있음. Gradle 탭에서 app → Tasks → ai testing 폴더를 열면 태스크들이 보이고, 더블클릭하면 실행됨.
배포 프로세스에 통합하고 싶다면 fastlane/Fastfile에 추가할 수 있음:
platform :android do
desc "AI가 생성한 테스트 실행"
lane :ai_test do
gradle(task: "aiTestLastCommit")
gradle(task: "test")
slack(
message: "AI 테스트 생성 및 실행 완료!",
success: true
)
end
desc "배포 전 AI 테스트 포함 전체 검증"
lane :deploy do
ai_test
gradle(task: "bundleRelease")
upload_to_play_store
end
end
몇 주 써보니까 정말 편함. 특히 리팩토링 후에 기존 테스트가 깨졌을 때 빠르게 새 테스트를 생성할 수 있어서 좋았음. AI가 생성한 기본 테스트에 도메인 지식을 좀 더하면 꽤 쓸만한 테스트 스위트가 만들어짐.
완벽하진 않아서 수정이 꽤 많이 필요하긴 하지만,,, 테스트 작성의 초기 부담을 확실히 덜어줌. 특히 반복적인 CRUD 테스트나 기본적인 UI 테스트 같은 경우는 거의 그대로 써도 될 정도로 품질이 괜찮았음.
전체 소스 코드는 바로 프로젝트에 적용할 수 있도록 구성해뒀음. 팀 상황에 맞게 커스터마이징해서 쓰면 될 것 같다. 테스트 작성이 부담스러웠던 개발자들에게는 정말 도움이 될 거라고 생각함.
AI 도구들이 점점 발전하고 있으니까 이런 자동화 시스템들도 계속 개선될 것 같다. 지금부터 도입해서 익숙해지면 나중에 더 큰 도움이 될 듯!