
AI에게 테스트 작성을 시켰을 때, 커버리지(Coverage)는 올라갔는데 품질은 올라가지 않는 이유 — “테스트의 품질”을 뮤테이션
요약
AI가 작성한 테스트는 코드 커버리지는 높여주지만, 실제 버그를 잡아내는 품질은 낮을 수 있습니다. 본 기사는 뮤테이션 테스트를 통해 테스트의 실질적인 품질을 측정하고, AI를 활용해 신뢰할 수 있는 테스트를 작성하는 방법을 다룹니다.
핵심 포인트
- 높은 코드 커버리지가 반드시 높은 테스트 품질을 보장하지 않음
- AI 에이전트가 작성한 테스트의 약 80%가 명시적 오라클이 없는 상태
- 뮤테이션 테스트를 통해 테스트의 유효성을 검증하는 방법 제시
- AI 테스트 작성 시 올바른 프롬프트 전략과 주의사항 안내
AI에게 테스트 작성을 시키면, 정말 순식간이죠.
"이 코드의 테스트를 작성해줘"라고 부탁하기만 하면, 파일이 툭 튀어나옵니다. 실행하면 테스트 러너(Test Runner)가 초록색 바를 보여줍니다. 커버리지(Coverage)도 어제까지 40%였던 것이 단번에 85%가 됩니다. 왠지 제대로 보호받고 있다는 기분이 듭니다.
하지만, 잠시 멈춰서 생각해보고 싶습니다.
그 초록색 바는, "코드가 올바르다"는 신호일까요? 아니면, "코드가 동작했다"는 신호일까요?
이 지점이 어긋나면 꽤 무서운 일이 일어납니다. 테스트는 있는데, 버그가 운영 환경까지 빠져나갑니다. 나중에 원인을 추적해보면, 테스트 자체가 "실행은 하고 있지만, 아무것도 확인하지 않고 있었다". 초록색 바에게 계속 거짓말을 당하고 있었던 것과 같은 상태입니다.
이것은 제 개인적인 감상만으로 말하는 것이 아닙니다. 2026년 6월에 발표된 논문(arXiv:2606.18168 「All Smoke, No Alarm」, IEEE AITest 2026 채택)이 꽤 충격적인 수치를 내놓았습니다.
OpenAI Codex / GitHub Copilot / Devin / Cursor / Claude Code라는 5개의 AI 에이전트가 작성한
86,156건의 테스트 파일 변경 사항을 2,807개의 리포지토리(Repository)에 걸쳐 조사한 결과, 80.2%가 "약하거나 명시적인 오라클(Oracle, 즉 정답 확인)이 없는" 테스트였습니다.
제목인 「All Smoke, No Alarm(연기는 나는데, 경보는 울리지 않는다)」이 이미 본질을 꿰뚫고 있다고 생각합니다. 연기는 모락모락 피어오르고 있습니다. 하지만 정작 중요한 화재 경보기는 울리지 않습니다. 테스트는 잔뜩 있습니다. 하지만 버그가 발생해도 울려주지 않습니다.
이 기사는 그 "울리지 않는 테스트"를 구별해내고, 제대로 울리는 테스트로 바꾸어 나가는 이야기에 관한 것입니다. 구체적으로는 다음과 같은 순서로 진행됩니다.
- 왜 AI가 작성한 테스트는 "동작은 하지만 보호하지 못하는가", 그 3가지 고장 유형
- 커버리지(Coverage)라는 숫자가 왜 "테스트의 품질"을 측정할 수 없는가
- 테스트의 품질을 정면으로 측정하는 방법 = 뮤테이션 테스트 (Mutation Test)
- AI에게 테스트 작성을 시킬 때의 올바른 순서와 프롬프트 3가지
- 함정과 안전하게 사용하기 위한 기준선
AI 개발에 아직 익숙하지 않은 분들도 뒤처지지 않도록, 용어는 등장할 때마다 풀어서 설명하겠습니다. 코드도 Python과 TypeScript로 바로 시도해 볼 수 있는 형태로 제공하겠습니다.
갑자기 "뮤테이션이", "오라클이"라고 하면 힘들겠죠. 그래서 먼저 등장인물을 정리하겠습니다. 모두 화재 경보기 비유로 통일하겠습니다.
테스트 (Unit Test, 유닛 테스트): 특정 코드에 값을 넣었을 때 나온 결과가 올바른지 자동으로 체크하는 작은 프로그램. 화재 경보기 그 자체라고 생각하세요.
어설션 (Assertion, 단언): 테스트 안에서 "이 부분은 이렇게 되어 있어야 한다"라고 단언하는 한 줄. assert 결과 == 100 같은 것입니다. 이것이 화재 경보기의 "울리는 부분"입니다. 어설션이 없는 테스트는 센서는 있지만 소리가 나지 않는 경보기입니다.
커버리지 (Coverage): 테스트를 실행했을 때, 제품 코드의 몇 행(몇 분기)이 실행되었는지의 비율. "연기가 방에 도달했는가"만을 세고 있는 이미지입니다. 도달했는지 여부와 경보가 울렸는지는 별개의 문제입니다.
오라클 (Oracle): 해당 테스트에 있어 "정답은 무엇인가"를 알고 있는 사람 또는 메커니즘. 테스트의 신(神) 같은 느낌이지만, 요컨대 **"기대값은 이것이다, 라고 결정하는 근거"**입니다. apply_discount(1000, 0.2)의 정답은 800이라고 결정하는 것이 오라클입니다. 이 부분이 오늘 가장 중요한 단어입니다.
뮤턴트 (Mutant): 코드에 일부러 심어놓은 작은 버그. a + b를 a - b로 바꾸거나, >를 >=로 바꾸는 것과 같은 문자 하나 수준의 개변입니다.
뮤테이션 테스트 (Mutation Test): 그 뮤턴트(의도적인 버그)를 넣었을 때, 테스트가 실패하는지를 확인하는 기법. 실패하면 "버그를 잡았다 = kill (죽였다)", 실패하지 않으면 "놓쳤다 = survived (살아남았다)".
뮤테이션 스코어 (Mutation Score): 죽인 뮤턴트 ÷ 넣은 뮤턴트의 비율. 이것이 "화재 경보기, 제대로 울립니까?"를 테스트하는 것입니다.
여기서 딱 하나, 가슴 깊이 새겨두었으면 하는 것이 있습니다.
코드가 실행되는 것(커버리지)과, 오류를 검지할 수 있는 것(어설션)은 전혀 다릅니다.
연기가 방 안으로 들어와도 경보기가 울리지 않는다면 아무 의미가 없습니다. 커버리지(Coverage) 85%는 단지 "연기가 방의 85%에 도달했다"는 것일 뿐, "화재 시 울린다"는 보장은 어디에도 없습니다.
그렇다면 AI가 작성하는 테스트는 어떻게 망가질까요? 제가 관찰한 바로는 크게 세 가지 패턴으로 나뉩니다.
가장 흔한 패턴은 이것입니다. 코드는 호출하지만, 결과를 단언(Assert)하지 않습니다.
사실 이것은 AI 이전부터 있었던 오래된 이야기로, 마틴 파울러(Martin Fowler)가 "Assertion Free Testing (어설션이 없는 테스트)"라는 유명한 에세이에서 언급한 바 있습니다. 어떤 프로젝트가 "모든 public 함수에 JUnit 테스트가 있습니다"라며 초록색 바(Green bar)를 자랑스럽게 보여주었지만, 테스트 안에 어설션(Assertion)이 단 하나도 없었다는 일화입니다. 어설션이 없다면 테스트는 예외(Exception)로 떨어지지 않는 한 영원히 초록색입니다. 커버리지 100%도 만들어낼 수 있습니다.
# 약한 테스트: 호출만 함. 아무것도 단언하지 않음 (= 울리지 않는 경보기)
def test_apply_discount_weak():
result = apply_discount(1000, 0.2)
...
apply_discount가 800을 반환하든 7을 반환하든, 이 테스트는 통과합니다. 연기 센서는 있지만, 소리를 내는 배선이 연결되지 않은 상태와 같습니다.
# 강한 테스트: 무엇이 옳은지 단언함 (= 울리는 경보기)
def test_apply_discount_strong():
assert apply_discount(1000, 0.2) == 800
...
이것이 가장 까다로운 유형으로, 겉보기에는 멀쩡해 보이지만 알맹이가 비어 있는 경우입니다.
# 동어반복(Tautology): 구현 그 자체를 기대값으로 사용함
def test_calc_total_tautology():
cart = [{"price": 100, "qty": 3}]
...
이것은 절대 실패하지 않습니다. 왜냐하면 "calc_total의 결과는 calc_total의 결과와 같다"라고 말하고 있을 뿐이기 때문입니다. 만약 calc_total에 "수량을 곱하는 것을 잊어버리는" 버그가 있더라도, 버그가 포함된 답이 그대로 "정답"이 됩니다. 버그까지 통째로 고정해 버리는 것입니다.
왜 AI가 이런 실수를 자주 할까요? 이유는 간단합니다. AI는 "사양(Specification, 마땅히 이래야 하는 것)"이 아니라 "코드(Implementation, 지금 이렇게 동작하고 있는 것)"를 보고 테스트를 작성하기 때문입니다. 눈앞에 있는 코드의 동작을 관찰하고, 그것을 그대로 기대값으로 삼습니다. 그래서 코드가 틀렸더라도 그 오류까지 그대로 복사해 버립니다.
실제로 2024년 말 연구(arXiv:2412.14137)에서도, LLM 기반 테스트 생성 도구의 오라클(Oracle)이 "잘못된 동작을 그대로 인정하거나, 버그를 찾아내는 테스트를 기각하는" 경향이 있다고 보고되었습니다. 정답을 모르는 상태에서 정답을 쓰려고 하면 이렇게 됩니다.
세 번째는 정상계(Happy path)만 작성하고, 이상계(Edge case)나 경계값(Boundary value)을 작성하지 않는 패턴입니다.
# 해피 패스(Happy path)만 작성: 기분 좋은 입력값만 테스트함
def test_divide_happy():
assert divide(10, 2) == 5
divide(10, 0) (0으로 나누기)는요? 마이너스는요? 문자열이 들어오면요? ——버그는 대개 "기분 좋은 입력"의 바깥쪽에 숨어 있습니다. 경계(0, 빈 배열, 최댓값, 최솟값)와 이상(잘못된 타입, None, 예외)이야말로 경보기를 설치해야 할 곳인데, AI는 요청하지 않으면 대개 중간의 안전지대만 테스트해 줍니다.
이 세 가지의 공통점은 **"코드는 실행하고 있다(그래서 커버리지는 올라간다)"는 점과 "정확성은 확인하지 않는다(그래서 지켜내지 못한다)"**는 점입니다.
여기서 아까 느꼈던 위화감을 정면으로 언어화해 보겠습니다.
커버리지는 "실행된 행(Line)"을 세는 지표입니다. 어설션이 강한지 약한지는 1밀리미터도 고려하지 않습니다. 그래서 약한 테스트라도 커버리지는 아무렇지 않게 올라갑니다. 오히려 AI는 "커버리지를 높여달라"는 요청을 받으면, 어설션을 늘리는 대신 "어쨌든 코드를 통과하는 입력"을 늘리는 경향이 있습니다. 이것이 숫자만 번지르르한 "장식용 테스트"가 만들어지는 메커니즘입니다.
Meta(Facebook)의 엔지니어링 블로그(2025년 9월, 뮤테이션 테스트의 대규모 실용화에 관한 기사)도 바로 이 지점을 지적하고 있습니다.
구조적인 커버리지 기준(Statement Coverage나 Branch Coverage)은 코드의 행이 실행되었는지 여부만을 나타냅니다. 행이 실행되었다고 해서 반드시 버그를 검지할 수 있는 것은 아닙니다.
즉, 다음과 같은 상황이 흔히 발생합니다.
커버리지 95% / 하지만 실제로 버그를 잡아내는 능력(Mutation Score)은 30%
숫자는 95점인데 실력은 30점인 셈입니다. 화재 경보기가 95%의 방에 설치되어 있는데, 불을 붙여도 3할밖에 울리지 않는 것과 같습니다. 이를 두고 "품질 보증이 되고 있다"라고 말하기에는 무리가 있습니다.
오해하지 말아야 할 점은, 커버리지가 무의미하다는 뜻은 아니라는 것입니다. 커버리지는 "아직 한 번도 건드려지지 않은 곳"을 알려주는 최소한의 위생 점검(Hygiene Check)으로서는 유효합니다. 파울러(Fowler)도 보충 설명으로 "Assertion이 없더라도 null 참조와 같은 런타임 에러(Runtime Error)는 발견될 수 있다"라고 말한 바 있습니다.
다만, 커버리지는 품질의 "하한"을 보는 것이지, 품질의 "높이"를 보증하는 것이 아닙니다. 이 부분을 혼동한 채 "커버리지 80% 달성, 테스트 완료"라고 진행하다가는 조용히 사고를 치게 됩니다.
그렇다면 테스트의 "좋고 나쁨"을 어떻게 측정할까요? 여기서 주인공이 등장합니다. 바로 **뮤테이션 테스트(Mutation Test)**입니다.
생각하는 방식은 허탈할 정도로 단순합니다.
- 제품 코드에 작은 버그(Mutant)를 일부러 하나 넣습니다. 예:
a + b$\rightarrow$a - b. - 그 상태에서 테스트를 전부 실행합니다.
- 어떤 테스트라도 실패한다면, 그 버그는 잡힌 것 $=$ Kill.
- 모든 테스트가 통과해 버린다면, 버그를 놓친 것 $=$ Survived (생존).
- 이것을 수십, 수백 개의 Mutant로 반복하여
Kill $\div$ 전체$=$ **뮤테이션 스코어(Mutation Score)**를 산출합니다.
요컨대, **"화재 경보기 점검을 위해 일부러 연기를 피워보는 것"**과 같습니다. 울리면 합격, 울리지 않으면 그 경보기(테스트)는 장식에 불과했다는 사실이 드러납니다. 그래서 저는 뮤테이션 테스트를 "테스트를 위한 테스트"라고 부릅니다.
Meta의 블로그에서도 뮤테이션 테스트를 "(수십 년간의 연구가 일관되게 보여준) 가장 강력한 테스트 형태"라고 표현했습니다. 동시에 "뮤테이션 테스트는 그것 단독으로는 성립하지 않는다. 먼저 테스트가 존재하지 않으면 시작조차 할 수 없다"라고도 했습니다. 어디까지나 이미 존재하는 테스트의 강도를 측정하여, 약한 Assertion을 찾아내는 장치인 것입니다.
| 언어 | 도구 | 한 줄 요약 |
|---|---|---|
| Python | mutmut | mutmut run으로 실행, mutmut browse로 생존 확인. pytest와 궁합이 좋음 |
| JS / TS / .NET | Stryker | 뮤테이션 종류가 풍부함, Jest / Vitest 대응, 변경분만 실행하는 incremental 모드 있음 |
| Java | PIT | JVM 계열의 표준 |
방금 말한 "약한 테스트"에 mutmut을 적용한다고 가정하고 살펴보겠습니다. 대상은 다음과 같은 코드라고 합시다.
# shop.py
def apply_discount(price: int, rate: float) -> int:
if rate < 0 or rate > 1:
...
mutmut을 실행합니다.
# 설치 후, 대상과 테스트 명령을 지정하여 실행
pip install mutmut
mutmut run --paths-to-mutate shop.py --runner "pytest -q"
...
만약 테스트가 "약한 방식 ①"(assert result is not None)뿐이라면, 다음과 같은 보고가 돌아옵니다.
survived: rate < 0 $\rightarrow$ rate <= 0 으로 바꿔도 테스트가 모두 통과함
survived: price - price * rate $\rightarrow$ price + price * rate 으로 바꿔도 통과함
"울리지 않았다"라고 알려주는 것입니다. 여기서 비로소 자신의 테스트가 경보기로서 기능하지 못했다는 것을 알게 됩니다. 그러면 Assertion을 추가하고 다시 실행하는 것입니다.
def test_apply_discount_kills_mutants():
assert apply_discount(1000, 0.2) == 800 # 「-」를 「+」로 바꾸면 1200이 되어 실패함 → kill
assert apply_discount(1000, 0) == 1000
...
다시 실행하여 뮤테이션 스코어 (Mutation Score)가 올라간다면, 그것은 "경보기가 제대로 울리게 되었다"는 눈에 보이는 진전입니다. 커버리지 (Coverage)와 달리 속임수가 통하지 않습니다.
TypeScript에서도 방식은 같습니다. 설정 파일을 두고 실행하기만 하면 됩니다.
// stryker.conf.js
/** @type {import('@stryker-mutator/api/core').PartialStrykerOptions} */
module.exports = {
...
}
npx stryker run
# → killed / survived / no coverage를 색상으로 구분한 리포트가 출력됨
thresholds.break를 설정해 두면, "뮤테이션 스코어가 일정 수준 이하로 떨어지면 빌드를 실패시킨다"를 구현할 수 있습니다. 커버리지 게이트 (Coverage Gate)보다 한 단계 더 높은 세계입니다.
자, 여기까지 오면 하나의 벽에 부딪힙니다.
뮤턴트 (Mutant)를 삽입하고 실행하는 것은 기계가 잘합니다. Assertion (단언)의 "형태"를 쓰는 것도 AI가 잘합니다. 하지만 "apply_discount(1000, 0.2)의 정답은 정말로 800인가?"를 결정하는 것은 누구의 일일까요?
이것이 소프트웨어 테스트 세계에서 예전부터 "테스트 오라클 문제 (Test Oracle Problem)"라고 불려온 가장 어려운 부분입니다. 테스트의 메커니즘 (How)은 얼마든지 자동화할 수 있습니다. 하지만 "무엇이 옳은 답인가 (What / Why)"는 사양(Specification)과 의도를 알고 있는 인간만이 결정할 수 있습니다.
여기서 AI에게 "정답도 추측해줘"라고 시키면, 파괴 방식 ②인 동어반복 (Tautology)으로 되돌아가게 됩니다. AI는 눈앞의 코드를 보고 "아마 이것이 정답일 것"이라며 채워 넣기 때문에, 코드가 틀려 있으면 틀린 것이 정답이 되어버립니다. 그러므로,
오라클 (정답의 근거)은 인간이 제공한다. 테스트의 메커니즘은 AI에게 쓰게 한다.
이 역할 분담이 오늘 가장 강조하고 싶은 한 줄입니다. 표로 정리하면 다음과 같습니다.
| 공정 | 인간 (What / Why) | AI (How) |
|---|---|---|
| 지키고 싶은 동작을 결정 | ◎ 사양·의도·정답을 결정 | △ 후보 제시 보조 |
| ... |
AI는 우수한 "테스트 장인"입니다. 하지만 무엇을 지켜야 하는지에 대한 설계도는 인간이 그립니다. 장인에게 설계까지 통째로 맡겨버리면, 훌륭하지만 아무도 살 수 없는 집이 지어지는 것과 같습니다.
역할 분담이 결정되었다면 다음은 순서입니다. 저는 이것을 "측정 주도 품질 루프 (Measurement-Driven Quality Loop)"라고 부릅니다. 5단계로 구성됩니다.
- 인간이 지키고 싶은 동작과 경계값·예외 케이스를 먼저 결정한다. 이것이 오라클의 씨앗입니다. "할인율은 0~1이며 그 외에는 예외 처리", "빈 장바구니는 0원"처럼 코드를 보기 전에 언어화합니다.
- AI에게 테스트를 쓰게 한다. 단, "구현부로부터가 아니라, 이 사양으로부터 작성해줘"라고 전달합니다.
- Assertion (단언)의 강도를 리뷰한다. "이 테스트는 무엇을 검증하지 못하고 있는가?"를 AI와 인간 모두에게 묻습니다.
- 뮤테이션 테스트 (Mutation Test)로 채점한다. survived (생존)가 나타나면 그곳이 경보기의 구멍입니다.
- CI 게이트를 커버리지가 아닌 뮤테이션 스코어로 설정한다. 단, 모든 곳에 실행하면 무거우므로 변경이 있는 파일이나 중요 모듈만으로 범위를 좁힙니다.
5단계를 GitHub Actions로 대략 작성하면 다음과 같습니다.
# .github/workflows/mutation-gate.yml
name: mutation-gate
on: [pull_request]
...
포인트는 임계값 (얼마나 많은 뮤턴트가 생존했을 때 실패로 간주할지)은 인간이 결정한다는 것과, 변경분만으로 범위를 좁혀 비용을 억제한다는 것입니다. 갑자기 전사 코드에 대해 100%를 요구하면 계산 시간 때문에 마비될 것입니다. 우선 "돈, 안전, 데이터와 관련된 중요 모듈"부터 시작하는 것이 현실적입니다.
여기서부터는 AI에게 요청할 때 사용하는 실물 예시입니다. 세 가지 모두 최종 판단은 인간이 한다는 전제로 작성되었습니다. 복사하여 더미 부분을 자신의 코드로 교체해 사용하세요.
당신은 테스트 설계 리뷰어입니다.
다음의 「함수 명세(Specification)」만을 근거로, 테스트해야 할 케이스를 찾아내세요.
※ 구현 코드는 보지 말고, 명세로부터 생각할 것.
...
명세로부터 생각하게 만드는 것이 핵심입니다. 이를 통해 깨지는 방식 ③(해피 패스(Happy Path)만 테스트하는 경우)을 상당히 방어할 수 있습니다.
다음 명세와 테스트 케이스 표를 바탕으로, pytest의 테스트를 작성해 주세요.
# 엄수 규칙
- 기대값은 제가 표로 제공한 값만 사용할 것.
...
"구현을 호출하여 기대값을 만들지 마라"라고 명시함으로써, 깨지는 방식 ②(동어반복(Tautology))를 구조적으로 방지합니다. "각 assert가 어떤 버그를 잡아내는지 작성하라"고 말하면, 어설션(Assertion)이 자연스럽게 강력해집니다.
다음 테스트 코드와, 뮤테이션 테스트(Mutation Test)에서 "살아남은 뮤턴트(Survived Mutant)" 목록을 전달합니다.
당신의 업무는 테스트의 "구멍"을 찾는 것입니다.
# 수행할 작업
...
여기서도 AI에게 "단정 짓지 않게" 하는 것이 중요합니다. 등가 뮤턴트(Equivalent Mutant)인지 여부는 인간이 결정합니다. AI는 후보와 이유를 제시하는 역할에 집중하게 합니다.
편리한 도구일수록 대충 사용하면 사고가 납니다. 자주 발생하는 함정 6가지와 뮤테이션 테스트 특유의 주의사항을 정리해 둡니다.
| # | 함정 | 결과 |
|---|---|---|
| 1 | 커버리지(Coverage) 100%에 안심함 | 실행되었을 뿐임. 울리지 않는 경보기가 가득 찬 방과 같음 |
| ... |
그리고 뮤테이션 테스트에도 약점이 있습니다. 이를 모르고 사용하면 오히려 휘둘리게 됩니다.
등가 뮤턴트 (Equivalent Mutant): 코드를 변경했음에도 동작이 전혀 변하지 않는 뮤턴트가 있습니다 (예: 절대 통과하지 않는 분기 내부의 변경). 이는 "죽일 수 없는" 것이 정상임에도, 리포트상에는 survived로 남습니다. 모두 0으로 만들려고 하면, 이 환상 속의 버그를 잡느라 시간을 낭비하게 됩니다. "이것은 등가이다"라고 인간이 판단하여 제외해도 좋습니다. -
계산 비용이 높음: 뮤턴트를 수백 개 만들어서 매번 테스트를 돌리기 때문에 무겁습니다. 따라서 변경분이나 중요 모듈로 범위를 좁히는 것이 철칙입니다. 모든 코드에 매번 전체를 적용하는 것이 아닙니다. -
스코어는 만능이 아님: 뮤테이션 스코어(Mutation Score) 80~90%대는 매우 좋은 지표이지만, 100%라고 해서 버그가 없다는 증명은 아닙니다. 프로퍼티 기반 테스트(Property-based Test)나 통합 테스트, 인간의 리뷰와 조합해서 사용해야 합니다.
철수 라인(Retreat Line)도 정해 두어야 합니다.
스코어가 한계에 다다르면 추적을 멈춘다. 90%를 넘는 것을 95%로 만들기 위한 노력보다, 다른 모듈의 50%를 80%로 만드는 것이 가치가 높은 경우가 많습니다. -
등가 뮤턴트투성이인 모듈은 뮤테이션 대상에서 제외한다. 무리하게 쫓지 않습니다. -
중요도가 낮은 코드는 최소한의 커버리지로 충분하다. 모든 곳에 동일한 엄격함을 요구하지 않습니다.
마지막으로, 안전에 관한 이야기를 조금만 하겠습니다. 테스트 관련 작업은 은근히 사고가 나기 쉬운 영역입니다.
테스트 데이터에 실제 개인정보나 비밀 정보를 넣지 않는다. AI에게 테스트를 작성하게 할 때 실데이터를 그대로 붙여넣으면 "AI에게 전달함 = 외부로 유출함"이 됩니다. 성함, 이메일, API 키, 운영 데이터는 반드시 더미(예: user@example.com, test-token-xxxx 등)로 교체하십시오. -
테스트가 되돌릴 수 없는(Irreversible) 작업을 실제 환경을 향해 수행하지 않도록 한다. 운영 DB 쓰기, 결제, 메일 발송, 데이터 삭제 등 이러한 "되돌릴 수 없는 작업"은 테스트에서 모크(Mock)나 샌드박스(Sandbox)로 격리해야 합니다. AI가 생성한 테스트가 실수로 운영 엔드포인트를 호출하고 있지는 않은지 인간이 반드시 확인해야 합니다. -
외부에서 온 텍스트는 "데이터"로 취급한다. 이슈(Issue)나 로그를 그대로 프롬프트에 섞으면, 그 안에 숨겨진 지시에 AI가 반응할 수 있습니다 (프롬프트 인젝션(Prompt Injection)). 외부 입력은 명령이 아닌 데이터로서 감싸서 전달하십시오.
이러한 부분은 테스트의 좋고 나쁨을 따지기 이전에 "문단속"입니다. 좋은 테스트를 작성하기 전에 먼저 자물쇠를 채워 두십시오.
여기까지 함께해 주셔서 감사합니다.
마지막으로 하나만 더 말씀드리겠습니다. 제가 이 주제를 중요하게 생각하는 이유는, 테스트란 결국 "미래의 자신과의 약속"이기 때문입니다.
약한 테스트는 초록색 바(Green Bar)를 보여주며 "괜찮아"라고 말해줍니다. 하지만 그것은 아무것도 확인하지 않은 "텅 빈 안도감"입니다. 반년 뒤, 운영 환경에서 버그가 발생했을 때 그 초록색 바는 아무것도 지켜주지 못합니다. 오히려 "테스트가 있었는데 왜"라며 과거의 자신을 원망하게 만들 뿐입니다.
하지만 여기서 과거의 자신을 탓하는 것은 조금 방향이 다르다고 생각합니다. 약한 테스트를 작성해 버린 것은, 단지 '어설션(Assertion)의 강도'라는 관점을 아직 갖지 못했을 뿐입니다.
강한 테스트는 반대입니다. 버그가 침투하려는 순간, 경보 장치처럼 울려줍니다. "여기, 바뀌었어"라고 알려줍니다. 그것은 미래의 자신에게 남기는 쪽지이며, **"그때 제대로 울리도록 설정해 줘서 고마워요"**라고 말할 수 있는 선물이 됩니다. 비난하기 위한 도구가 아니라, 배려를 위한 도구인 것입니다.
그리고 제대로 울리는 테스트 군은 내버려 두어도 사라지지 않습니다. 코드를 다시 작성할 때마다 몇 번이고 지켜줍니다. 이것은 쌓여가는 자산입니다. 작성하는 것은 AI(How)여도 괜찮습니다. 하지만 "무엇을 지킬 것인가(What / Why)"를 결정하는 능력은 AI에게 빼앗기지 않습니다. 오히려 AI 시대에 가장 가치가 높아질 기술이라고 생각합니다.
그러니 만약 오늘 딱 하나만 시도해 본다면, 이것을 해보세요.
가장 중요한 함수를 하나 선택하기 (돈, 데이터, 안전과 관련된 것).
mutmut 또는 Stryker를 딱 한 번만 돌려보기. 점수가 예상보다 낮아서 아마 놀라게 될 것입니다.
살아남은 뮤턴트(Mutant)를 하나 골라, 그것을 죽일 수 있는 어설션(Assertion)을 한 줄 추가하기.
CI에 변경 사항에 대해서만 수행하는 뮤테이션 체크(Mutation Check)를 하나 추가하기.
단 한 줄의 어설션이 반년 뒤의 당신을 구할지도 모릅니다. 초록색 바를 단순한 색깔이 아닌, 제대로 된 의미를 담은 약속으로 바꾸어 나갑시다.
그럼, 오늘도 안전하게 작업하시길.
AI 자동 생성 콘텐츠
본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기