AI로 단위 테스트 자동 생성하는 법 — 프롬프트부터 커버리지 확인까지

함수는 다 짰는데 테스트는 안 짠 상태로 커밋해 본 적이 있을 겁니다. 테스트가 중요하다는 건 알지만, 함수 하나에 케이스 대여섯 개를 손으로 적다 보면 정작 기능 개발보다 시간이 더 걸립니다. 그래서 대부분은 콘솔에 값 몇 개 찍어보고 넘어가죠. 그러다 두 달 뒤 할인 로직을 고치면서 쿠폰 계산이 조용히 깨지고, 그걸 고객 문의로 알게 됩니다. AI 단위 테스트 생성은 바로 이 구간, 즉 “쓰기는 지루하지만 없으면 나중에 크게 물리는” 코드를 몇 분으로 줄여줍니다. 이 글에서는 실제 함수 하나를 놓고 프롬프트를 두 단계로 나눠 던지는 방법, AI가 만든 테스트에서 자주 나오는 결함, 그리고 커버리지로 빈틈을 확인하는 절차까지 정리합니다.

시작 전에 정해둬야 할 세 가지

AI에게 “테스트 만들어줘”라고만 하면 프레임워크도 제각각이고, 실행조차 안 되는 코드가 나오기 쉽습니다. 요청 전에 아래 세 가지를 먼저 확정해 두면 결과물의 품질이 확연히 달라집니다.

  1. 프레임워크 — 파이썬이면 pytest인지 unittest인지, 자바스크립트면 Jest인지 Vitest인지 명시합니다. 안 적으면 프로젝트에 깔려 있지도 않은 걸로 만들어 옵니다.
  2. 테스트 대상의 범위 — 함수 하나씩 주는 게 가장 정확합니다. 파일 전체를 던지면 중요한 함수와 사소한 헬퍼에 똑같은 분량을 씁니다.
  3. 외부 의존성 처리 방침 — DB나 네트워크를 타는 함수라면 “mock을 쓸지, 아니면 그 부분은 빼고 순수 로직만 검증할지”를 지정합니다. 이걸 안 정하면 실행하려면 실제 서버가 떠 있어야 하는 테스트가 나옵니다.

예제로 쓸 함수

설명을 위해 할인 금액을 계산하는 평범한 함수를 쓰겠습니다. 분기가 세 개, 반올림이 한 번 들어가 있어서 테스트 케이스를 뽑기에 적당합니다.

# discount.py
def final_price(price, rate, coupon=0):
    """정가에 할인율과 쿠폰을 적용한 최종 결제 금액을 돌려준다."""
    if price < 0:
        raise ValueError("price must be >= 0")
    if not 0 <= rate <= 1:
        raise ValueError("rate must be between 0 and 1")

    discounted = price * (1 - rate)
    result = discounted - coupon
    return max(0, round(result))

1단계: 정상 케이스부터 만들게 한다

처음부터 “모든 케이스 다 만들어줘”라고 하면 분량은 많은데 정작 핵심 경로가 빠진 결과가 자주 나옵니다. 정상 동작 → 예외 순으로 두 번에 나눠 요청하는 편이 결과가 좋습니다. 첫 번째 프롬프트는 이렇게 씁니다.

아래는 파이썬 함수야. pytest로 단위 테스트를 만들어줘.

조건:
- 테스트 프레임워크는 pytest, 파일명은 test_discount.py
- 한 테스트 함수는 한 가지만 검증할 것
- 테스트 함수 이름은 test_<상황>_<기대결과> 형식
- mock은 쓰지 말고 순수 입출력만 검증
- 정상 동작 케이스부터 먼저 만들어줘

<여기에 함수 전문 붙여넣기>

테스트 함수 이름 규칙을 지정한 부분이 생각보다 중요합니다. 이름이 통일되어 있으면 나중에 테스트가 깨졌을 때 실패 메시지만 보고도 어떤 상황이 망가졌는지 바로 알 수 있습니다. 위 프롬프트로 받은 결과는 대략 이런 모양입니다.

