본문으로 건너뛰기
Kreath Archive
TechProjectsBooksAbout
TechProjectsBooksAbout

내비게이션

  • Tech
  • Projects
  • Books
  • About
  • Tags

카테고리

  • AI / ML
  • 웹 개발
  • 프로그래밍
  • 개발 도구

연결

  • GitHub
  • Email
  • RSS
© 2026 Kreath Archive. All rights reserved.Built with Next.js + MDX
홈TechProjectsBooksAbout
//
  1. 홈
  2. 테크
  3. 4장: AI 기반 테스트 자동 생성
2026년 1월 25일·AI / ML·

4장: AI 기반 테스트 자동 생성

코드 변경을 분석하여 단위 테스트와 통합 테스트를 자동으로 생성하는 시스템을 구축하고, 테스트 품질을 검증하는 방법을 다룹니다.

18분971자6개 섹션
devtoolsautomationcode-qualitydevopsllm
공유
ai-dev-workflow4 / 10
12345678910
이전3장: AI 코드 리뷰 실전 구축 - GitHub Actions 통합다음5장: AI 기반 문서화 자동화

AI 테스트 생성이 필요한 이유

테스트 작성은 소프트웨어 품질을 보장하는 핵심 활동이지만, 개발자들이 가장 미루기 쉬운 작업이기도 합니다. 새로운 기능을 구현한 후 테스트를 작성하는 것은 시간이 소요되고, 기존 코드의 테스트 커버리지를 높이는 것은 더욱 어렵습니다.

AI 기반 테스트 생성은 이 문제를 두 가지 측면에서 해결합니다.

첫째, PR의 변경 사항을 분석하여 해당 변경에 대한 테스트를 자동으로 생성합니다. 개발자가 기능을 구현하면, AI가 해당 기능의 경계 조건, 에러 시나리오, 정상 흐름에 대한 테스트를 제안합니다.

둘째, 기존 코드베이스에서 테스트가 부족한 영역을 식별하고, 해당 영역의 테스트를 생성합니다. 이를 통해 점진적으로 전체 커버리지를 높일 수 있습니다.

text
기존 테스트 작성 흐름:
  개발자가 기능 구현 --> 개발자가 테스트 작성 --> 리뷰 요청
  문제: 테스트 누락, 경계 조건 미흡, 시간 소모
 
AI 보조 테스트 흐름:
  개발자가 기능 구현 --> AI가 테스트 초안 생성 --> 개발자가 검증/보완 --> 리뷰 요청
  장점: 기본 커버리지 보장, 경계 조건 포함, 시간 절약

테스트 생성 시스템 아키텍처

전체 구조

테스트 생성 시스템은 코드 분석, 테스트 전략 수립, 테스트 코드 생성, 검증의 네 단계로 구성됩니다.

코드 분석 단계

테스트를 생성하기 전에 변경된 코드를 깊이 있게 분석해야 합니다. 함수의 시그니처, 의존성, 분기 조건, 에러 처리 패턴 등을 파악합니다.

src/code-analyzer.ts
typescript
interface FunctionAnalysis {
  name: string
  filePath: string
  // 함수 시그니처
  parameters: ParameterInfo[]
  returnType: string
  // 함수의 복잡도 정보
  cyclomaticComplexity: number
  // 의존성 (호출하는 외부 함수/모듈)
  dependencies: string[]
  // 분기 조건
  branches: BranchInfo[]
  // 에러 처리
  errorHandling: ErrorHandlingInfo[]
  // 부수 효과 (DB, API, 파일 I/O)
  sideEffects: SideEffect[]
}
 
interface BranchInfo {
  type: "if" | "switch" | "ternary" | "guard"
  condition: string
  lineNumber: number
}
 
interface SideEffect {
  type: "database" | "api" | "filesystem" | "network"
  description: string
}
 
function analyzeFunction(
  sourceCode: string,
  functionName: string
): FunctionAnalysis {
  // AST 기반 분석 또는 LLM 기반 분석
 
  // 분기 복잡도 계산
  const complexity = calculateCyclomaticComplexity(
    sourceCode, functionName
  )
 
  // 의존성 추출
  const deps = extractDependencies(sourceCode, functionName)
 
  // 부수 효과 식별
  const effects = identifySideEffects(sourceCode, functionName)
 
  return {
    name: functionName,
    filePath: "",
    parameters: extractParameters(sourceCode, functionName),
    returnType: inferReturnType(sourceCode, functionName),
    cyclomaticComplexity: complexity,
    dependencies: deps,
    branches: extractBranches(sourceCode, functionName),
    errorHandling: extractErrorHandling(
      sourceCode, functionName
    ),
    sideEffects: effects,
  }
}

