함수는 다 짰는데 테스트는 안 짠 상태로 커밋해 본 적이 있을 겁니다. 테스트가 중요하다는 건 알지만, 함수 하나에 케이스 대여섯 개를 손으로 적다 보면 정작 기능 개발보다 시간이 더 걸립니다. 그래서 대부분은 콘솔에 값 몇 개 찍어보고 넘어가죠. 그러다 두 달 뒤 할인 로직을 고치면서 쿠폰 계산이 조용히 깨지고, 그걸 고객 문의로 알게 됩니다. AI 단위 테스트 생성은 바로 이 구간, 즉 “쓰기는 지루하지만 없으면 나중에 크게 물리는” 코드를 몇 분으로 줄여줍니다. 이 글에서는 실제 함수 하나를 놓고 프롬프트를 두 단계로 나눠 던지는 방법, AI가 만든 테스트에서 자주 나오는 결함, 그리고 커버리지로 빈틈을 확인하는 절차까지 정리합니다.
시작 전에 정해둬야 할 세 가지
AI에게 “테스트 만들어줘”라고만 하면 프레임워크도 제각각이고, 실행조차 안 되는 코드가 나오기 쉽습니다. 요청 전에 아래 세 가지를 먼저 확정해 두면 결과물의 품질이 확연히 달라집니다.
- 프레임워크 — 파이썬이면 pytest인지 unittest인지, 자바스크립트면 Jest인지 Vitest인지 명시합니다. 안 적으면 프로젝트에 깔려 있지도 않은 걸로 만들어 옵니다.
- 테스트 대상의 범위 — 함수 하나씩 주는 게 가장 정확합니다. 파일 전체를 던지면 중요한 함수와 사소한 헬퍼에 똑같은 분량을 씁니다.
- 외부 의존성 처리 방침 — 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에게 해석시켜 원인 지점을 빠르게 좁히는 방법을 다루겠습니다.