본문으로 건너뛰기
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. 2장: AI 기반 단위 테스트 자동 생성
2026년 3월 6일·AI / ML·

2장: AI 기반 단위 테스트 자동 생성

LLM 기반 단위 테스트 자동 생성의 원리와 실전 활용법을 다룹니다. Diffblue, Codium/Qodo 도구를 활용한 pytest/Jest 테스트 생성 실습과 생성된 테스트의 품질 검증 방법을 안내합니다.

19분1,018자8개 섹션
testingautomationquality-assuranceai
공유
ai-testing2 / 10
12345678910
이전1장: AI 기반 테스트 자동화의 진화와 현재다음3장: 통합 테스트와 API 테스트 자동화

학습 목표

  • LLM이 단위 테스트를 생성하는 원리를 이해합니다
  • Diffblue, Codium/Qodo 등 주요 도구의 특성과 활용법을 파악합니다
  • Python(pytest)과 JavaScript(Jest) 환경에서 AI 테스트 생성을 실습합니다
  • 생성된 테스트의 품질을 검증하는 기준과 방법을 학습합니다

단위 테스트 자동 생성의 원리

AI 기반 단위 테스트 생성은 단순히 템플릿을 채우는 것이 아닙니다. LLM은 소스 코드의 의미를 이해하고, 함수의 계약(입력-출력 관계)을 추론하여 의미 있는 테스트 케이스를 만들어냅니다.

생성 프로세스

LLM은 다음과 같은 정보를 종합적으로 분석합니다.

  • 함수 시그니처: 매개변수 타입, 반환 타입, 기본값
  • 함수 본문: 분기 조건, 반복문, 예외 처리 로직
  • 의존성: 외부 모듈, 데이터베이스 연결, API 호출
  • 문서화: JSDoc, docstring, 타입 힌트 등 개발자가 남긴 의도
  • 프로젝트 컨텍스트: 테스트 프레임워크 설정, 기존 테스트 패턴

핵심 기법: 심볼릭 분석 + LLM 추론

최신 도구들은 Symbolic Analysis(심볼릭 분석)와 LLM 추론을 결합합니다. 심볼릭 분석은 코드의 실행 경로를 정적으로 추적하여 가능한 모든 분기를 파악하고, LLM은 각 분기에 대해 의미 있는 입력값을 생성합니다.

example_function.py
python
def calculate_discount(price: float, membership: str, coupon_code: str | None = None) -> float:
    """회원 등급과 쿠폰에 따른 할인 금액을 계산합니다."""
    if price <= 0:
        raise ValueError("가격은 0보다 커야 합니다")
    
    discount_rate = 0.0
    
    if membership == "gold":
        discount_rate = 0.15
    elif membership == "silver":
        discount_rate = 0.10
    elif membership == "bronze":
        discount_rate = 0.05
    
    if coupon_code == "SPECIAL2026":
        discount_rate += 0.05
    
    return round(price * discount_rate, 2)

이 함수에 대해 AI는 다음과 같은 관점에서 테스트를 생성합니다.

관점테스트 케이스
정상 경로각 회원 등급별 할인율 계산
경계값가격이 0인 경우, 매우 큰 가격
에지 케이스알 수 없는 회원 등급, None 쿠폰 코드
조합회원 할인 + 쿠폰 할인 중복 적용
예외음수 가격 입력 시 ValueError

주요 도구 분석

Diffblue Cover -- Java 엔터프라이즈 특화

Diffblue Cover는 Java 생태계에서 가장 성숙한 AI 단위 테스트 생성 도구입니다. Oxford 대학의 연구에서 출발한 이 도구는 강화 학습과 심볼릭 실행을 결합하여 JUnit 테스트를 자동 생성합니다.

UserService.java
java
public class UserService {
    private final UserRepository userRepository;
    
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
    
    public User findActiveUser(String email) {
        User user = userRepository.findByEmail(email);
        if (user == null) {
            throw new UserNotFoundException("사용자를 찾을 수 없습니다: " + email);
        }
        if (!user.isActive()) {
            throw new InactiveUserException("비활성 사용자입니다: " + email);
        }
        return user;
    }
}

Diffblue Cover가 생성하는 테스트 예시는 다음과 같습니다.

UserServiceTest.java (Diffblue 생성)
java
class UserServiceTest {
    @Mock
    private UserRepository userRepository;
    