테스트 전략 수립

코드 분석 결과를 바탕으로 어떤 테스트를 생성할지 전략을 수립합니다.

src/test-strategy.ts
typescript
interface TestStrategy {
  // 테스트할 함수
  targetFunction: string
  // 테스트 유형
  testType: "unit" | "integration" | "e2e"
  // 테스트 케이스 목록
  testCases: TestCaseSpec[]
  // 필요한 모킹
  mocks: MockSpec[]
  // 테스트 프레임워크
  framework: "jest" | "vitest" | "pytest" | "mocha"
}
 
interface TestCaseSpec {
  description: string
  category: "happy-path" | "edge-case" | "error-case" | "boundary"
  inputs: Record<string, unknown>
  expectedOutput: unknown
  priority: "high" | "medium" | "low"
}
 
function buildTestStrategy(
  analysis: FunctionAnalysis
): TestStrategy {
  const testCases: TestCaseSpec[] = []
 
  // 1. 정상 경로 테스트 (필수)
  testCases.push({
    description: analysis.name + "의 정상 동작 검증",
    category: "happy-path",
    inputs: generateTypicalInputs(analysis.parameters),
    expectedOutput: null, // LLM이 추론
    priority: "high",
  })
 
  // 2. 경계값 테스트
  for (const param of analysis.parameters) {
    const boundaryInputs = generateBoundaryInputs(param)
    for (const input of boundaryInputs) {
      testCases.push({
        description: param.name + "의 경계값 (" + input.label + ")",
        category: "boundary",
        inputs: input.values,
        expectedOutput: null,
        priority: "medium",
      })
    }
  }
 
  // 3. 에러 케이스 테스트
  for (const errorCase of analysis.errorHandling) {
    testCases.push({
      description: "에러 처리: " + errorCase.description,
      category: "error-case",
      inputs: generateErrorTriggerInputs(errorCase),
      expectedOutput: null,
      priority: "high",
    })
  }
 
  // 4. 분기별 테스트
  for (const branch of analysis.branches) {
    testCases.push({
      description: "분기 조건: " + branch.condition,
      category: "edge-case",
      inputs: generateBranchInputs(branch),
      expectedOutput: null,
      priority: "medium",
    })
  }
 
  // 모킹 전략 결정
  const mocks = analysis.sideEffects.map(effect => ({
    target: effect.description,
    type: effect.type,
    behavior: "stub" as const,
  }))
 
  return {
    targetFunction: analysis.name,
    testType: analysis.sideEffects.length > 0
      ? "integration"
      : "unit",
    testCases,
    mocks,
    framework: "vitest",
  }
}

LLM을 활용한 테스트 코드 생성

프롬프트 설계

테스트 생성을 위한 프롬프트는 코드 분석 결과와 테스트 전략을 모두 포함해야 합니다.

src/test-generator.ts
typescript
function buildTestGenerationPrompt(
  sourceCode: string,
  analysis: FunctionAnalysis,
  strategy: TestStrategy,
  existingTests: string | null
): string {
  let prompt = `다음 함수에 대한 테스트 코드를 생성해 주세요.
 
## 대상 함수
\`\`\`typescript
` + sourceCode + `
\`\`\`
 
## 함수 분석
- 순환 복잡도: ` + analysis.cyclomaticComplexity + `
- 의존성: ` + analysis.dependencies.join(", ") + `
- 부수 효과: ` + analysis.sideEffects.map(e => e.type).join(", ") + `
 
## 요청 테스트 케이스
`
  for (const tc of strategy.testCases) {
    prompt += "- [" + tc.priority + "] " + tc.description
      + " (" + tc.category + ")\n"
  }
 
  prompt += `
## 테스트 작성 규칙
1. 테스트 프레임워크: ` + strategy.framework + `
2. 각 테스트는 독립적으로 실행 가능해야 합니다
3. 외부 의존성은 모킹합니다
4. 테스트명은 "should + 기대 동작" 형식입니다
5. Arrange-Act-Assert 패턴을 따릅니다
6. 타입 안전한 코드를 작성합니다
`
 
  if (existingTests) {
    prompt += `
## 기존 테스트 (스타일 참고)
\`\`\`typescript
` + existingTests + `
\`\`\`
기존 테스트의 스타일과 패턴을 따라 작성하세요.
`
  }
 
  prompt += `
응답은 실행 가능한 테스트 코드만 포함하세요. 설명은 코드 주석으로 포함합니다.
`
  return prompt
}