# test_discount.py
import pytest
from discount import final_price

def test_할인율_0이면_정가_그대로():
    assert final_price(10000, 0) == 10000

def test_할인율_20퍼센트_적용():
    assert final_price(10000, 0.2) == 8000

def test_쿠폰이_할인가에서_차감된다():
    assert final_price(10000, 0.2, coupon=3000) == 5000

def test_음수_가격이면_ValueError():
    with pytest.raises(ValueError):
        final_price(-1, 0.2)

2단계: 경계값과 예외를 따로 요구한다

1단계 결과는 “정상적으로 잘 도는지”만 확인합니다. 실제로 버그가 숨는 곳은 경계값입니다. 할인율이 정확히 1일 때, 쿠폰이 할인가보다 클 때, 반올림이 걸리는 소수점 값 같은 것들이죠. 두 번째 프롬프트에서 이 관점을 명시적으로 나열해 줍니다.

방금 만든 테스트에 경계값과 예외 케이스를 추가해줘.

다음 관점을 각각 최소 한 개씩 커버할 것:
- 0, 1, 빈 값 같은 경계값
- 허용 범위를 벗어난 입력(음수, 1 초과)
- 결과가 음수가 될 수 있는 조합
- 소수점이 생겨 반올림이 일어나는 값

각 테스트에 왜 이 케이스가 필요한지 한 줄 주석을 달아줘.

관점을 항목으로 나눠 적는 이유는 AI가 한두 가지 유형에만 몰리는 걸 막기 위해서입니다. 그냥 “엣지 케이스도 추가해줘”라고 하면 예외 발생 테스트만 다섯 개를 만들고 반올림은 건드리지 않는 식으로 편향됩니다. 결과물은 이렇습니다.

# 쿠폰이 할인가보다 크면 0으로 막히는지 (환불 사고 방지)
def test_쿠폰이_할인가보다_크면_0():
    assert final_price(10000, 0.5, coupon=99999) == 0

# 할인율 경계값 1.0은 허용되어야 한다
def test_할인율_1이면_0원():
    assert final_price(10000, 1) == 0

# 할인율 1 초과는 거부되어야 한다
def test_할인율_1_초과면_ValueError():
    with pytest.raises(ValueError):
        final_price(10000, 1.5)

# 소수점이 생기는 조합에서 반올림 규칙 고정
@pytest.mark.parametrize("price,rate,expected", [
    (1000, 0.333, 667),
    (999, 0.5, 500),
])
def test_반올림_규칙(price, rate, expected):
    assert final_price(price, rate) == expected

마지막 parametrize 블록처럼 입력·기댓값 쌍을 표로 묶어달라고 하면 케이스를 나중에 늘리기 쉬워집니다. 새 값이 필요할 때 줄 하나만 추가하면 되니까요.

AI가 만든 테스트에서 자주 나오는 문제

생성된 테스트를 그대로 커밋하면 안 됩니다. 테스트 코드의 버그는 일반 코드의 버그보다 악질인데, 통과한다는 이유로 아무도 다시 안 들여다보기 때문입니다. 실제로 반복해서 마주치는 유형은 다음과 같습니다.

유형증상확인 방법
기댓값을 구현에서 역산함수가 8000을 뱉으니 기댓값도 8000으로 적음. 로직이 틀려도 통과기댓값을 손으로 계산해 한 번 대조
항상 통과하는 단언assert result is not None 처럼 사실상 아무것도 검증하지 않음함수 반환값을 일부러 바꿔보고 실패하는지 확인
존재하지 않는 API 호출실제로 없는 인자나 메서드를 씀pytest 실행 시 즉시 에러로 드러남
중복 케이스입력만 다르고 같은 분기를 세 번 검증커버리지 리포트로 확인
테스트 간 상태 공유앞 테스트가 만든 파일·전역변수에 의존해 실행 순서가 바뀌면 깨짐테스트 순서를 섞어 실행해 본다

