AI 생성 테스트 감사하기: 녹색 CI(Green CI)의 절반은 아무것도 증명하지 못한다
요약
AI 에이전트가 코드와 테스트를 동시에 작성할 때 발생하는 '미러링(mirroring)' 문제를 지적합니다. 테스트 통과(Green CI)가 코드의 올바름을 보장하지 않으며, 테스트가 단순히 구현을 그대로 반영하는 현상을 측정하는 방법론을 제안합니다.
핵심 포인트
- 테스트 통과는 구현과의 일치성을 의미할 뿐, 코드의 논리적 정확성을 증명하지 않음
- AI 에이전트가 코드와 테스트를 동시에 작성할 경우 미러링 위험이 급증함
- mirror_audit.py를 통해 AST 기반으로 테스트의 미러링 비율을 측정 가능
- 미러 비율은 버그 발생률이 아닌 독립적 신호의 누락 정도를 나타냄
AI 생성 테스트를 감사하려면, 코드를 검증하는 대신 코드를 얼마나 그대로 반영(mirror)하고 있는지 점수를 매기세요. 녹색 CI(Green CI)는 테스트가 코드와 _일치_한다는 것을 증명할 뿐, 코드가 올바르다는 것을 증명하지 않습니다. 그리고 한 에이전트(agent)가 코드와 테스트를 모두 작성할 때, 그들은 종종 단순히 코드를 그대로 반영할 뿐입니다. mirror_audit.py는 ast를 사용하여 테스트 소스를 읽으며, 절대 실행하지 않습니다. 이 도구로 단일 패스 스위트(one-pass suite)를 측정했을 때 50.0%, exit 1의 점수가 나왔습니다.
AI 공개 사항: 저는 AI 글쓰기 어시스턴트를 사용하여 이 글을 초안했습니다. 사용된 도구, 세 개의 피스처(fixtures), 그리고 아래의 모든 숫자는 Python 3.13.5 및 표준 라이브러리(stdlib)만 사용한 실제 로컬 실행 결과에서 가져왔습니다. 저는 직접 실행하고, 종료 코드(exit codes)를 확인했으며, STDOUT을 두 번 해싱하여 바이트 단위로 결정론적(deterministic)임을 확인했고, 게시하기 전에 모든 줄을 편집했습니다.
통과하는 테스트는 증거처럼 느껴집니다. 하지만 대개 여러분이 생각하는 것보다 그 가치는 낮습니다.
솔직하게 말하자면, 여기에 함정이 있습니다. 테스트는 코드가 테스트가 기대하는 대로 동작하는지를 단언(assert)합니다. 만약 동일한 작성자(인간 또는 에이전트)가 한 자리에서 코드와 기대치(expectation)를 모두 작성한다면, 그 기대치는 코드에 의해 형성됩니다. 테스트가 통과하는 이유는 통과하도록 작성되었기 때문입니다. 녹색 체크 표시가 뜰 때까지 수천 번을 실행하더라도 여러분이 확인한 것은 단 한 가지뿐입니다: 해당 스위트(suite)가 구현(implementation)과 일치한다는 사실입니다. 구현이 옳다는 것이 아닙니다. 이 둘은 서로 다른 주장이며, 녹색 체크 표시는 그 차이를 숨깁니다.
에이전트들이 구현과 테스트를 하나의 디프(diff), 하나의 작성자로 묶어 전체 풀 리퀘스트(pull request)를 제출하기 시작한 순간, 이 문제는 더욱 날카로워졌습니다. 체크 표시가 더 신뢰할 수 있게 된 것은 아닙니다. 그것을 만들어내는 주체가 바뀌었을 뿐인데, 우리의 신뢰는 변하지 않았습니다.
내가 실제로 측정한 것
요약(TL;DR).
- 녹색 테스트(Green test)는 테스트 스위트가 코드와 일치한다는 것을 증명할 뿐, 코드가 올바르다는 것을 증명하지는 않습니다. 동일한 저자가 작성한 코드와 테스트는 그 간극을 더욱 넓힙니다.
mirror_audit.py는 테스트 파일의ast(추상 구문 트리, Abstract Syntax Tree)를 읽으며(그 어떤 것도 실행하지 않음), 테스트당 4개의 미러 패턴(mirror patterns)을 식별하고, 4개 중 2개 이상이 일치하면 해당 테스트를 _미러(mirror)_로 간주하여 카운트합니다.- 의도적으로 미러 형태를 띤 테스트 스위트의 경우: 미러 비율(mirror-ratio) 50.0%, 8개 중 4개 테스트, exit 1 (CI 실패).
- 정직한 테스트 스위트(부정 사례, 독립적인 기대값, 경계값 포함)의 경우: 0.0%, exit 0 (통과). 이 주장은 반증 가능하며, 정직한 스위트에서는 통과했습니다.
- 미러 비율은 버그 발생률(bug-rate)이 아닙니다. 이는 버그의 존재가 아니라 _누락된 독립적 신호(missing independent signal)_를 측정합니다. 이에 대한 자세한 내용은 아래에서 다루겠습니다. 이 점이 이 분석을 정직하게 유지해 주는 핵심입니다.
- 표준 라이브러리
ast만 사용합니다. API 키, 네트워크, 실행되는 것 모두 없습니다. 잘못된 입력 시 → exit 2.
논증에 앞서 실행 결과부터 보여드리겠습니다. 실행 결과 자체가 곧 논증이기 때문입니다.
반론: "테스트 통과"가 "코드 작동"을 의미하지는 않는다
AI 생성 테스트에 관한 대부분의 글은 커버리지(coverage) 단계에서 멈춥니다. 에이전트가 테스트를 작성했는가? 테스트가 통과하는가? 그러면 배포합니다. 그것이 쉬운 80%입니다. 비용이 많이 드는 나머지 20%는, 통과한 테스트 중 어느 하나라도 실제적인 이유로 실패할 수 있었는가 하는 점입니다. 즉, 어디에든 독립적인 오라클(oracle)이 존재하는지, 아니면 그저 코드가 거울을 보고 스스로를 확인하고 있는 것뿐인지의 문제입니다.
한 번의 테스트 실행 결과에서 반복적으로 나타나는 세 가지 형태가 있습니다:
- 재계산 (The recompute).
assert apply_discount(200, 10) == round(200 * (1 - 10/100), 2). 우변은 구현부 자체의 공식을 다시 타이핑한 것입니다. 이는 코드와 의견이 다를 수 없습니다. 이는 코스튬을 입은f(x) == f(x)일 뿐입니다. - 실행 결과에서 복사한 골든 리터럴 (The golden literal copied from a run).
assert apply_discount(100, 25) == 75.0와 같은 경우, 여기서75.0은 독립적으로 도출된 것이 아니라 코드를 한 번 실행하여 얻은 값입니다. 당신은 버그를 포함하여 코드가 첫날에 수행한 결과에 테스트를 고정해 버린 것입니다. - 스모크 테스트 (The smoke test).
parse_iso_date("2026-06-23")에assert가 없거나,assert result is not None과 같은 경우입니다. 함수가 무엇이든 반환하기만 하면 녹색(pass)이 됩니다. 신호(signal)는 거의 제로에 가깝습니다.
이 중 어느 것도 틀린 것은 아닙니다. 단지 _검증(checks)_이 아닐 뿐입니다. 그리고 결정적으로, 이 모든 것들은 단 하나의 테스트가 실행되기 전, 즉 머지(merge) 전의 소스 코드 단계에서 정적으로 찾아낼 수 있습니다.
패배할 가능성을 열어두고 기술한 반증 가능한 주장(falsifiable claim)은 다음과 같습니다: 구현을 그대로 반영하도록 작성된 테스트 스위트와 이를 독립적으로 검증하도록 작성된 테스트 스위트를 비교했을 때, 테스트 소스를 정적으로 읽는 도구는 반영형(mirror) 스위트에는 높은 점수를, 정직한(honest) 스위트에는 낮은 점수를 주어야 합니다. 만약 도구가 정직한 스위트까지 지적한다면, 그 도구는 그저 '녹색 테스트 혐오자'일 뿐이며 무용지물입니다. 하지만 도구는 그러지 않았습니다. 반영형 스위트는 50.0%, 정직한 스위트는 **0.0%**가 나왔습니다. 도구는 둘 사이에 명확한 선을 그었습니다. 실행 결과는 바로 여기에 있습니다.
실행 결과
mirror_audit.py는 하나 이상의 Python 테스트 파일(그리고 재계산(recompute) 및 복사된 골든 패턴(copied-golden patterns)을 포착하기 위해 선택적으로 구현 파일을)을 입력으로 받습니다. 이 도구는 ast.parse를 사용하여 각 파일을 파싱하고, def test_* 함수를 찾아 순회하며 네 가지 결정론적 플래그(deterministic flags)를 적용합니다. 이 도구는 절대로 사용자의 테스트를 임포트(import)하거나 실행하지 않습니다. 리뷰어가 디프(diff)를 훑어보는 방식으로 코드를 읽지만, 지치지 않을 뿐입니다.
먼저, 에이전트가 자신의 코드와 함께 생성한 스위트입니다: 해피 패스(happy paths), 재계산(recomputes), 몇 개의 # generated 스탬프, 그리고 두 개의 스모크 테스트(smoke tests)가 포함되어 있습니다.
$ python3 mirror_audit.py fixtures/tests_mirror.py --impl fixtures/impl_under_test.py
mirror_audit (static, offline, read-only; no tests executed)
impl-oracle : on (impl_under_test.py)
...
50.0%. Exit 1. 녹색 테스트의 절반이 독립적인 신호(signal)를 담고 있지 않으며, 게이트(gate)가 빌드를 실패 처리합니다. 집계의 비대칭성에 주목하십시오. 모든 테스트가 no_negative_case(8가지 검증 항목 중 오류나 경계값을 다루는 항목이 아님)를 트리거했지만, 단 하나의 플래그만으로는 충분하지 않습니다. 어떤 테스트가 '반영형'이라고 불리기 위해서는 두 가지 축에서 반영형이어야 합니다. 그 임계값이 단순히 얕은 테스트가 가짜로 낙인찍히는 것을 방지합니다.
이제 정직한 스위트입니다: 테스트 대상 코드는 동일하지만, 부정 케이스(negative cases), 수기로 작성된 기대값 테이블(expectation table), 그리고 경계값 단언(boundary asserts)이 포함되어 있습니다.
$ python3 mirror_audit.py fixtures/tests_honest.py --impl fixtures/impl_under_test.py
tests scanned : 5
mirror tests : 0 (>= 2 of 4 flags)
...
0.0%. Exit 0. 다섯 개의 정직한 테스트(honest tests) 중 세 개가 단일 플래그(flag)를 트리거하지만(여기서는 스모크 테스트(smoke-ish shape) 형태, 저기서는 해피 패스(happy path) 형태), 도구는 여전히 이를 통과시킵니다. 왜냐하면 그 중 어느 것도 두 축(two axes)에서 미러(mirror)가 아니기 때문입니다. 이것이 반증(falsification)이 유지되는 방식입니다. 감사 도구(auditor)는 테스트 스위트(suite)가 녹색(green)이라고 해서 벌을 주지 않습니다. 대신, 테스트 스위트가 녹색이면서 동시에 실제 이유로 적색(red)이 될 수 있는 요소가 전혀 없을 때 벌을 줍니다.
구체적인 사례: 버그
비율은 추상적입니다. 여기 그 비율이 대변하는 버그가 있습니다. 테스트 대상 구현체(implementation under test)에는 하나의 실제 경계 결함(edge defect)이 있습니다. apply_discount 함수가 퍼센티지의 상한(top)은 제한하지만 하한(bottom)은 잊어버려서, 음수 할인(negative discount)이 가격을 부풀립니다.
$ python3 fixtures/prove_bug.py
mirror recompute: apply_discount(100,-50)=150.0 == recompute(150.0) -> True (테스트 통과, 녹색 CI)
honest contract : apply_discount(100,-50)=150.0 <= 100 -> False (테스트 실패 - 버그 포착)
...
-50% "할인"은 100달러짜리 물건에 대해 150달러를 청구합니다. 미러 테스트(mirror test)는 동일하게 망가진 공식으로 기대값을 계산하여 150달러를 얻고, 150 == 150을 단언(assert)하여 녹색이 됩니다. 정직한 테스트(honest test)는 계약(contract, 할인은 절대로 가격을 높여서는 안 된다)을 단언하며, 150 <= 100을 얻어 적색이 됩니다. 동일한 코드, 동일한 입력입니다. 한 스위트는 버그를 축복하고, 다른 스위트는 버그를 잡아냅니다. mirror_audit.py는 이 중 어느 것도 실행하지 않습니다. 그저 당신이 체크 표시를 신뢰하기 전에, 정적으로 어떤 스위트를 가지고 있는지 알려줄 뿐입니다.
네 가지 플래그, 그리고 왜 두 개인가
각 플래그는 실제 ast 노드에서 결정론적(deterministically)으로 발생합니다. 모델도, 표류하는 휴리스틱(heuristics-that-drift)도, 무작위성도 없습니다. 매번 동일한 파일이 입력되면 동일한 판정이 나옵니다.
- no-negative-case (부정 사례 없음).
pytest.raises/assertRaises가 없고, 관계적 경계 단언(<=,>=)도 없으며, 본문 어디에도 에러 계약(error contract)이 없습니다. 오직 해피 패스(happy path)만 존재합니다. 이것은 가장 흔하면서도 그 자체로는 가장 취약한 사례입니다. 훌륭한 테스트 중 상당수가 해피 패스입니다. 바로 그렇기 때문에 단 하나의 플래그(flag)만으로는 판정을 내릴 수 없습니다. - assert-mirrors-impl (단언이 구현을 거울처럼 반영함). 양쪽 모두 구현(implementation)을 호출하는 등가 단언(equality assert)입니다:
f(x) == f(x)또는result == recompute_with_impl(x). 독립적인 오라클(oracle)이 없으며, 기대값이 곧 코드입니다. (이 항목은 어떤 이름이 "구현"인지 알기 위해--impl파일이 필요합니다.) - no-real-assert (실질적 단언 없음). 단언이 아예 없거나(순수 스모크 테스트), 혹은 동어반복적인 단언만 존재합니다:
assert x is not None,assert isinstance(...),assertTrue(True). 통과(Green)되지만, 신호(signal)는 ≈ 0입니다. - self-grading marker (자기 채점 마커). 이 특정 테스트에
# generated/# auto스탬프가 찍혀 있거나, 구현 소스 코드에도 함께 등장하는 숫자 리터럴에 대한 등가 단언이 있는 경우입니다. 의도는 코드를 통해 유도된 값이 아니라 코드에서 복사해온 골든 값(golden value)을 잡아내는 것이지만, 이를 증명이 아닌 충돌 휴리스틱(collision heuristic)으로 읽어야 합니다. 이는 "코드에서 복사됨"과 "정직한 답이 우연히 코드에 있는100이었음"을 구분할 수 없습니다. 네 가지 중 가장 노이즈가 많습니다(주의 사항에서 더 자세히 다룹니다).
4가지 중 2개 이상이 발생할 때 테스트는 **거울(mirror)**이 됩니다. 이 임계값(threshold)이 설계의 핵심입니다. 단일하고 얕은 신호는 흔하며 용서될 수 있지만, 두 개가 동시에 나타나는 것은 검증하기 위해서가 아니라 동의하기 위해 작성된 테스트의 특징입니다. 제가 2를 선택한 이유는 제 두 가지 피스처(fixtures)를 깔끔하게 분리할 수 있었기 때문입니다. 이것은 시작점이지 법이 아닙니다. 여러분의 테스트 스위트(suites)에 맞춰 조정해 보고 결과가 어떻게 나오는지 알려주세요.
내장된 하나의 정직 규칙: 거울 비율(mirror-ratio)은 버그 발생률(bug-rate)이 아니다
이것은 제가 여러분이 그냥 지나치지 못하게 할 문장입니다. 거울 비율(mirror-ratio)은 여러분이 얼마나 많은 버그를 가지고 있는지 추정하지 않습니다. 그것은 여러분의 녹색 CI(Green CI) 중 얼마나 많은 부분이 _독립적인 신호(independent signal)_를 담고 있지 않은지, 즉 테스트 스위트(test suite)가 코드에 그저 고개를 끄덕이며 동조하고 있는 정도가 어느 정도인지를 추정합니다. 50%의 거울 비율은 통과된 테스트의 절반이 만약 오답이 있었더라도 그것을 잡아낼 수 없었음을 의미합니다. 이것이 여러분 코드의 절반이 버그투성이라는 것을 의미하는 것은 아닙니다. 완벽하게 올바른 코드에서도 (운이 좋다면) 50%의 거울 비율이 나올 수 있고, 다섯 개의 정직한 테스트가 우연히 놓친 망가진 코드에서도 5%의 거울 비율이 나올 수 있습니다. 이 도구는 _코드의 정확성(correctness)_이 아니라 _검증의 품질(quality of the check)_을 측정합니다. 출력 결과에는 매 실행마다 note: mirror-ratio measures MISSING INDEPENDENT SIGNAL, not bug-rate.(참고: 거울 비율은 버그 발생률이 아니라 누락된 독립적 신호를 측정합니다)라고 문자 그대로 인쇄되어, 저를 포함해 그 누구도 이를 결함 수로 인용할 수 없게 합니다.
만약 제가 이것을 "코드의 절반이 망가졌다"라고 꾸며서 말했다면, 그것이야말로 이 도구가 잡아내기 위해 존재하는 바로 그 과장된 주장(overclaim)이 되었을 것입니다. 그러므로 그렇게 하지 않겠습니다.
외부 수치들이 적용되는 곳 (그리고 적용되지 않는 곳)
저는 이것이 단순한 장난감 수준의 도구를 넘어 실제로 의미가 있는지 찾아보았습니다. 세 가지 외부 조사 결과가 1차 자료(primary source)를 뒷받침합니다. 저는 이 결과들을 본문에 출처를 밝히고 포함할 것이며, 결코 저의 결과로 제시하거나 제목에 사용하지 않을 것입니다.
- Veracode, 2025 GenAI Code Security Report: 100개 이상의 LLM을 통해 실행된 80개의 선별된 코딩 작업 전반에 걸쳐, 생성된 샘플의 45%가 보안 테스트에 실패하고 OWASP Top-10 취약점을 유발했습니다. Java가 72%의 실패율로 가장 최악이었습니다 (Veracode). 정확하게 읽어야 합니다: 이것은 벤치마크 작업에서의 보안 실패율이지, "모든 AI 코드의 45%가 공격 가능하다"는 뜻이 아닙니다. 그럼에도 불구하고, 이는 녹색 체크 표시(green checkmark) 뒤에 수많은 코드가 배포되고 있음을 의미합니다.
- ICSE 2026 (SEIP 트랙), "Vibe Coding in Practice" (Fawzy, Tahir & Blincoe 작성, 101개의 실무자 출처와 518개의 직접적인 사례를 검토한 회색 문헌 리뷰)에 따르면, QA(Quality Assurance, 품질 보증) 관행이 빈번하게 간과되며, 테스트를 건너뛰는 것이 가장 흔한 단일 행동으로 나타났습니다. 이는 종종 코드를 작성한 것과 동일한 AI 도구에 검증을 다시 맡기는 방식으로 이루어집니다 (arXiv 2510.00328). 이 마지막 절이 문제의 핵심을 한 문장으로 요약하고 있습니다.
- "Rethinking Verification for LLM Code Generation" (Ma et al., arXiv 2507.06920, 2025년 7월)은 모델이 구축한 평가 세트가 **동질적(homogeneous)**인 경향이 있다는 것을 발견했습니다. 저자들의 표현을 빌리자면, "제한된 수의 동질적인 테스트 케이스로 인해 미세한 결함이 감지되지 않은 채 넘어간다"는 것입니다. 이 논문은 커버리지를 넓히기 위해 인간과 LLM의 협업을 제안합니다. 우리의 상황에 대입해 보면, 이는 단언(assert)의 양쪽 측면 모두에서 나타나는 동일한 사각지대입니다. 즉, 좁고 형태가 일정한 테스트 세트는 그 자체의 협소함이 숨기고 있는 결함을 잡아낼 수 없습니다.
그리고 제가 약하다고 표시하여 무시해도 좋다고 말씀드리는 사례가 하나 있습니다. AI 코드 리뷰(code-review) 벤더인 CodeRabbit은 470개의 오픈 소스 PR(CodeRabbit)을 조사한 결과, AI가 공동 작성한 PR에서 인간이 작성한 PR보다 약 1.7배 더 많은 "이슈(issues)"를 보고했다고 밝혔습니다. 저는 이 수치를 액면 그대로 믿지 않습니다. 해당 "이슈"들은 적은 표본을 대상으로 CodeRabbit의 자체 제품에 의해 등급이 매겨졌으며, 그들은 AI 리뷰 서비스를 판매하는 회사입니다. 흥미로운 방향성이긴 하지만, 근거로 삼을 만한 사실은 아닙니다. 제가 이 내용을 포함하면서 동시에 이해상충(conflict of interest) 문제를 언급하는 이유는, 이를 제외하는 것은 체리 피킹(cherry-picking)이 될 것이고, 아무런 자격 검증 없이 그대로 제시하는 것 또한 마찬가지이기 때문입니다.
이것은 런타임(runtime)에 관한 것이 아닙니다. 머지 전(pre-merge) 단계에 관한 것입니다
저는 이전에 200을 반환하며 거짓말을 하는 에이전트에 대해 글을 쓴 적이 있습니다. 즉, 실행 스팬 트레이스(execution span-trace)를 따라가며 에이전트가 실제로 달성하지 못한 성공을 수용하기를 거부하는 런타임(runtime) 체크에 관한 내용이었습니다. 사람들은 이것이 동일한 것이라고 가정할 것입니다. 하지만 그렇지 않으며, 그 차이는 매우 중요합니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기