    @InjectMocks
    private UserService userService;
    
    @BeforeEach
    void setUp() {
        MockitoAnnotations.openMocks(this);
    }
    
    @Test
    void findActiveUser_WhenUserExistsAndActive_ReturnsUser() {
        User activeUser = new User("test@example.com", true);
        when(userRepository.findByEmail("test@example.com")).thenReturn(activeUser);
        
        User result = userService.findActiveUser("test@example.com");
        
        assertThat(result).isEqualTo(activeUser);
        verify(userRepository).findByEmail("test@example.com");
    }
    
    @Test
    void findActiveUser_WhenUserNotFound_ThrowsException() {
        when(userRepository.findByEmail("unknown@example.com")).thenReturn(null);
        
        assertThrows(UserNotFoundException.class,
            () -> userService.findActiveUser("unknown@example.com"));
    }
    
    @Test
    void findActiveUser_WhenUserInactive_ThrowsException() {
        User inactiveUser = new User("inactive@example.com", false);
        when(userRepository.findByEmail("inactive@example.com")).thenReturn(inactiveUser);
        
        assertThrows(InactiveUserException.class,
            () -> userService.findActiveUser("inactive@example.com"));
    }
}
Info

Diffblue Cover는 CI 파이프라인에 통합하여 새로운 코드가 푸시될 때마다 자동으로 테스트를 생성하고 PR에 추가하는 워크플로를 구성할 수 있습니다. 이를 통해 개발자가 테스트 작성을 잊더라도 최소한의 커버리지를 보장합니다.

Codium/Qodo -- 다중 언어 지원

Codium(현재 Qodo로 리브랜딩)은 Python, JavaScript/TypeScript, Java 등 다중 언어를 지원하는 AI 테스트 생성 도구입니다. IDE 확장(VS Code, JetBrains)으로 동작하며, 개발자가 코드를 작성하는 시점에 실시간으로 테스트를 제안합니다.

Codium/Qodo의 차별점은 BDD(Behavior-Driven Development) 스타일의 테스트 시나리오를 먼저 제시한 후, 개발자가 선택한 시나리오에 대해 테스트 코드를 생성한다는 것입니다.


실습: Python pytest 테스트 생성

실제 함수를 대상으로 AI 도구가 어떤 테스트를 생성하는지 살펴보겠습니다.

대상 함수

shopping_cart.py
python
from dataclasses import dataclass
from typing import Optional
 
 
@dataclass
class CartItem:
    product_id: str
    name: str
    price: float
    quantity: int
 
 
class ShoppingCart:
    def __init__(self, max_items: int = 50):
        self._items: list[CartItem] = []
        self._max_items = max_items
 
    def add_item(self, item: CartItem) -> None:
        if item.quantity <= 0:
            raise ValueError("수량은 1 이상이어야 합니다")
        if item.price < 0:
            raise ValueError("가격은 0 이상이어야 합니다")
 
        total_quantity = sum(i.quantity for i in self._items) + item.quantity
        if total_quantity > self._max_items:
            raise OverflowError(f"장바구니 최대 수량({self._max_items})을 초과합니다")
 
        existing = self._find_item(item.product_id)
        if existing:
            existing.quantity += item.quantity
        else:
            self._items.append(item)
 
    def remove_item(self, product_id: str) -> None:
        item = self._find_item(product_id)
        if item is None:
            raise KeyError(f"상품을 찾을 수 없습니다: {product_id}")
        self._items.remove(item)
 
    def get_total(self, discount_rate: float = 0.0) -> float:
        if not 0.0 <= discount_rate <= 1.0:
            raise ValueError("할인율은 0.0에서 1.0 사이여야 합니다")
        subtotal = sum(item.price * item.quantity for item in self._items)
        return round(subtotal * (1 - discount_rate), 2)
 
    def _find_item(self, product_id: str) -> Optional[CartItem]:
        return next((i for i in self._items if i.product_id == product_id), None)
 
    @property
    def item_count(self) -> int:
        return len(self._items)
 
    @property
    def total_quantity(self) -> int:
        return sum(i.quantity for i in self._items)

AI가 생성한 테스트

