AI 코딩 도구는 테스트의 첫 초안을 만드는 시간을 크게 줄입니다. null 처리, 경계값, parameterized case, mock 설정, fixture 생성처럼 반복적인 작업은 특히 빠릅니다. 그러나 테스트 파일 수와 line coverage가 늘었다는 사실만으로 회귀를 잡을 수 있다는 뜻은 아닙니다. 구현과 똑같은 조건문을 테스트에도 복사했거나, mock이 실제 계약과 멀어졌거나, 결과를 assert하지 않은 채 호출만 했다면 숫자는 좋아져도 보호막은 얇습니다.
그래서 최근 팀들이 마주하는 실무 과제는 “AI에게 테스트를 더 만들게 할까”가 아니라 생성된 테스트가 실제로 잘못된 동작을 거부한다는 증거를 어떤 비용으로 얻을 것인가입니다. Mutation Testing은 소스의 비교 연산자·반환값·조건을 의도적으로 작게 바꾼 mutant를 만들고, 기존 테스트가 그것을 실패시키는지 보는 방법입니다. 모든 코드에 점수를 강제하는 도구가 아니라, AI 변경으로 빨라진 테스트 생산량을 신뢰할 수 있는지 확인하는 선택적 검증 계층으로 보는 편이 맞습니다. 저장소별 AI 에이전트 평가, AI Codemod의 Semantic Diff, Dependency Budget, TDD·JUnit·Mockito의 관점을 하나의 테스트 품질 gate로 묶어 보겠습니다.
이 글에서 얻는 것
- coverage와 mutation score가 각각 알려 주는 범위를 구분합니다.
- AI 생성 테스트에서 자주 나오는 구현 복제·과도한 mock·약한 assertion을 찾습니다.
- 느린 mutation testing을 핵심 변경에만 적용하는 우선순위를 세웁니다.
- 점수보다 더 중요한 invariant, 제외 사유, 실패 분류를 운영하는 기준을 만듭니다.
핵심 개념/이슈
1) 실행됐다는 것과 검증됐다는 것은 다르다
coverage 도구는 어떤 줄이나 branch가 실행됐는지 알려 줍니다. 이것은 도달하지 않은 경로를 찾는 데 유용하지만, 결과가 잘못돼도 test가 실패하는지는 별도 문제입니다. 아래 구현에서 >를 >=로 바꿨을 때 테스트가 모두 녹색이라면, 그 분기는 실행됐어도 경계 규칙은 보호되지 않았습니다.
boolean eligible(int age) {
return age > 19;
}
// mutant: return age >= 19;
eligible(20)만 검증하면 coverage는 100%일 수 있습니다. eligible(19) == false라는 정책 경계를 assert해야 mutant가 죽습니다. Mutation Testing에서 살아남은 mutant는 “반드시 버그”가 아니라 현재 테스트가 구분하지 못하는 행동 변화 후보입니다. 컴파일 오류가 나거나 동등한 동작을 만드는 mutant도 있으므로, 수치를 사람의 판단 없이 결함 수로 세면 안 됩니다.
| 신호 | 답하는 질문 | 놓치는 것 |
|---|---|---|
| line/branch coverage | 코드가 실행됐는가 | assertion의 강도, 경계 의미 |
| test pass | 현재 fixture가 기대한가 | fixture 밖의 행동 변화 |
| mutation result | 작은 코드 변화가 실패를 유발하는가 | 동등 mutant, 실제 업무 우선순위 |
| production invariant | 실제 시스템 제약이 지켜지는가 | 모든 조합의 구현 세부 |
AI는 이름이 그럴듯한 test를 많이 만들 수 있습니다. 하지만 “method를 호출했는가”와 “권한 없는 요청이 거부됐는가”는 다릅니다. 특히 mock된 repository가 save를 한 번 호출했는지 확인하는 테스트는 저장 전 validation·transaction·unique constraint가 깨져도 통과할 수 있습니다. 외부 collaborator interaction은 필요한 곳에서만 확인하고, 제품이 약속하는 observable result를 우선 assert해야 합니다.
2) AI 생성 테스트의 실패 모드는 반복된다
첫째는 구현 복제입니다. production code가 if (total >= 50_000)인데 test도 같은 조건으로 expected를 계산하면, 둘 다 같은 방향으로 틀릴 수 있습니다. 둘째는 과도한 mock입니다. HTTP client, clock, repository를 모두 mock하면 빠르지만 serialization, query, transaction boundary 같은 실제 결합 오류는 사라집니다. 셋째는 약한 assertion입니다. 예외가 나지 않았다는 것, list 크기만 맞다는 것, mock method가 호출됐다는 것만 확인하면 중요한 field나 authorization 결과가 틀려도 통과합니다.
AI에게 테스트 작성을 맡길 때는 “coverage를 올려라”보다 결과 계약을 제공하는 편이 낫습니다.
invariant: 취소된 주문은 결제 승인 상태로 전이할 수 없다.
boundary: 50,000원 할인은 조건을 만족할 때 한 번만 적용된다.
failure: 동일 idempotency key의 두 요청은 같은 결과를 반환하고 side effect는 한 번이다.
evidence: API response, persisted state, emitted event 중 두 개 이상을 확인한다.
이 문장은 테스트 개수를 늘리는 prompt가 아니라 reviewer와 agent가 같은 실패 조건을 보게 하는 계약입니다. AI 에이전트 실행 계약처럼 생성 결과와 완료 판정을 분리해야 합니다. 테스트가 추가됐다는 agent의 보고는 산출물이고, invariant·mutation·통합 fixture가 통과했다는 기록이 완료 증거입니다.
3) mutation score는 전사 KPI가 아니라 위험 기반 탐색 신호다
모든 PR에 전체 mutation suite를 실행하면 CI 시간이 길어지고, 개발자는 느린 gate를 피해 가려 합니다. UI glue, generated code, 단순 DTO처럼 score가 낮아도 제품 위험이 작거나 동등 mutant가 많은 영역도 있습니다. 반대로 결제 금액, 권한 판정, retry 조건, 데이터 삭제, feature flag 기본값은 한 줄의 조건 변경이 큰 사고가 될 수 있습니다.
우선순위는 높은 영향도와 낮은 복구 가능성 → 자주 바뀌는 핵심 규칙 → 외부 경계 → 일반 구현 코드가 좋습니다. 새로 만든 API 전체에 80점을 요구하는 대신, “권한 판정 module에서 변경된 lines의 critical mutant 0개 생존”처럼 좁고 설명 가능한 gate부터 시작하세요. score가 올라가도 테스트가 의미 없으면 실패이며, score가 낮아도 동등 mutant·비즈니스 비중요 코드라면 명시적 제외가 더 정직합니다.
실무 적용
1) 빠른 PR lane과 깊은 검증 lane을 분리한다
첫 lane은 모든 PR에서 unit/integration test, lint, changed-line coverage, test diff 요약을 빠르게 실행합니다. AI가 테스트를 추가했으면 PR 본문에 “깨뜨리려는 규칙, 경계값, mock한 의존성, 실행하지 않은 통합 경로”를 네 줄 이하로 적게 합니다. 이 단계는 10분 안에 결과를 내는 것을 목표로 합니다.
둘째 lane은 위험 표식이 있는 변경만 mutation testing 또는 stronger property/contract test로 보냅니다. authorization, payment, delete, retry, idempotency, feature flag, migration path 중 하나가 바뀌거나, AI가 같은 module에 test를 20개 이상 추가했는데 production diff가 작다면 후보가 됩니다. 기본 branch에는 매일 또는 주 1회 full sampling을 돌려 fast lane이 놓친 경향을 확인합니다.
| gate | 대상 | 출발 기준 | 실패 시 우선 행동 |
|---|---|---|---|
| fast test | 모든 PR | 10분 이내, 필수 test 100% 통과 | 환경 실패와 product failure 분리 |
| changed-module mutation | 고위험 module 변경 | critical mutant 생존 0개 | invariant 또는 test fixture 보강 |
| scheduled sample | 핵심 module 20~30% | baseline 대비 5%p 이상 하락 금지 | 도구·제외·test 설계 원인 분류 |
| integration/contract | 외부 경계 변경 | API·event 불변 조건 100% | mock 과잉 여부 확인 |
숫자는 절대 규칙이 아니라 운영 시작점입니다. CI p95가 30분을 넘거나 flaky mutation 결과가 2% 이상이면, gate를 더 넓히기 전에 cache, deterministic seed, test isolation과 baseline 안정성부터 고쳐야 합니다. 느린 검증을 “AI가 만든 코드니까”라는 이유만으로 전면 확대하면 사람 변경에도 같은 품질 신호를 잃습니다.
2) 살아남은 mutant를 세 가지로 분류한다
mutation 결과가 나쁘면 테스트만 더 쓰는 자동화로 반응하기 쉽습니다. 먼저 살아남은 mutant를 아래처럼 분류해야 합니다.
- 중요한 누락: 권한, 금액, 상태 전이, 오류 처리의 행동이 달라진다. 독립적인 expected 값이나 property를 추가합니다.
- 동등 또는 비중요: 변경해도 observable behavior가 같거나 위험이 낮다. 이유·owner·재검토 날짜와 함께 제외합니다.
- 설계 경고: private implementation에만 닿고 public behavior로 검증하기 어려울 만큼 분기가 복잡하다. 테스트를 억지로 늘리기보다 함수 분리·정책 table·명시적 domain model을 검토합니다.
이 분류는 AI agent에도 좋은 feedback이 됩니다. “mutation score를 90으로 맞춰라”는 지시는 의미 없는 assertion과 mock을 부르기 쉽습니다. 반대로 “상태 전이 표의 invalid edge 6개를 API 수준에서 검증하고, 동등 mutant는 제외 근거를 남겨라”는 지시는 변경의 의미를 유지합니다. Agent Codemod의 Semantic Diff처럼 test 결과도 before/after behavior와 연결해야 합니다.
3) 품질 지표는 속도와 재작업까지 함께 본다
테스트가 많아졌는데 review 시간이 늘거나 production bug가 그대로면 개선이 아닙니다. 파일 수, assertion 수, coverage, mutation result만 나란히 놓지 말고 high-risk 변경의 escape defect, reviewer가 수정한 test 비율, CI p95, flaky rate, gate bypass 사유를 같이 기록하세요. AI가 만든 test의 비율도 “나쁨”의 지표가 아니라, 어느 실패 유형이 사람이 작성한 test와 다른지 비교할 표본입니다.
첫 4주에는 AI 생성 테스트가 포함된 high-risk PR 20개만 표본으로 잡아도 충분합니다. critical mutant 생존률, reviewer rewrite 비율, 테스트가 잡은 실제 regression 수, CI 시간 변화를 baseline과 비교합니다. quality gate가 review에서 발견하던 결함을 더 일찍 찾고 시간 예산을 지킨다는 근거가 있을 때만 대상 module을 넓히세요.
트레이드오프/주의점
Mutation Testing은 만능 테스트 품질 점수도, 전면적인 병합 차단 장치도 아닙니다. 컴파일러 최적화, 언어 특성, 코드 생성 방식에 따라 동등 mutant가 생기고, 비결정적인 test는 결과를 흔듭니다. 더 높은 score를 위해 production 코드를 test 친화적으로 왜곡하거나 테스트가 implementation detail에 과도하게 결합하는 일도 경계해야 합니다.
테스트 입력에는 고객 데이터·secret·production request를 그대로 넣지 마세요. AI가 fixture를 확장할수록 실제 로그나 payload를 복사해 넣는 유혹이 생깁니다. synthetic fixture, masked trace, 짧은 retention의 artifact를 쓰고, 테스트 결과에 원문이 남지 않는지 확인해야 합니다. 우선순위는 제품 불변 조건 → 재현 가능한 fixture → 위험 기반 mutation → CI 시간 예산 → 점수 최적화입니다.
체크리스트 또는 연습
- AI가 만든 테스트마다 실패해야 하는 invariant 또는 경계값을 한 문장으로 적었다.
- line/branch coverage와 mutation 결과를 같은 품질 점수로 합산하지 않는다.
- authorization, payment, delete, retry, idempotency, feature flag 같은 고위험 module을 먼저 골랐다.
- critical mutant 생존 0개, scheduled sample 20~30%, CI p95 30분 이하처럼 확대·중단 기준을 정했다.
- 살아남은 mutant를 중요한 누락, 동등·비중요, 설계 경고로 분류하고 제외 사유를 남긴다.
- mock interaction만 검증한 테스트에 API response·persisted state·emitted event 중 observable assertion을 추가했다.
- fixture와 CI artifact에 고객 원문·token·secret이 남지 않도록 점검했다.
연습으로 최근 AI가 만든 테스트 PR 하나를 고르세요. 그중 가장 중요한 조건문 세 개를 반전하거나 경계값을 한 단계 움직였다고 가정하고, 어떤 테스트가 실패해야 하는지 먼저 적습니다. 실패할 테스트가 없다면 mutation 도구를 바로 도입하기보다 invariant를 API 또는 domain 수준 assertion으로 바꾸세요. 실패한다면 해당 module만 mutation sample에 넣고, CI 시간과 reviewer 재작업이 실제로 줄어드는지 4주 동안 비교해 보세요.
💬 댓글