AI 코딩 도구가 일상이 된 지금, 테스트 코드도 AI가 생성하는 경우가 많아졌다. 문제는 AI가 작성한 테스트가 겉으로는 그럴듯해 보이지만, 구현 세부사항에 강하게 결합되거나 상호작용 검증에만 집중하는 패턴이 반복된다는 점이다. 리팩토링 한 번에 테스트 수십 개가 한꺼번에 깨지는 경험은 그 결과다.
이 글은 특정 언어나 프레임워크의 API 사용법이 아닌, TDD 관점에서 언어를 가로질러 적용 가능한 테스트 철학, 전략, 원칙들을 다룬다. Mockist(London School) 방식이 왜 AI 코드베이스에서 특히 취약한지, Classicist(Chicago School) 접근법이 왜 더 탄탄한지를 중심으로, 현장에서 바로 적용할 수 있는 9가지 원칙을 정리한다. 이 테스트 철학, 전략, 원칙들은 claude의 사용자 레벨(모든 프로젝트 공통)에 구성하고 그 기반 아래 언어별로 상세 테스트 스펙을 프로젝트 디렉토리의 rules 아래에 구현하면 된다.
대상 독자는 TDD 기초는 알고 있지만 실제 코드베이스에 적용할 때 흔들리는 개발자, 또는 AI 도구를 사용하면서 테스트 품질이 낮아졌다고 느끼는 팀이다.
테스트 철학
9가지 원칙은 모두 여섯 가지 철학에서 파생된다. 이 철학을 먼저 받아들이지 않으면, 개별 규칙은 근거 없는 제약으로 느껴질 수 있다.
구현이 아닌 동작을 테스트한다. 순수 리팩토링은 테스트를 깨뜨려서는 안 된다.
테스트의 역할은 코드가 어떻게 동작하는지가 아니라 무엇을 하는지를 보장하는 것이다. 메서드 이름을 바꾸고, 반복문을 재귀로 교체하고, 중간 변수를 제거하는 리팩토링은 외부 동작을 바꾸지 않는다. 이런 변경에 테스트가 깨진다면, 그 테스트는 동작이 아닌 구현을 잠근 것이다. 리팩토링 후 테스트를 고치느라 시간을 쓰는 패턴이 반복된다면, 테스트가 방어망이 아닌 짐이 됐다는 신호다.
시스템 경계에서만 모킹한다. 경계 안쪽은 전부 실제다.
“어디까지 내 코드인가”를 기준으로 경계를 그린다. 내가 소유하고 제어할 수 있는 모든 것은 실제 구현을 사용한다. 데이터베이스, 캐시, 내부 서비스가 여기 해당한다. 반면 외부 결제 API, 서드파티 OAuth 서버, 운영체제의 시스템 시계처럼 내가 제어할 수 없는 것은 경계 밖이다. 경계 안쪽을 모킹하면 협력 객체의 실제 동작이 검증에서 빠지고, 구현 변화에 취약한 테스트만 남는다.
Classicist(Chicago) TDD를 선호한다. Mockist(London) 방식은 AI 기반 코드베이스에서 신뢰도가 빠르게 낮아진다.
이 선택은 다음 섹션에서 자세히 다루지만, 핵심은 단순하다. Mockist 방식은 협력 객체 간 상호작용을 검증하기 때문에 구현에 결합된다. AI가 코드를 리팩토링하거나 재생성할 때마다 구현이 달라지고, 달라진 구현마다 Mockist 테스트가 따라서 깨진다. Classicist 방식의 테스트는 최종 상태와 반환값을 검증하므로, AI가 내부를 어떻게 바꾸든 사양을 만족하면 통과한다.
통합 테스트를 단위 테스트보다 우선한다.
단위 테스트는 빠르지만 실제 시스템과 멀어질수록 검증력이 떨어진다. 유스케이스 하나를 실제 DB와 함께 통합 테스트로 검증하면, 그 경로 위의 단위 테스트 여러 개를 대체한다. 단위 테스트는 통합 테스트로 커버하기 어려운 복잡한 도메인 로직이나 엣지 케이스에만 집중한다.
테 스트는 빠르고(Fast), 격리되어 있으며(Isolated), 결정적(Deterministic)이어야 한다.
느린 테스트는 실행을 기피하게 만들고, 격리되지 않은 테스트는 실행 순서에 따라 결과가 달라지며, 비결정적 테스트는 CI를 무작위로 깨뜨린다. 셋 중 하나라도 무너지면 테스트 스위트 전체에 대한 신뢰가 흔들린다. 세 조건을 모두 갖춰야 테스트가 실질적인 안전망이 된다.
의미 있는 소수의 테스트가 신뢰도 낮은 다수의 테스트보다 가치 있다.
테스트 커버리지 숫자를 높이려고 단순 getter, 프레임워크가 자동으로 처리하는 초기화 코드, 설정 상수까지 테스트하는 경우가 많다. 이런 테스트는 실행 비용만 높이고, 실제 버그를 잡지 못한다. 테스트 한 줄을 추가하기 전에 “이 테스트가 없으면 실제 버그가 통과할 수 있는가”를 먼저 묻는다. 그 답이 “아니오”라면 테스트를 추가하지 않는 것이 낫다.
Classicist vs Mockist — 두 학파와 AI 시대의 선택
TDD에는 두 가지 뚜렷한 학파가 있다. 1990년대 말 Kent Beck이 주도한 디트로이트 방식이 Classicist(Chicago School)이고, 2000년대 초 런던 XP 커뮤니티에서 Steve Freeman과 Nat Pryce가 발전시킨 방식이 Mockist(London School)다. 두 학파는 “테스트에서 협력 객체를 어떻게 다루는가”를 두고 갈린다.
Mockist(London School) 는 테스트 대상 객체가 협력 객체와 어떻게 상호작용하는지를 검증한다. 협력 객체는 Mock으로 교체하고, verify(userRepo).save(user) 같은 방식으로 올바른 메시지가 전달됐는지를 확인한다. 장점은 객체 간 계약을 명시적으로 드러낸다는 것이다. 다만 이 방식의 취약점은 Mockist 자체에 있는 것이 아니라 over-specification 에 있다. 호출 순서, 인자 목록, 내부 메서드 이름까지 전부 검증하는 테스트는 구현이 조금만 바뀌어도 깨진다. 반면 의미 있는 상호작용만 골라 검증하는 좋은 Mockist 테스트는 훨씬 안정적이다.
Classicist(Chicago School) 는 유스케이스 전체를 실제 협력 객체와 함께 실행하고, 최종 상태나 반환값을 검증한다. 데이터베이스도 실제(또는 테스트 전용 DB)를 사용한다. 리팩토링이 일어나도 동작이 동일하면 테스트는 통과한다. 단점은 테스트 환경 구성 비용이 높다는 것이다.
왜 AI 코드베이스에서 over-specification이 더 자주 발생하는가.
AI 도구는 기능을 빠르게 생성하지만, 내부 구현의 형태를 자주 바꾼다. 변수명을 정리하고, 메서드를 합치거나 분리하고, 호출 순서를 재배치한다. 더 큰 문제는 AI가 Mockist 스타일의 테스트를 생성할 때다. AI는 메서드 호출 패턴에서 테스트를 역으로 추론하기 때문에, 생성된 테스트가 구현과 1:1로 결합되는 경향이 있다. 즉 AI 환경에서는 over-specified Mockist 테스트가 기본값이 되기 쉽고, 이것이 “구현을 바꿀 때마다 테스트를 다시 짜야 하는” 패턴을 만든다. 이 반복이 누적되면 팀은 테스트를 신뢰하지 않게 되고, “AI가 코드를 짜고 테스트는 형식적으로 맞춰준다”는 패턴이 고착되기 쉽다.
상호작용 검증이 적합한 영역은 따로 있다.
두 학파의 핵심 차이는 검증 대상이다. Mockist는 “올바른 메시지가 전달됐는가(상호작용)“를 보고, Classicist는 “올바른 결과가 나왔는가(상태)“를 본다. 내부 비즈니스 로직에서는 상태 검증이 대부분 더 적합하지만, 상호작용 검증이 본질적으로 맞는 영역이 있다. 외부 결제 API 호출, 메시지 큐 발행, 이메일 발송 같은 side-effect가 그렇다. 이런 경우는 결과 상태로 검증하기 어렵고, “이 메시지가 실제로 전달됐는가”가 곧 테스트의 핵심이다. 이 영역에서는 상호작용 검증이 더 본질적이다. 결국 선택 기준은 학파가 아니라 검증하려는 동작의 성격이다. 내부 로직은 상태로, 외부 side-effect는 상호작용으로 검증하는 것이 원칙에 부합한다.