가장 확실한 검증법은 일부러 코드를 망가뜨려 보는 것입니다. 함수 안의 부등호를 반대로 바꾸거나 상수를 1만큼 틀리게 고친 뒤 테스트를 돌려보세요. 그래도 전부 통과한다면 그 테스트는 아무 일도 하고 있지 않은 겁니다.

커버리지로 빈틈 확인하기

눈으로 훑는 대신 도구로 확인하는 편이 빠릅니다. 파이썬은 pytest-cov를 붙이면 어느 줄이 한 번도 실행되지 않았는지 알려줍니다.

pip install pytest pytest-cov

pytest --cov=discount --cov-report=term-missing

--cov-report=term-missing 옵션이 핵심입니다. 이걸 붙여야 퍼센트만이 아니라 실행되지 않은 줄 번호까지 나옵니다.

Name          Stmts   Miss  Cover   Missing
-------------------------------------------
discount.py       8      1    88%   7
-------------------------------------------
TOTAL             8      1    88%

위 결과에서 7번 줄이 한 번도 실행되지 않았다는 뜻입니다. 할인율 범위를 검사하는 분기죠. 그러면 그 줄 번호를 그대로 AI에게 넘겨서 “7번 줄을 지나가는 테스트를 추가해줘”라고 요청하면 됩니다. 막연히 “더 만들어줘”라고 할 때보다 훨씬 정확한 케이스가 나옵니다.

다만 커버리지 100%가 버그 0을 뜻하지는 않습니다. 모든 줄을 거쳤다는 것과 모든 경우를 검증했다는 건 다릅니다. 커버리지는 “확실히 빠진 곳”을 찾는 용도이지 품질 점수가 아닙니다.

주의할 점

  • 사내 코드를 그대로 붙여넣기 전에 회사 정책을 확인하세요. 비즈니스 로직이 담긴 함수는 그 자체로 내부 자료입니다. 애매하면 변수명과 상수를 바꿔 구조만 남긴 형태로 주는 방법도 있습니다.
  • API 키, 실제 계정, 운영 DB 접속 정보가 코드에 섞여 있지 않은지 보고 넘기세요. 테스트 대상 함수 위아래에 설정값이 붙어 있는 경우가 흔합니다.
  • 테스트가 통과했다고 기능이 맞는다는 뜻은 아닙니다. AI는 구현을 보고 기댓값을 적으므로, 구현 자체가 요구사항과 다르면 그 오해를 그대로 테스트로 굳혀 버립니다. 기댓값 몇 개는 반드시 요구사항 문서나 기획서를 보고 직접 확인하세요.
  • 한 번에 파일 전체를 넘기지 마세요. 맥락이 길어질수록 각 함수에 배정되는 주의가 줄어듭니다. 함수 단위로 끊어 요청하는 편이 총 시간도 짧습니다.
  • 생성된 테스트는 반드시 한 번 실행한 뒤 커밋하세요. import 경로가 틀려 아예 수집조차 안 되는 경우가 드물지 않습니다.

마무리

전부 다 도입할 필요는 없습니다. 지금 작업 중인 파일에서 가장 손대기 무서운 함수 하나를 골라 위 두 단계 프롬프트만 돌려보세요. 테스트 파일 하나 만드는 데 5분이면 충분하고, 그 함수를 다음에 고칠 때 들이는 확인 시간이 절반 이하로 줄어듭니다. 분기가 많고 돈이나 날짜 계산이 얽힌 함수일수록 효과가 큽니다.

다음 글에서는 오류 메시지와 스택 트레이스를 AI에게 해석시켜 원인 지점을 빠르게 좁히는 방법을 다루겠습니다.

댓글 남기기