AI 코딩 도구가 일상이 된 지금, 테스트 코드도 AI가 생성하는 경우가 많아졌다. 문제는 AI가 작성한 테스트가 겉으로는 그럴듯해 보이지만, 구현 세부사항에 강하게 결합되거나 상호작용 검증에만 집중하는 패턴이 반복된다는 점이다. 리팩토링 한 번에 테스트 수십 개가 한꺼번에 깨지는 경험은 그 결과다.
이 글은 특정 언어나 프레임워크의 API 사용법이 아닌, TDD 관점에서 언어를 가로질러 적용 가능한 테스트 철학, 전략, 원칙들을 다룬다. 맨 처음에 이 원칙들을 떠받치는 여섯 가지 철학을 기술하고, 1번부터 16번까지는 테스트를 어떻게 잘 짜고 실패를 어떻게 처리하는가를, 17번부터 19번까지는 AI 에이전트가 만든 테스트가 실제로 무엇을 보장하는가를, 마지막 20번은 이미 Mockist 테스트가 쌓인 코드베이스를 어떻게 옮겨가는가를 다룬다. 그리고 이 테스트 철학, 전략, 원칙들은 사용자 레벨(모든 프로젝트 공통)에서 정의하고 이를 기반으로 프로젝트별로 언어나 환경 등의 디테일을 더해주면 AI마다 일관된 테스트 코드가 나온다. 그 기준을 정리하기 위해 이 글을 쓴다.
대상 독자는 TDD 기초는 알고 있지만 실제 코드베이스에 적용할 때 흔들리는 개발자, 또는 AI 도구를 사용하면서 테스트 품질이 낮아졌다고 느끼는 팀이다.
테스트 철학
구현이 아닌 동작을 테스트한다. 순수 리팩토링은 테스트를 깨뜨려서는 안 된다.
리팩토링이 외부 동작을 바꾸지 않았다면 테스트도 깨지지 않아야 한다. 이 기준이 스무 원칙 전체의 출발점이다. 1번 원칙에서 이어진다.
특별한 사유가 없다면 Classicist(Chicago, 결과 상태를 검증하는 학파) TDD를 기본으로 선택한다. Mockist(London, 협력 객체 간 상호작용 을 검증하는 학파) 방식은 AI 기반 코드베이스에서 신뢰도가 빠르게 낮아진다.
Mockist는 협력 객체 간 상호작용을 검증하기 때문에 구현에 결합되고, AI가 코드를 재생성할 때마다 그 구현을 따라 깨진다. 왜 그런지는 1번 원칙에서 다룬다.
테스트는 빠르고(Fast), 격리되어 있으며(Isolated), 결정적(Deterministic)이어야 한다.
셋 중 하나라도 무너지면 테스트 스위트 전체에 대한 신뢰가 흔들린다. 4번 원칙에서 이어진다.
시스템 경계에서만 모킹한다. 경계 안쪽은 전부 실제 구현한다.
내가 소유하고 제어할 수 있는 코드는 실제 구현을 쓰고, 결제 API나 시스템 시계처럼 제어할 수 없는 경계 밖만 대체한다. 8번 원칙(Test Boundary)에서 구체적인 판단 기준으로 이어진다.
통합 테스트의 검증력을 단위 테스트만큼 중시한다.
단위 테스트는 빠르지만 실제 시스템과 멀어질수록 검증력이 떨어진다. 레벨을 위험에 따라 선택하는 구체적인 기준은 11번 원칙에서 다룬다.
의미 있는 소수의 테스트가 신뢰도 낮은 다수의 테스트보다 가치 있다.
커버리지 숫자를 채우는 테스트가 아니라 실제 버그를 잡는 테스트가 가치 있다. 15번 원칙에서 다룬다.
1. 테스트는 구현이 아니라 행동을 보호한다
구현이 아닌 동작을 테스트한다. 순수 리팩토링은 테스트를 깨뜨려서는 안 된다.
테스트의 역할은 코드가 어떻게 동작하는지가 아니라 무엇을 하는지를 보장하는 데 있다. 메서드 이름을 바꾸고, 반복문을 재귀로 교체하고, 중간 변수를 제거하는 리팩토링은 외부 동작을 바꾸지 않는다. 이런 변경에 테스트가 깨진다면, 그 테스트는 동작이 아닌 구현을 잠갔다. 리팩토링 후 테스트를 고치느라 시간을 쓰는 패턴이 반복된다면, 테스트가 방어망이 아닌 짐이 됐다는 신호다.
다만 모든 테스트가 리팩토링에 영향을 받지 않아야 한다는 뜻은 아니다. 외부 계약이나 중요한 상호작용 자체가 바뀌었다면 테스트가 깨지는 것이 정상이다. 예를 들어 API 응답 필드 이름이 바뀌거나, 결제 완료 후 발행하던 이벤트를 더 이상 발행하지 않기로 했다면 테스트는 그 변경을 붙잡아야 한다. 그래서 테스트가 실패했을 때 가장 먼저 던져야 할 질문은 “이 실패가 구현 변경 때문인가, 계약 변경 때문인가”이다. 전자라면 테스트가 잘못 짜인 것이고, 후자라면 테스트가 제 역할을 한 것이다.
Mockist(London School)는 협력 객체와의 상호작용 자체를 검증하므로, 호출 순서나 인자 목록까지 과하게 못 박는 순간(over-specification) 구현이 조금만 바뀌어도 테스트가 깨진다. Classicist는 최종 상태와 반환값만 검증하므로 내부 구현이 바뀌어도 사양만 만족하면 통과한다. 리팩토링에 깨지지 않는 정도가 이 원칙의 기준이므로, Classicist를 기본값으로 선택한다. 두 학파의 차이 자체가 이 글의 핵심 논점은 아니며, Mockist도 외부 시스템과의 상호작용만 신중히 골라 검증하면 안정적일 수 있다.
왜 AI 코드베이스에서 over-specification이 더 자주 나타나는가
AI 도구는 기능을 빠르게 생성하지만 내부 구현의 형태를 자주 바꾼다. 변수명을 정리하고, 메서드를 합치거나 분리하고, 호출 순서를 재배치한다. 구현이 이렇게 자주 흔들리는 환경에서 상호작용을 못 박은 테스트는 그만큼 자주 깨진다. 여기에 테스트 생성 방식의 문제가 겹친다. 에이전트는 기존 코드의 메서드 호출 패턴을 출발점 삼아 “이 메서드가 이 인자로 호출된다”는 테스트를 만들어내는 경향이 있고, 그 결과 생성된 테스트가 구현과 1:1로 결합되고, AI 환경에서는 over-specified Mockist 테스트가 기본값이 되기 쉽다. 이 반복이 누적되면 팀은 테스트를 신뢰하지 않게 되고, “AI가 코드를 짜고 테스트는 형식적으로 맞춰준다”는 패턴이 고착된다.
2. Test Oracle을 먼저 정의한다
Test Oracle(테스트 판정 기준)은 특정 입력에 대해 무엇이 옳은 결과인지 판정하는 근거다. 테스트 코드를 작성하는 능력과 Test Oracle을 설계하는 능력은 서로 다른 능력이다. 전자는 “이 함수를 어떻게 호출하고 무엇을 assert할까”를 정하는 일이고, 후자는 “이 상황에서 무엇이 정답인가”를 정하는 일이다.
Oracle이 약하면 테스트는 실행되지만 아무것도 증명하지 못한다.
result != null이런 Assertion(테스트가 기대값과 실제값을 비교해 성공·실패를 판정하는 코드)은 “null만 아니면 통과”라는 조건이 도메인의 요구사항과 무관하므로 Oracle 역할을 하지 못한다. 반면 다음은 도메인의 요구사항을 직접 표현한다.
VIP 회원의 할인율은 20%이다.
이미 만료된 비밀번호로 로그인하면 인증에 실패한다.테스트의 품질은 Assertion의 개수보다 정답을 얼마나 정확하게 정의하고 있는가에 더 크게 좌우된다. 그래서 테스트를 작성하는 순서도 정해져 있다. 사양(Requirement)을 확인하고, 그중 무엇이 외부에서 관찰 가능한지(Observable Behavior, 3번 원칙) 먼저 좁힌 뒤, 그 관찰 대상에서 무엇이 정답인지(Test Oracle)를 정하고, 그다음에야 테스트 케이스와 Assertion을 쓴다. Oracle이 불분명한 상태에서 테스트 코드부터 작 성하지 않는다.
AI 에이전트는 테스트 케이스를 쓰는 일에는 강하지만 Oracle을 설계하는 일에는 약하다. 12번 원칙에서 다룰 것처럼 AI 도구는 구체적인 예시 테스트는 잘 만들지만 불변 조건을 스스로 정의하는 데는 취약한데, Test Oracle 설계도 같은 문제를 안고 있다. 에이전트에게 “이 코드가 정확하다는 것을 증명하는 테스트를 작성하라”고 지시하면, 에이전트는 자신이 방금 작성한 구현이 통과하는 테스트를 만든다. 구현과 테스트가 같은 가정을 공유하므로, 그 가정이 틀렸다면 테스트는 아무것도 걸러내지 못한다.
이 문제는 지시 방향을 뒤집으면 완화된다. “정확성을 증명하라” 대신 “이 코드가 틀렸다는 것을 입증하는 테스트를 작성하라”고 요청한다. Falsification(반증) 기반 접근이다. 반증을 시도하도록 지시받은 에이전트는 경계값, 예외 경로, 동시성 문제, 잘못된 입력처럼 구현을 깨뜨릴 수 있는 지점을 적극적으로 찾는다. 정확함을 증명하라는 지시는 구현을 옹호하는 테스트로 이어지기 쉽지만, 틀림을 입증하라는 지시는 구현을 공격하는 테스트로 이어진다. 이 반증을 실제 테스트 케이스로 좁혀나가는 구체적인 방법은 6번 원칙에서 다룬다.
Coverage 수치도 Test Oracle 문제를 대신 풀어주지 않는다. 라인 커버리지 90%는 코드가 실행됐다는 사실만 보장하며, 실행 결과가 옳은지 검증했다는 사실은 보장하지 않는다. Assertion이 없거나 의미 없는 Assertion으로 채워진 테스트(19번 원칙 참고)는 커버리지 수치를 높이면서도 Test Oracle은 텅 빈 채로 남긴다. Coverage 자체를 어떻게 해석해야 하는지는 15번 원칙에서 별도로 다룬다.
3. Observable Behavior를 검증한다
Observable Behavior(외부에서 관찰 가능한 동작)는 시스템 내부가 아니라 바깥에서 확인할 수 있는 결과를 말한다. 1번 원칙이 “구현이 아닌 동작”이라는 기준을 세웠다면, 이 원칙은 그 동작이 코드 위에서 구체적으로 무엇으로 나타나는지를 정한다. 검증 대상이 되는 결과는 대표적으로 다음 네 가지다.
- 함수의 반환값
- DB에 저장된 실제 상태
- API 응답
- 이벤트 발생 여부
내부 메서드의 호출 여부나 순서, 협력 객체에 전달된 인자는 이 목록에 없다. 외부에서 관찰할 수 없는 구현 세부사항이므로 검증 대상이 아니다. 네 가지 중 앞의 셋은 결과 상태를 직접 읽으면 되지만, 이벤트 발생 여부는 DB처럼 조회할 수 없어 검증 방식이 다르다. 이 차이와 상호작용 검증을 예외적으로 허용하는 조건은 9번 원칙에서 다룬다. 이렇게 정한 관찰 대상을 테스트 구조와 이름으로 어떻게 드러내는지는 5번 원칙에서 이어진다.