생성된 테스트의 형태

LLM이 생성하는 테스트 코드의 예시를 확인합니다. 다음은 사용자 인증 함수에 대해 생성된 테스트입니다.

src/auth.test.ts (생성 예시)
typescript
import { describe, it, expect, vi, beforeEach } from "vitest"
import { authenticateUser } from "./auth"
import { findUserByEmail } from "./user-repository"
import { comparePassword } from "./crypto"
import { generateToken } from "./token"
 
// 외부 의존성 모킹
vi.mock("./user-repository")
vi.mock("./crypto")
vi.mock("./token")
 
describe("authenticateUser", () => {
  const mockUser = {
    id: "user-1",
    email: "test@example.com",
    passwordHash: "hashed-password",
    isActive: true,
  }
 
  beforeEach(() => {
    vi.clearAllMocks()
  })
 
  // 정상 경로 테스트
  it("should return token when credentials are valid", async () => {
    // Arrange
    vi.mocked(findUserByEmail).mockResolvedValue(mockUser)
    vi.mocked(comparePassword).mockResolvedValue(true)
    vi.mocked(generateToken).mockReturnValue("mock-token")
 
    // Act
    const result = await authenticateUser(
      "test@example.com",
      "correct-password"
    )
 
    // Assert
    expect(result).toEqual({
      success: true,
      token: "mock-token",
      user: { id: "user-1", email: "test@example.com" },
    })
  })
 
  // 에러 케이스: 존재하지 않는 사용자
  it("should throw when user is not found", async () => {
    vi.mocked(findUserByEmail).mockResolvedValue(null)
 
    await expect(
      authenticateUser("unknown@example.com", "password")
    ).rejects.toThrow("Invalid credentials")
  })
 
  // 에러 케이스: 잘못된 비밀번호
  it("should throw when password is incorrect", async () => {
    vi.mocked(findUserByEmail).mockResolvedValue(mockUser)
    vi.mocked(comparePassword).mockResolvedValue(false)
 
    await expect(
      authenticateUser("test@example.com", "wrong-password")
    ).rejects.toThrow("Invalid credentials")
  })
 
  // 경계 케이스: 비활성 계정
  it("should throw when account is deactivated", async () => {
    const inactiveUser = { ...mockUser, isActive: false }
    vi.mocked(findUserByEmail).mockResolvedValue(inactiveUser)
    vi.mocked(comparePassword).mockResolvedValue(true)
 
    await expect(
      authenticateUser("test@example.com", "correct-password")
    ).rejects.toThrow("Account is deactivated")
  })
 
  // 경계 케이스: 빈 입력
  it("should throw when email is empty", async () => {
    await expect(
      authenticateUser("", "password")
    ).rejects.toThrow("Email is required")
  })
 
  it("should throw when password is empty", async () => {
    await expect(
      authenticateUser("test@example.com", "")
    ).rejects.toThrow("Password is required")
  })
})

테스트 품질 검증

AI가 생성한 테스트가 실제로 유효한지 검증하는 과정이 필수적입니다. 생성만 하고 검증하지 않으면 오히려 거짓 안정감(False Confidence)을 줄 수 있습니다.

실행 기반 검증

생성된 테스트를 실제로 실행하여 통과 여부를 확인합니다.

src/test-validator.ts
typescript
import { execSync } from "child_process"
 
interface ValidationResult {
  passed: boolean
  totalTests: number
  passedTests: number
  failedTests: number
  errors: TestError[]
  coverage: CoverageInfo | null
}
 
interface TestError {
  testName: string
  error: string
  type: "compilation" | "assertion" | "runtime"
}
 
