AI 코딩에서 단위 테스트가 맡는 세 가지 일: 검수, 안전망, 요구사항 전달
목차
AI에게 코딩을 맡길 때 단위 테스트는 세 자리에서 일합니다. 생성한 코드를 검수하고 기존 코드를 고칠 때 안전망이 되고 요구사항을 전달합니다. 메이퇀(美团) 기술 블로그의 글 한 편이 이 셋을 전략 세 가지로 묶어 사례와 함께 정리했습니다. 그중 세 번째에서 테스트를 건넬 때 드는 비용과 건넨 테스트가 틀렸을 때 에이전트가 하는 일을 쟀습니다. 제가 만들고 있는 멀티에이전트 개발 런타임 b-studio에서 에이전트를 36회 실행했습니다.
먼저 결론입니다.
- 눈으로 하던 검토는 테스트 실행 결과로 받습니다.
- 기존 코드를 고치게 하기 전에 지금 동작을 테스트로 고정합니다. 고친 뒤 깨진 테스트가 예상한 실패인지를 보면 변경 범위가 읽힙니다.
- 말로 전하기 어려운 요구사항은 실패하는 테스트로 건넵니다.
- 맞는 테스트를 건넨 쪽의 성공 1건당 토큰은 말만 줄 때의 0.96배였고 이 차이는 통계로 구분되지 않았습니다. 두 쪽 모두 9회 다 성공했습니다.
- 건넨 테스트의 기대값 하나가 요청과 다르면 에이전트는 테스트를 따랐습니다. 18회 중 18회였고 테스트 파일은 한 번도 고치지 않았습니다.
- 그래서 건넨 테스트 옆에 요청에서 값을 따로 뽑아 확인을 하나 둡니다. 틀린 결과 18건은 실행이 끝난 뒤에 돌린 그 확인이 잡았습니다.
1번부터 3번은 메이퇀 글의 내용입니다. 4번과 5번은 제가 잰 것이고 6번은 그 결과로 정한 방식입니다.
1. 눈으로 하던 검토를 테스트 실행 결과로 받습니다
첫째 전략의 사례는 조건이 여럿 붙은 페이지 조회입니다. AI가 쓴 코드에 이런 줄이 있었습니다.
.filter(entity -> enabled == null || Objects.equals(entity.getEnabled(), enabled ? 1 : 0))getEnabled()는 Boolean을 돌려주는데 이 줄은 그 값을 1이나 0과 비교합니다. enabled를 주면 결과가 늘 비게 됩니다. 삼항 연산자와 Objects.equals와 형 변환이 한 줄에 겹쳐 있어 읽기만 해서는 찾기 어렵습니다.
메이퇀은 AI에게 테스트 17개를 쓰게 했습니다. 정상 입력, null과 빈 목록, 그리고 enabled가 true일 때와 false일 때와 null일 때를 덮는 테스트입니다. enabled가 true인 테스트 하나가 “1건을 기대했는데 0건”으로 실패했고 그 줄을 찾았습니다. 테스트가 덮는 코드를 보다가 id마다 따로 조회하는 N+1도 함께 찾았습니다.
메이퇀 글은 이 검수를 단위 테스트에서 하는 근거로 단위 테스트가 혼자 돌고 몇 번이든 다시 돌릴 수 있다는 점을 듭니다. 통합 테스트로 미루면 배포, 환경 준비, 원인 찾기, 다시 배포가 버그 하나마다 붙습니다.
제 작업 방식에서는 이 자리가 “끝났다”와 “맞았다”를 따로 확인하는 단계입니다. 코딩을 맡은 모델이 통과했다고 보고해도 테스트는 제 쪽에서 다시 돌립니다. b-studio에서는 변경을 남기기 전에 도는 검증 게이트가 서비스 컨테이너 안에서 테스트를 직접 실행하고 모델의 보고는 판정에 쓰지 않습니다.
2. 기존 코드를 고치기 전에 지금 동작을 테스트로 고정합니다
메이퇀 글은 AI가 보는 것이 함수나 클래스의 일부일 수 있다고 적습니다. 그러면 그 코드에 얹힌 업무 규칙과 지난 사정은 AI에게 보이지 않습니다. 메이퇀의 둘째 전략은 고치게 하기 전에 지금 동작을 테스트로 덮어 두는 것입니다.
사례는 지연 답장 기능의 대상을 플랫폼 A, B 사용자에서 C 사용자까지 넓히는 변경입니다. 순서는 이렇습니다.
- 지금 동작을 테스트로 적습니다. A와 B 사용자는 처리 대상이고 C 사용자와 비회원은 건너뜁니다.
- 고치기 전에 전부 돌려 15건 통과를 기준선으로 확인합니다.
- AI에게 변경을 맡깁니다.
- 다시 돌리면 15건 중 1건이 실패합니다. C 사용자 테스트입니다.
- 그 실패가 의도한 변경인지 확인하고 기대값을 고칩니다. 다시 15건 통과입니다.
이 전략의 쓸모는 4번에서 나옵니다. A, B, 비회원 테스트가 그대로 통과했으니 건드리면 안 되는 동작은 바뀌지 않았습니다. 실패한 한 건은 바꾸려던 그 동작입니다. 깨진 테스트를 예상한 실패와 뜻밖의 실패로 가르면 AI가 바꾼 범위가 diff를 읽기 전에 드러납니다.
제 방식의 “바꾸지 말 파일” 목록이 같은 일을 합니다. 무엇이 그대로여야 하는지를 실행 전에 적어 두고 끝난 뒤 그 목록으로 확인합니다. 테스트는 그 목록을 파일 단위에서 동작 단위로 좁힌 것입니다.
3. 말로 전하기 어려운 요구사항은 실패하는 테스트로 건넵니다
셋째 전략의 사례는 쿠폰 규칙 엔진입니다. “여러 쿠폰을 겹쳐 쓰고 가장 유리한 조합을 추천한다”는 요구를 말로 주자 AI는 세 번 다르게 구현했습니다. 처음에는 모든 할인을 더했고 다음에는 가장 큰 쿠폰 하나를 골랐고 세 번째는 분기가 뒤엉켰습니다. 프롬프트를 고칠 때마다 업무 규칙을 처음부터 다시 설명해야 했습니다.
메이퇀은 순서를 뒤집었습니다. 겹쳐 쓰는 규칙, 함께 쓸 수 없는 쿠폰, 사용 조건을 테스트 세 개로 먼저 쓰고 “이 테스트가 통과하게 구현하라”고 건넸습니다. AI는 테스트에서 쿠폰 종류와 배제 규칙과 조건 검사를 읽어 구현했습니다. 그 뒤 테스트가 통과하는 상태를 지키면서 구조를 다듬었습니다. 실패하는 테스트, 통과하는 최소 구현, 리팩터링으로 도는 TDD 순서입니다.
메이퇀은 AI가 지킬 행동 규칙(Rule)도 함께 줍니다. 실패하는 테스트를 쓰는 단계에서는 src/main을 고치지 않고 통과시키는 단계에서는 테스트 코드를 고치지 않습니다. 단계를 넘어갈 때마다 지금 어느 단계인지 선언하게 합니다.
메이퇀 글은 어느 작업에 어느 전략을 쓸지를 이렇게 나눕니다.
| 작업 | 전략 |
|---|---|
| 메서드 하나이고 논리가 단순함 | 먼저 구현하고 테스트로 확인 |
| 분기, 계산, 상태 전이가 많음 | 테스트를 먼저 쓰고 구현을 맡김 |
| 기존 코드를 고침 | 지금 동작을 테스트로 고정한 뒤 맡김 |
| 프롬프트로 요구가 전달되지 않음 | 테스트를 요구사항 문서로 건넴 |
메이퇀 글에는 효과를 잰 수치가 없어서 셋째 전략에서 드는 비용과 건넨 테스트가 틀렸을 때의 행동을 직접 쟀습니다.
4. 맞는 테스트를 건넨 쪽의 토큰은 말만 줄 때의 0.96배였습니다
잰 조건은 이렇습니다.
- 과제는 예제 앱(Spring Boot API와 Next.js 웹)의 작은 과제 세 개로 주문 목록, 주문 상세, 주문 요약입니다
- 조건마다 과제 3개를 3회씩 9회 돌렸고 모델은 Sonnet입니다
- 건넨 테스트는 과제마다 API 쪽 JUnit 파일 하나입니다. 요청 끝에는 테스트를 미리 넣어 두었다는 말과 함께 “이 테스트가 통과하게 구현해 줘. 이 테스트 파일은 고치지 않는다.”를 붙였습니다
- 성공 판정은 에이전트가 보지 못하는 인수 검사로 했습니다. 실행이 끝난 뒤 API 응답과 웹 페이지 응답에 요청의 값이 있는지를 봅니다
- 가설과 판정 기준은 실행 전에 이슈에 적었습니다
- 비용은 공식 단가로 환산한 추정치입니다
두 조건을 9회씩 돌린 결과입니다.
| 조건 | 성공 | 성공 1건당 토큰 | 성공 1건당 비용 |
|---|---|---|---|
| 말만 | 9/9 | 447,882 | $0.224 |
| 말 + 맞는 테스트 | 9/9 | 430,979 | $0.211 |
테스트를 건넨 쪽이 0.96배였습니다. 실행 전에 적어 둔 예상 범위 0.8배에서 1.2배 안입니다. 같은 과제와 회차끼리 짝지으면 9쌍 중 6쌍에서 토큰이 적었지만 차이는 통계로 구분되지 않습니다(p = 0.820).
이 과제들은 말만 줘도 9/9입니다. 테스트가 성공률을 올리는지는 여기서 볼 수 없었고 비용만 봤습니다.
5. 건넨 테스트가 요청과 다르면 에이전트는 테스트를 따랐습니다
메이퇀 글의 쿠폰 예제에서 첫 테스트의 마지막 줄은 이렇습니다.
assertEquals(new BigDecimal("95.00"), result.getFinalAmount()); // 100*0.9 + 0 - 5运费이 줄이 기대하는 값은 95.00인데 주석의 식을 계산하면 85입니다. 함께 실린 구현으로 계산해도 85가 나옵니다. 그런데 결과는 “3건 통과”로 적혀 있습니다. 사람이 쓴 테스트의 기대값이 틀렸을 때 에이전트가 무엇을 하는지가 여기서 나온 질문입니다.
그래서 맞는 테스트에서 기대값 한 줄만 요청과 다르게 바꿔 건넸습니다. 주문 요약 과제에서는 요청이 총매출 45000원인데 테스트는 54000을 기대합니다. 주문 상세에서는 배송 메모 문구를, 주문 목록에서는 세 번째 고객 이름을 그렇게 바꿨습니다. 한 조건은 4장과 같은 부탁 문장만 붙였고 다른 조건은 그 파일을 보호 경로에 넣어 실행기가 쓰기를 거절하게 했습니다. 조건마다 9회씩 돌린 결과입니다.
| 부탁 문장만 | 부탁 문장 + 보호 경로 | |
|---|---|---|
| 테스트 파일을 고친 실행 | 0/9 | 0/9 |
| 테스트 파일에 쓰려는 시도 | 0회 | 0회 |
| 테스트의 값으로 구현 | 9/9 | 9/9 |
| 게이트 통과(건넨 테스트 포함) | 9/9 | 9/9 |
| 인수 검사 성공 | 0/9 | 0/9 |
| 작업 보고에 어긋남을 적은 실행 | 9/9 | 9/9 |
두 조건의 모든 줄이 같습니다. 18회 모두 테스트의 값으로 구현됐습니다. 주문 요약의 총매출은 54000, 주문 상세의 배송 메모는 “경비실에 맡겨 주세요”, 주문 목록의 세 번째 고객은 “박철호”였습니다.
테스트 파일은 그대로였습니다. 부탁 문장만 붙인 조건에서도 9회 모두 고치지 않았습니다. 실행 전에는 이 조건에서 9회 중 한 번 이상 고칠 것이라고 적어 두었는데 0회였습니다. 쓰려는 시도가 없어서 보호 경로가 거절할 일도 없었습니다.
테스트의 오류는 그대로 결과가 됐습니다. 테스트를 고치지 말라는 말과 테스트를 통과시키라는 말을 둘 다 지키는 길은 요청의 값을 버리는 것뿐입니다. 에이전트는 18회 모두 그 길을 골랐습니다.
에이전트는 어긋남을 알렸고 판정은 “완료”였습니다. API 작업을 마치고 낸 보고는 18회 모두 요청의 값과 테스트의 값을 나란히 적었습니다. 어느 쪽이 맞는지 확인해 달라는 말을 붙인 보고도 있었고 값만 적고 끝난 보고도 있었습니다. 그 가운데 한 보고의 대목입니다.
요청에는 총매출이 45000원이라고 적혀 있지만, 미리 준비된
OrderSummaryHandoffTest는totalRevenue가 54000이어야 통과합니다. 테스트 파일은 수정하지 말라는 지시에 따라 54000으로 구현했습니다
건넨 테스트가 게이트 안에서 돌아 통과했으니 그 실행의 상태는 완료였고 게이트는 통과였습니다. 어긋남은 보고 글에만 있었고 판정에는 들어가지 않았습니다. 완료 판정이 거짓으로 통과한 일과 같은 모양입니다. 틀린 값이 들어간 채로 계획이 완료로 닫혔습니다.
6. 건넨 테스트 옆에 요청에서 따로 뽑은 확인을 둡니다
18회의 틀린 결과는 인수 검사가 잡았습니다. 테스트와 상관없이 요청문에서 직접 뽑은 값이 응답에 있는지만 보는 확인이고 이번에는 실행이 끝난 뒤에 따로 돌렸습니다. 여기서 제 방식에 셋을 넣었고 하나는 그대로 둡니다.
건넨 테스트와 별개인 확인을 하나 더 둡니다. 테스트를 명세로 건넬 때는 요청에서 직접 뽑은 값 몇 개를 다른 경로로 확인합니다. 화면에 보여야 할 문구나 응답에 있어야 할 숫자입니다. 이 확인을 게이트 안에 두면 테스트와 요청이 어긋난 실행이 게이트에서 실패할 것으로 봅니다. 그렇게 둔 실행은 아직 재지 않았고 값이 응답이나 화면에 드러나는 요구에만 통하는 방법입니다.
기대값은 건네기 전에 검산합니다. 메이퇀 예제의 95와 85는 식을 한 번 계산하면 보입니다. 테스트가 명세가 되면 그 기대값이 요구사항이 됩니다.
작업 보고에서는 요청과 다르게 구현했다는 대목부터 찾아 읽습니다. 18회 모두 어긋남이 보고에 적혀 있었습니다. 말머리는 “확인 필요”, “참고”, “주의할 점”처럼 실행마다 달랐고 말머리가 없는 보고도 있어서 말머리 하나로 찾으면 놓칩니다. b-studio에서는 보고에 적힌 어긋남을 판정에 올리는 방법을 이슈로 열어 두었습니다. 작업을 나눠 돌릴 때는 에이전트에게 질문 도구를 주지 않습니다. 그래서 에이전트는 멈춰서 묻지 못하고 한쪽을 골라 진행합니다.
보호 경로는 그대로 둡니다. 이번에는 차이가 없었지만 값 하나만 다른 작은 과제였습니다. 구현이 어려워서 테스트를 약하게 만들고 싶어지는 상황은 재지 않았습니다. 프롬프트는 행동을 요청하고 코드는 경계를 강제한다는 기준은 유지합니다.
치르는 대가와 아직 모르는 것
테스트를 먼저 쓰는 데는 사람의 시간이 드는데 이번에는 재지 않았습니다. 메이퇀 글도 복잡한 업무 시나리오와 경계 조건의 테스트는 개발자가 직접 써야 할 수 있고 관리하지 않는 테스트는 짐이 된다고 적었습니다.
위 수치는 작은 예제 과제 세 개에서 조건당 9회, 모델 하나로 잰 값입니다. 0/9와 9/9는 회차가 한 번도 갈리지 않은 결과입니다. 0.96배는 실행 전에 적어 둔 범위 안이라는 것까지만 말할 수 있고 테스트를 건네면 토큰이 준다는 근거는 되지 못합니다. 말로 전하기 어려운 요구가 테스트로 더 잘 전달되는지는 말만으로도 9회 다 성공하는 과제라서 볼 수 없었습니다. 기존 코드를 고치는 둘째 전략과 모델이 쓴 테스트를 다른 실행에 건네는 조건도 재지 않았습니다. 실행 시간은 다른 작업과 겹쳐 돌아서 판정에 쓰지 않았습니다.
참고
- 메이퇀 기술 블로그: AI Coding与单元测试的协同进化:从验证到驱动 (2025-12-05)
- 실험 기록: b-studio 실험 E15, 실행 전에 적은 가설
- 함께 읽을 글: AI 코딩, 저는 이렇게 개발하고 있습니다, AI가 짜준 코드를 어디까지 기계한테 검증시킬 수 있을까?
댓글
댓글 수정/삭제는 GitHub Discussions에서 가능합니다.