test_shopping_cart.py (AI 생성)
python
import pytest
from shopping_cart import ShoppingCart, CartItem
 
 
class TestShoppingCartAddItem:
    """add_item 메서드 테스트"""
 
    def test_add_single_item(self):
        cart = ShoppingCart()
        item = CartItem("P001", "노트북", 1500000.0, 1)
        cart.add_item(item)
        assert cart.item_count == 1
        assert cart.total_quantity == 1
 
    def test_add_multiple_different_items(self):
        cart = ShoppingCart()
        cart.add_item(CartItem("P001", "노트북", 1500000.0, 1))
        cart.add_item(CartItem("P002", "마우스", 50000.0, 2))
        assert cart.item_count == 2
        assert cart.total_quantity == 3
 
    def test_add_duplicate_item_increases_quantity(self):
        cart = ShoppingCart()
        cart.add_item(CartItem("P001", "노트북", 1500000.0, 1))
        cart.add_item(CartItem("P001", "노트북", 1500000.0, 2))
        assert cart.item_count == 1
        assert cart.total_quantity == 3
 
    def test_add_item_with_zero_quantity_raises_error(self):
        cart = ShoppingCart()
        with pytest.raises(ValueError, match="수량은 1 이상"):
            cart.add_item(CartItem("P001", "노트북", 1500000.0, 0))
 
    def test_add_item_with_negative_quantity_raises_error(self):
        cart = ShoppingCart()
        with pytest.raises(ValueError, match="수량은 1 이상"):
            cart.add_item(CartItem("P001", "노트북", 1500000.0, -1))
 
    def test_add_item_with_negative_price_raises_error(self):
        cart = ShoppingCart()
        with pytest.raises(ValueError, match="가격은 0 이상"):
            cart.add_item(CartItem("P001", "노트북", -100.0, 1))
 
    def test_add_item_exceeding_max_items_raises_error(self):
        cart = ShoppingCart(max_items=3)
        cart.add_item(CartItem("P001", "A", 100.0, 2))
        with pytest.raises(OverflowError, match="최대 수량"):
            cart.add_item(CartItem("P002", "B", 200.0, 2))
 
    def test_add_item_at_exact_max_limit(self):
        cart = ShoppingCart(max_items=3)
        cart.add_item(CartItem("P001", "A", 100.0, 3))
        assert cart.total_quantity == 3
 
 
class TestShoppingCartRemoveItem:
    """remove_item 메서드 테스트"""
 
    def test_remove_existing_item(self):
        cart = ShoppingCart()
        cart.add_item(CartItem("P001", "노트북", 1500000.0, 1))
        cart.remove_item("P001")
        assert cart.item_count == 0
 
    def test_remove_nonexistent_item_raises_error(self):
        cart = ShoppingCart()
        with pytest.raises(KeyError, match="상품을 찾을 수 없습니다"):
            cart.remove_item("INVALID")
 
 
class TestShoppingCartGetTotal:
    """get_total 메서드 테스트"""
 
    def test_total_without_discount(self):
        cart = ShoppingCart()
        cart.add_item(CartItem("P001", "노트북", 1500000.0, 1))
        cart.add_item(CartItem("P002", "마우스", 50000.0, 2))
        assert cart.get_total() == 1600000.0
 
    def test_total_with_discount(self):
        cart = ShoppingCart()
        cart.add_item(CartItem("P001", "노트북", 1000000.0, 1))
        assert cart.get_total(discount_rate=0.1) == 900000.0
 
    def test_total_empty_cart(self):
        cart = ShoppingCart()
        assert cart.get_total() == 0.0
 
    def test_total_with_invalid_discount_raises_error(self):
        cart = ShoppingCart()
        with pytest.raises(ValueError, match="할인율은"):
            cart.get_total(discount_rate=1.5)
 
    def test_total_with_negative_discount_raises_error(self):
        cart = ShoppingCart()
        with pytest.raises(ValueError, match="할인율은"):
            cart.get_total(discount_rate=-0.1)
Tip

AI가 생성한 테스트를 그대로 사용하기보다, 다음을 점검하는 것이 좋습니다: (1) 비즈니스 로직의 의도를 정확히 반영하는지, (2) 테스트 이름이 동작을 명확히 설명하는지, (3) 불필요한 중복 테스트는 없는지 확인합니다.


실습: JavaScript Jest 테스트 생성

프론트엔드 환경에서의 AI 테스트 생성도 살펴보겠습니다.

대상 함수