async function validateGeneratedTests(
  testFilePath: string,
  sourceFilePath: string
): Promise<ValidationResult> {
  // 1단계: 타입 검사
  const typeCheckResult = typeCheck(testFilePath)
  if (!typeCheckResult.passed) {
    return {
      passed: false,
      totalTests: 0,
      passedTests: 0,
      failedTests: 0,
      errors: typeCheckResult.errors.map(e => ({
        testName: "compilation",
        error: e,
        type: "compilation" as const,
      })),
      coverage: null,
    }
  }
 
  // 2단계: 테스트 실행
  try {
    const output = execSync(
      "npx vitest run " + testFilePath + " --reporter=json",
      { encoding: "utf-8", timeout: 30000 }
    )
 
    const result = JSON.parse(output)
    return parseVitestResult(result)
  } catch (error) {
    return parseTestFailure(error)
  }
}

뮤테이션 테스트로 품질 측정

생성된 테스트가 단순히 통과하는 것을 넘어, 실제로 버그를 잡을 수 있는지 확인하기 위해 뮤테이션 테스트(Mutation Testing)를 활용합니다.

text
뮤테이션 테스트 원리:
 
  1. 원본 코드에 의도적인 변경(뮤턴트)을 삽입
     예: if (x > 0) --> if (x >= 0)
         return a + b --> return a - b
 
  2. 변경된 코드에 대해 테스트 실행
 
  3. 테스트가 실패하면 뮤턴트가 "죽음" (killed)
     테스트가 통과하면 뮤턴트가 "생존" (survived)
 
  4. 뮤테이션 점수 = 죽인 뮤턴트 / 전체 뮤턴트
 
  높은 뮤테이션 점수 = 테스트가 실제 버그를 잡는 능력이 높음
src/mutation-tester.ts
typescript
interface MutationResult {
  mutationScore: number     // 0.0 ~ 1.0
  totalMutants: number
  killedMutants: number
  survivedMutants: number
  // 생존한 뮤턴트 정보 (테스트 보완 필요)
  survivors: MutantInfo[]
}
 
interface MutantInfo {
  type: string              // 뮤테이션 유형
  location: {
    file: string
    line: number
  }
  original: string          // 원본 코드
  mutated: string           // 변경된 코드
}
 
async function runMutationTest(
  sourceFile: string,
  testFile: string
): Promise<MutationResult> {
  // Stryker를 사용한 뮤테이션 테스트 실행
  const output = execSync(
    "npx stryker run --files " + sourceFile
    + " --testFiles " + testFile
    + " --reporters json",
    { encoding: "utf-8", timeout: 120000 }
  )
 
  return parseMutationResult(output)
}
Tip

뮤테이션 점수가 80% 이상이면 테스트 품질이 양호하다고 판단할 수 있습니다. 생존한 뮤턴트 정보를 LLM에 제공하면, 해당 뮤턴트를 잡기 위한 추가 테스트를 생성할 수 있습니다.

자동 수정 루프

생성된 테스트가 실패하면, 에러 정보를 LLM에 전달하여 수정을 시도합니다.

src/auto-fix.ts
typescript
async function generateAndValidateTests(
  sourceCode: string,
  analysis: FunctionAnalysis,
  maxRetries: number = 3
): Promise<string | null> {
  let attempt = 0
  let testCode: string | null = null
  let lastError: string | null = null
 
  while (attempt < maxRetries) {
    // 테스트 생성 (이전 에러가 있으면 맥락에 포함)
    const prompt = lastError
      ? buildRetryPrompt(sourceCode, analysis, testCode, lastError)
      : buildTestGenerationPrompt(
          sourceCode, analysis,
          buildTestStrategy(analysis), null
        )
 
    testCode = await generateTestWithLLM(prompt)
 
    // 테스트 검증
    const tempFile = writeTempFile(testCode)
    const result = await validateGeneratedTests(
      tempFile, analysis.filePath
    )
 
    if (result.passed) {
      return testCode
    }
 
    lastError = result.errors
      .map(e => e.testName + ": " + e.error)
      .join("\n")
 
    attempt++
  }
 
  // 모든 재시도 실패 시 null 반환
  return null
}
 
function buildRetryPrompt(
  sourceCode: string,
  analysis: FunctionAnalysis,
  previousTest: string | null,
  error: string
): string {
  return `이전에 생성한 테스트에서 오류가 발생했습니다. 수정해 주세요.
 
## 대상 함수
\`\`\`typescript
` + sourceCode + `
\`\`\`
 
## 이전 테스트 코드
\`\`\`typescript
` + (previousTest || "") + `
\`\`\`
 
## 발생한 오류
` + error + `
 
오류를 수정한 전체 테스트 코드를 다시 작성해 주세요.
`
}

