간단히 말하자면: 녹색이라고 해서 믿어서는 안 됩니다. AI는 코드의 현재 동작을 단언하는 테스트를 작성합니다. 여기에는 버그도 포함됩니다. 변이 테스트(mutation testing)를 통해 확신을 얻으세요. 코드를 의도적으로 변경하여 테스트가 실패하는지 확인해야 합니다. 녹색으로 남아있는 것은 당신에게 거짓말하고 있는 것입니다.
지난달, 저는 Claude에게 가격 책정 함수에 대한 테스트 파일을 생성하도록 했습니다. 40개의 테스트 모두 통과했고, 커버리지는 94%였으며, 90초가 걸렸습니다. 마치 선물 같았습니다. 그러다 할인 등급에서 <=를 <로 변경했는데 — 제가 실제로 배포했을 법한 전형적인 오프-바이-원(off-by-one) 오류였습니다 — 모든 40개 테스트는 여전히 통과했습니다. 이 스위트(suite)는 가격 책정 로직을 테스트하고 있는 것이 아니었습니다. 그것은 그 로직을 설명하고 있을 뿐이었습니다.
그것이 함정이며, 생성된 테스트에만 국한되는 문제입니다.
AI 테스트가 깨진 코드에 통과하는 이유
테스트를 작성할 때는 보통 “이것이 어떻게 해야 하는지”에서 시작합니다. 하지만 모델이 테스트를 작성할 때는 구현을 읽고, 코드가 _무엇을 하는지_와 일치하는 단언(assertion)을 작성합니다. 이 둘은 같은 작업이 아닙니다. 만약 함수에 버그가 있다면, 모델은 그 버그를 그대로 고정하고 녹색으로 표시하는 테스트를 충실하게 작성합니다.
반복적으로 나타나는 세 가지 실패 패턴이 있습니다:
- 미러 테스트(The mirror test). 동일한 코드 경로에서 출력을 재도출함으로써
result == function(input)을 단언합니다. 이는 동어반복적 진술(tautology)입니다. 결코 실패할 수 없습니다.
- 단언이 아닌 단언(The assertion that isn't).
assert response is not None, assert len(result) > 0 등. 기술적으로는 단언이지만, 거의 아무것도 잡아내지 못합니다. 모델들은 이것을 좋아하는데, 왜냐하면 항상 통과하기 때문입니다.
- 커버리지 연극(Coverage theater). 라인이 실행되었기 때문에 커버리지가 이를 계산하지만, 그 결과 값에 대해 확인하는 것은 아무것도 없습니다. 94%의 커버리지, 0%의 신뢰도입니다.
커버리지 도구는 이 중 어느 것도 볼 수 없습니다. 라인은 단지 실행되는 순간 “커버되었다”고 간주됩니다. 단 하나의 단언조차 그 결과에 신경 쓰는지 여부와 관계없이 말이죠. 이것이 제가 몇 년 전에 커버리지를 품질 신호로 취급하는 것을 멈춘 이유이며, AI가 생성한 스위트에서는 특히 위험하기 때문에 그렇습니다. 잘못된 것을 측정하는 큰 녹색 숫자를 보여주기 때문입니다.
당신의 테스트를 증명하는 테스트: 변이 테스트 (mutation testing)
여기서 핵심은 다음과 같습니다. 단순히 "내 테스트가 통과하는지?"라고 묻는 대신, "만약 코드가 틀렸다면, 내 테스트가 그걸 감지할 수 있을까?"라고 물어보세요. 이 질문에 답하려면 의도적으로 버그(mutants)를 주입하고, 그 변이체 각각에 대해 전체 테스트 스위트를 실행해야 합니다.
> 를 >= 로 변경하기
+ 를 - 로 변경하기
- 반환 값을
None으로 대체하기
- 한 줄 삭제하기
and 를 or 로 바꾸기
모든 변이체는 작고 현실적인 버그입니다. 좋은 테스트 스위트는 이 변이체를 '제거(kills)'합니다. 즉, 최소한 하나의 테스트가 실패하게 만듭니다. 살아남은 변이체는 여러분의 테스트가 놓친 버그라는 뜻입니다. 점수는 간단합니다: 제거된 변이체 수 ÷ 전체 변이체 수. 커버리지(coverage)가 아니라 이 숫자가 여러분의 스위트가 실제로 얼마나 가치가 있는지 보여줍니다.
직접 손으로 할 수는 없습니다. 이미 성숙한 도구들이 있습니다:
- Python —
mutmut 또는 cosmic-ray
- JavaScript/TypeScript — Stryker
- Java — PITest
- C# — Stryker.NET
AI가 생성한 가격 책정 스위트에 처음 돌연변이 테스트를 실행했을 때의 모습은 다음과 같습니다:
$ mutmut run
# ... 몇 분 후
$ mutmut results
...
40개의 변이체 중 18개가 살아남았습니다. 완벽해 보였던 스위트가 주입된 버그의 절반 이상을 잡아내고 있었습니다. 이제 저는 할 일 목록을 갖게 되었습니다: 정확히 어떤 라인에서, 제가 테스트로 간과한 정확한 종류의 실수인지 알려주는 18개의 구멍들입니다. 저는 그 부분에 실제 단언문(assertions)을 작성하고 재실행했고, 살아남은 변이체 수를 두 개까지 줄였습니다 (둘 다 동등한 변이체—실제 동작을 변경하지 않는 변화로, 이는 익숙해지면 알아차릴 수 있습니다).
제가 실제로 추천하는 워크플로우
AI가 테스트를 계속 작성하게 하세요. 정말 빠르고 기본 구조도 괜찮습니다. 단 하나의 관문만 추가하면 됩니다:
- 테스트 생성하기. 모델에게 기본적인 작업을 맡기세요.
- 단순히 커버리지(coverage)만 측정하는 것이 아니라, 모듈에 대해 돌연변이 테스트(mutation testing)를 실행하세요.
- 살아남은 모든 변이체를 누락된 단언문으로 간주하세요. 점수를 고치는 것이 아니라, 테스트 코드를 수정하세요.
- 중요한 코드(결제, 인증, 돈을 잃거나 데이터를 유출할 수 있는 모든 것)에 대해 CI 환경에서 돌연변이 점수 임계값(mutation-score threshold)을 설정하세요. 70%부터 시작하여 진행하면서 높여가세요.
모든 커밋마다 전체 저장소(repo)를 변이 테스트(mutation-test)할 필요는 없습니다. 그것은 느리고 일주일 안에 사용을 중단하게 만들 것입니다. 위험도가 높은 모듈에 대해 실행하고, 특히 AI가 작성한 코드에는 반드시 실행해야 합니다. 그곳에 미러 테스트(mirror tests)가 숨어 있기 때문입니다.
이것이 해결하지 못하는 것들
변이 테스트는 테스트가 코드 변경을 감지할 수 있음을 증명합니다. 하지만 원래 명세(spec) 자체가 옳았는지 여부는 알려주지 않습니다. 요구사항 자체에 오류가 있다면, 잘못된 동작에 대해 높은 변이 점수(mutation score)를 받게 됩니다. 또한 무료도 아닙니다. 실행 시간은 초 단위가 아니라 분 단위이므로, 모든 키 입력마다 적용하기보다는 중요 경로(critical paths)와 병합 전(pre-merge) 단계에서 사용되어야 합니다. 그리고 비결정론적 AI 출력물(non-deterministic AI outputs)에 대해서는 아무것도 말해주지 않습니다. LLM의 답변을 테스트하는 것은 별개의 문제입니다 (이는 평가 및 실제 버그와 모델 드리프트 구별에서 다룹니다).
변이 테스트가 하는 일은 '테스트가 녹색(green)이다'라는 상태를 '실제로 버그를 포착할 수 있는 테스트다'로 변환하는 것입니다. 이것이야말로 가치가 있는 유일한 형태의 '녹색'입니다.
AI가 작성한 테스트에 의존하기 전에 이를 검증하는 이 계층(verification layer)은 AI Testing in Practice의 핵심 축입니다. 읽기 전에 직접 느껴보고 싶다면, 무료 실습 환경이 있습니다. 이 10분짜리 실행은 네 개의 실제 버그 위에 놓인 녹색 테스트 스위트와 그 버그들을 노출시키는 변이 패스를 포함합니다.
FAQ
테스트가 잘 작성되었다면 높은 커버리지(coverage)만으로 충분하지 않나요?
커버리지는 한 줄이 '실행되었음'을 알려줄 뿐, 그 결과에 대해 '무엇인가 검사했는지'는 알려주지 않습니다. 의미 있는 단언(assertion) 없이도 100% 커버리지에 도달할 수 있으며, AI가 생성한 테스트 스위트는 이를 끊임없이 합니다. 변이 점수는 단언(assertions)이 실제로 작동하는지 측정합니다.
변이 테스트는 단순히 더 많은 테스트를 작성하는 것과 어떻게 다른가요?
더 많은 테스트들은 모두 미러 테스트일 수 있습니다. 변이 테스트는 아무도 검사하지 않는 '특정' 동작을 찾아내기 때문에, 필요 없는 30개 대신 중요한 단언 3개를 추가하도록 합니다.
변이 테스트 실행(Mutation run)이 CI에 넣기엔 너무 느려요. 이제 어떻게 해야 할까요?
리포지토리 전체에 걸쳐 실행하지 마세요. 실제 위험을 안고 있는 모듈로 범위를 좁히고, 변경된 파일에 대해 병합 전(pre-merge)에 실행하세요. 결제 모듈에서 진행하는 90초짜리 변이 테스트 실행은 아무도 깨뜨리지 않을 코드 전체를 대상으로 하는 40분짜리 실행보다 더 가치가 있습니다.