format-utils.ts
typescript
export function formatCurrency(amount: number, locale: string = "ko-KR"): string {
  if (!Number.isFinite(amount)) {
    throw new TypeError("유효한 숫자가 아닙니다");
  }
 
  const formatter = new Intl.NumberFormat(locale, {
    style: "currency",
    currency: locale === "ko-KR" ? "KRW" : "USD",
    minimumFractionDigits: locale === "ko-KR" ? 0 : 2,
  });
 
  return formatter.format(amount);
}
 
export function truncateText(text: string, maxLength: number, suffix: string = "..."): string {
  if (maxLength < 0) {
    throw new RangeError("maxLength는 0 이상이어야 합니다");
  }
  if (text.length <= maxLength) {
    return text;
  }
  return text.slice(0, maxLength - suffix.length) + suffix;
}

AI 생성 테스트

format-utils.test.ts (AI 생성)
typescript
import { formatCurrency, truncateText } from "./format-utils";
 
describe("formatCurrency", () => {
  it("한국 원화 형식으로 포맷합니다", () => {
    expect(formatCurrency(15000)).toBe("\\15,000");
  });
 
  it("0원을 올바르게 포맷합니다", () => {
    expect(formatCurrency(0)).toBe("\\0");
  });
 
  it("음수 금액을 올바르게 포맷합니다", () => {
    expect(formatCurrency(-5000)).toBe("-\\5,000");
  });
 
  it("US 달러 형식으로 포맷합니다", () => {
    const result = formatCurrency(99.99, "en-US");
    expect(result).toBe("$99.99");
  });
 
  it("NaN 입력 시 TypeError를 던집니다", () => {
    expect(() => formatCurrency(NaN)).toThrow(TypeError);
  });
 
  it("Infinity 입력 시 TypeError를 던집니다", () => {
    expect(() => formatCurrency(Infinity)).toThrow(TypeError);
  });
});
 
describe("truncateText", () => {
  it("maxLength 이하 텍스트는 그대로 반환합니다", () => {
    expect(truncateText("안녕하세요", 10)).toBe("안녕하세요");
  });
 
  it("긴 텍스트를 잘라내고 접미사를 추가합니다", () => {
    expect(truncateText("이것은 매우 긴 텍스트입니다", 10)).toBe("이것은 매우...");
  });
 
  it("커스텀 접미사를 사용합니다", () => {
    expect(truncateText("긴 텍스트 예시", 7, " --")).toBe("긴 텍스 --");
  });
 
  it("음수 maxLength 시 RangeError를 던집니다", () => {
    expect(() => truncateText("테스트", -1)).toThrow(RangeError);
  });
 
  it("maxLength가 0이면 접미사만 반환합니다", () => {
    expect(truncateText("테스트", 3)).toBe("...");
  });
});

커버리지 목표와 전략

AI 테스트 생성 도구를 효과적으로 활용하려면, 명확한 커버리지 목표를 설정해야 합니다.

커버리지 유형별 목표

커버리지 유형설명권장 목표
Line Coverage(라인 커버리지)실행된 코드 라인의 비율80% 이상
Branch Coverage(분기 커버리지)실행된 조건 분기의 비율75% 이상
Function Coverage(함수 커버리지)호출된 함수의 비율90% 이상
Mutation Score(변이 점수)변이체를 잡아낸 비율 (5장 참조)70% 이상
Warning

커버리지 수치만 높다고 좋은 테스트는 아닙니다. assert 없이 코드를 실행하기만 하는 테스트는 라인 커버리지는 올리지만, 실제 결함을 잡아내지 못합니다. 5장에서 다룰 변이 테스트가 이 문제를 검증하는 효과적인 방법입니다.

커버리지 갭 분석

AI 도구가 생성한 테스트의 커버리지를 확인하고, 부족한 영역을 파악하는 것이 중요합니다.

pytest 커버리지 실행
bash
pytest --cov=src --cov-report=html --cov-report=term-missing
Jest 커버리지 실행
bash
npx jest --coverage --coverageReporters=text --coverageReporters=html

생성된 테스트의 품질 검증

AI가 생성한 테스트를 무조건 신뢰해서는 안 됩니다. 다음 기준으로 품질을 검증해야 합니다.