GitHub Actions 통합

테스트 생성 시스템을 GitHub Actions에 통합하여 PR마다 자동으로 테스트를 제안합니다.

.github/workflows/ai-test-gen.yml
yaml
name: AI Test Generation
 
on:
  pull_request:
    types: [opened, synchronize]
 
permissions:
  contents: write
  pull-requests: write
 
jobs:
  generate-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          ref: $GITHUB_HEAD_REF_VALUE
          fetch-depth: 0
 
      - uses: actions/setup-node@v4
        with:
          node-version: "20"
 
      - run: npm install
 
      - name: Analyze changes and generate tests
        env:
          ANTHROPIC_API_KEY: $ANTHROPIC_KEY_VALUE
        run: node scripts/generate-tests.js
 
      - name: Run generated tests
        run: npx vitest run --reporter=verbose
 
      - name: Commit generated tests
        run: |
          git config user.name "AI Test Bot"
          git config user.email "ai-test-bot@example.com"
          git add "**/*.test.ts" "**/*.spec.ts"
          git diff --staged --quiet || git commit -m "test: add AI-generated tests"
          git push
Warning

AI가 생성한 테스트를 자동으로 커밋하는 것은 팀의 합의가 필요합니다. 일부 팀에서는 테스트를 PR의 코멘트로 제안하고, 개발자가 검토 후 수동으로 추가하는 방식을 선호합니다. 팀의 문화와 코드 소유권 정책에 맞게 선택하세요.

정리

이 장에서는 AI 기반 테스트 자동 생성 시스템을 구축했습니다. 코드 분석, 테스트 전략 수립, LLM을 활용한 테스트 생성, 검증 루프, GitHub Actions 통합까지 전체 파이프라인을 다루었습니다.

핵심 내용을 정리합니다.

  • 코드 분석 단계에서 함수의 복잡도, 의존성, 분기 조건, 부수 효과를 파악합니다
  • 분석 결과를 바탕으로 정상 경로, 경계값, 에러 케이스, 분기별 테스트 전략을 수립합니다
  • LLM에 분석 결과와 전략을 전달하여 실행 가능한 테스트 코드를 생성합니다
  • 생성된 테스트를 실행하고, 실패 시 자동 수정 루프를 돌립니다
  • 뮤테이션 테스트로 생성된 테스트의 실질적 품질을 측정합니다

다음 장에서는 AI 기반 문서화 자동화를 다룹니다. 코드 변경에 따라 API 문서, README, 변경 로그를 자동으로 갱신하는 시스템을 구축합니다.

이 글이 도움이 되셨나요?

관련 주제 더 보기

#devtools#automation#code-quality#devops#llm

관련 글

AI / ML

5장: AI 기반 문서화 자동화

코드 변경에 따라 API 문서, README, 변경 로그를 AI로 자동 갱신하는 시스템을 구축하고, 문서와 코드의 동기화를 유지하는 전략을 다룹니다.

2026년 1월 27일·19분
AI / ML

3장: AI 코드 리뷰 실전 구축 - GitHub Actions 통합

GitHub Actions를 활용하여 PR에 자동으로 AI 코드 리뷰를 수행하는 시스템을 직접 구축하고, 실전에서 활용 가능한 수준으로 완성합니다.

2026년 1월 23일·18분
AI / ML

6장: PR 분석과 변경 영향도 예측

PR의 변경 범위와 위험도를 AI로 분석하고, 리뷰어에게 구조화된 인사이트를 제공하는 시스템을 구축합니다.

2026년 1월 29일·17분
이전 글3장: AI 코드 리뷰 실전 구축 - GitHub Actions 통합
다음 글5장: AI 기반 문서화 자동화

댓글

목차

약 18분 남음
  • AI 테스트 생성이 필요한 이유
  • 테스트 생성 시스템 아키텍처
    • 전체 구조
    • 코드 분석 단계
    • 테스트 전략 수립
  • LLM을 활용한 테스트 코드 생성
    • 프롬프트 설계
    • 생성된 테스트의 형태
  • 테스트 품질 검증
    • 실행 기반 검증
    • 뮤테이션 테스트로 품질 측정
    • 자동 수정 루프
  • GitHub Actions 통합
  • 정리