검증 체크리스트

  1. 의미 있는 단언(assertion)이 있는가? -- 코드를 실행하기만 하고 결과를 검증하지 않는 테스트는 무가치합니다
  2. 테스트 이름이 동작을 설명하는가? -- test_1, test_2 같은 이름은 유지보수를 어렵게 합니다
  3. 에지 케이스를 포함하는가? -- null, 빈 값, 경계값, 큰 값 등을 테스트하는지 확인합니다
  4. 독립적으로 실행되는가? -- 테스트 간 순서 의존성이 없어야 합니다
  5. 빠르게 실행되는가? -- 단위 테스트는 밀리초 단위로 실행되어야 합니다
  6. 중복이 없는가? -- 동일한 경로를 반복 테스트하는 것은 유지보수 비용만 늘립니다

흔한 문제와 대응

문제원인대응
의미 없는 단언AI가 "무엇을 검증해야 하는지" 모름비즈니스 규칙 기반으로 단언 보강
과도한 모킹실제 동작보다 구현 세부사항 테스트통합 수준의 테스트로 보완
하드코딩된 값특정 환경에서만 통과상대값 또는 팩토리 패턴 사용
부족한 에러 경로정상 경로 위주로 생성에러 시나리오 수동 추가

정리

이 장에서는 LLM 기반 단위 테스트 자동 생성의 원리와 실전 활용법을 살펴보았습니다.

핵심 내용을 정리하면 다음과 같습니다.

  • LLM은 함수의 시그니처, 본문, 의존성, 문서를 종합 분석하여 테스트를 생성합니다
  • Diffblue는 Java 엔터프라이즈, Codium/Qodo는 다중 언어 환경에 적합합니다
  • AI가 생성한 테스트는 에지 케이스와 예외 경로를 잘 포착하지만, 비즈니스 의도 검증은 부족할 수 있습니다
  • 커버리지 수치뿐 아니라 테스트의 실질적 품질(단언의 의미, 독립성, 유지보수성)을 검증해야 합니다

다음 장 미리보기

3장에서는 단위 테스트를 넘어 통합 테스트와 API 테스트 자동화를 다룹니다. API 스키마 기반 테스트 자동 생성, 계약 테스트(Contract Testing), 그리고 testcontainers와 AI를 결합한 데이터베이스 통합 테스트를 살펴봅니다.

이 글이 도움이 되셨나요?

관련 주제 더 보기

#testing#automation#quality-assurance#ai

관련 글

AI / ML

3장: 통합 테스트와 API 테스트 자동화

API 스키마 기반 테스트 자동 생성, 계약 테스트(Contract Testing), testcontainers와 AI를 결합한 데이터베이스 통합 테스트, 그리고 CI 파이프라인 통합 방법을 다룹니다.

2026년 3월 8일·18분
AI / ML

1장: AI 기반 테스트 자동화의 진화와 현재

전통적인 테스트 자동화에서 AI 기반 테스트로의 전환을 살펴봅니다. Agentic QA의 등장, 2026년 도구 생태계, 그리고 70% 이상 기업이 도입한 AI 테스트의 현황과 30-45% 효율 개선 사례를 분석합니다.

2026년 3월 4일·16분
AI / ML

4장: E2E 테스트 -- AI 에이전트 기반 자동화

자연어를 E2E 테스트로 변환하는 Momentic, testRigor, Functionize와 DOM 변경에 자동 적응하는 셀프 힐링 기능, Playwright와 AI를 결합한 실전 E2E 테스트 자동화를 다룹니다.

2026년 3월 10일·17분
이전 글1장: AI 기반 테스트 자동화의 진화와 현재
다음 글3장: 통합 테스트와 API 테스트 자동화

댓글

목차

약 19분 남음
  • 학습 목표
  • 단위 테스트 자동 생성의 원리
    • 생성 프로세스
    • 핵심 기법: 심볼릭 분석 + LLM 추론
  • 주요 도구 분석
    • Diffblue Cover -- Java 엔터프라이즈 특화
    • Codium/Qodo -- 다중 언어 지원
  • 실습: Python pytest 테스트 생성
    • 대상 함수
    • AI가 생성한 테스트
  • 실습: JavaScript Jest 테스트 생성
    • 대상 함수
    • AI 생성 테스트
  • 커버리지 목표와 전략
    • 커버리지 유형별 목표
    • 커버리지 갭 분석
  • 생성된 테스트의 품질 검증
    • 검증 체크리스트
    • 흔한 문제와 대응
  • 정리
    • 다음 장 미리보기