haechandev
AI와 일

AI가 테스트를 만들었다고, 테스트가 설계된 것은 아니다

개발 규칙을 지켜도 테스트는 쉽게 거대해진다. 1만 줄이 넘은 운영·배포 검증 테스트를 마주하며, 검증 자체를 다시 설계하기 시작한 이유.

2026년 8월 17일

AI와 개발할 때 파일을 작게 나누고, 기존 코드를 먼저 찾고, 애매한 것은 하나씩 정리하자는 규칙을 꽤 신경 써서 지킵니다. 기능 코드는 그 기준을 잘 지키게 만들 수 있습니다.

그런데 최근 진행 중인 프로젝트를 다시 보면서 하나를 더 배웠습니다. 테스트 코드는 “테스트도 작성해 줘”라고 한마디 덧붙인다고 같이 설계되지 않습니다.

AI는 기능을 만들고 테스트를 추가하라는 요청을 아주 성실하게 수행합니다. 문제는 그 테스트가 이미 있는 큰 파일의 마지막에 붙는 식으로 쌓이기 쉽다는 점입니다. 당장 회귀는 막았고 CI도 통과합니다. 하지만 몇 번 더 수정하고 예외를 추가하면, 테스트는 어느새 기능 코드와는 다른 방식의 거대한 덩어리가 됩니다.

규칙을 지켜도 테스트는 커질 수 있다

지금 진행 중인 프로젝트의 운영·배포 검증 테스트 중 하나는 deploy-seed.test.mjs라는 파일인데, 현재 10,686줄입니다.

이 파일이 길어진 이유는 단순히 테스트를 대충 썼기 때문만은 아닙니다. 배포 과정은 권한 확인, 원격 실행, 환경 검증, 롤백, 재시도, 장애 복구처럼 실패하면 안 되는 조건이 많습니다. 변경할 때마다 “이 경우도 막아야 하지 않을까?”라는 테스트가 자연스럽게 추가됐습니다.

문제는 시간이 지나면서 다음 질문들의 경계가 한 파일 안에서 흐려진다는 점입니다.

  • 이 테스트는 배포 정책을 검증하는가, 원격 명령의 형식을 검증하는가
  • 실패했을 때 복구가 되는지를 보는가, 권한이 없을 때 아무 것도 하지 않는지를 보는가
  • 같은 준비 코드가 필요한 것인가, 아니면 서로 다른 시나리오가 우연히 한곳에 모인 것인가

파일이 8천 줄, 1만 줄을 넘기기 시작하면 테스트 실패 한 줄을 봐도 먼저 “어느 상황을 깨뜨린 거지?”부터 찾아야 합니다. 기능을 바꾸는 일보다, 테스트가 표현하는 상황을 해석하는 데 시간이 더 듭니다. 테스트가 안전망이어야 하는데, 수정하기 어려운 두 번째 레거시가 되는 순간입니다.

AI는 테스트의 존재를 챙기지만, 경계까지 저절로 챙기지는 않는다

예를 들어 이렇게 요청하면 어떨까요.

배포 정책을 바꿨으니 기존 테스트를 수정하고 빠진 케이스도 추가해 줘.

AI는 관련 테스트를 찾아 조건을 추가하고, 실패 케이스도 몇 개 더 넣을 겁니다. 여기까지는 맞습니다. 다만 그 테스트가 어느 책임에 속하는지, 더 작은 검증으로 나눌 수 있는지, 공통 준비 코드가 다음 변경에도 이해 가능한지는 별도의 질문입니다.

그래서 이제는 구현을 부탁할 때 테스트도 같이 이렇게 정리하려고 합니다.

구현 전에 이 변경에서 절대 깨지면 안 되는 계약을 세 개 이내로 정리해 줘. 각 계약은 단위 테스트, 통합 테스트, 실제 흐름 검증 중 어디에 두는 것이 맞는지도 이유와 함께 제안해 줘. 기존 큰 테스트 파일에 추가해야 한다면 그 파일이 맡고 있는 책임과 분리할 후보도 먼저 알려 줘.

핵심은 테스트를 더 많이 만들라는 말이 아닙니다. 어떤 실패를, 어느 층에서, 왜 막는지 먼저 정하는 것입니다.

테스트도 제품 코드처럼 역할을 나눈다

지금 이 문제를 정리하면서 제가 잡은 기준은 단순합니다.

1. 먼저 “절대 깨지면 안 되는 것”을 문장으로 쓴다

배포처럼 위험한 변경이라면 테스트 이름보다 먼저 계약을 적습니다.

  • 권한이 맞지 않으면 원격 실행이나 파일 변경이 시작되지 않는다.
  • 중간에 실패하면 이전 상태를 복구하거나, 복구할 수 없는 상태임을 명확히 남긴다.
  • 성공 응답처럼 보여도 검증 근거가 맞지 않으면 성공으로 처리하지 않는다.

이 문장이 있어야 같은 내용을 여러 테스트가 애매하게 중복하지 않습니다. 또 새 예외가 생겼을 때 어디에 추가해야 하는지도 판단할 수 있습니다.

2. 작은 규칙과 긴 시나리오를 분리한다

입력 형식 검증, 상태 전이, 권한 판정처럼 빠르게 확인할 수 있는 규칙은 작고 독립적인 테스트에 둡니다. 반대로 실제 배포 흐름, 롤백, 원격 명령 순서처럼 여러 단계가 함께 맞아야 하는 것은 시나리오 테스트로 둡니다.

모든 것을 E2E처럼 만들면 느리고 실패 원인을 찾기 어렵습니다. 반대로 작은 단위 테스트만 있으면 실제로 연결된 흐름이 깨질 수 있습니다. 한쪽을 선택하는 문제가 아니라, 같은 사실을 가장 싸고 명확하게 증명할 수 있는 위치를 고르는 문제입니다.

3. 시나리오의 준비물도 따로 관리한다

긴 테스트가 어려워지는 큰 이유는 테스트 본문보다 준비 코드입니다. 환경, 권한, 원격 응답, 이전 상태를 매번 조금씩 다르게 만들기 시작하면 어떤 값이 핵심인지 보이지 않습니다.

그래서 공통으로 쓰는 가짜 데이터와 생성 함수는 한곳에 두되, 시나리오별로 의미가 다른 준비는 억지로 공유하지 않으려 합니다. 예를 들어 “권한 없는 요청”, “롤백 가능한 중간 실패”, “원격 응답이 끊긴 상황”은 이름부터 분명한 생성 함수로 만듭니다.

const request = unauthorizedDeployRequest();

const result = await startDeployment(request);

expect(result).toEqual({ status: "denied" });
expect(remoteCommand).not.toHaveBeenCalled();

이렇게 보면 테스트가 무엇을 증명하는지 바로 보입니다. 반대로 긴 설정 객체와 파일 작성 코드가 본문 대부분을 차지한다면, 검증하려는 핵심이 준비물 사이에 묻혀 있다는 신호입니다.

변경 작업의 완료 조건에 테스트 설계를 넣는다

예전에는 기능이 동작하고 테스트가 통과하면 일단 끝이라고 생각했습니다. 이제는 작업 목록에 아래 질문도 같이 넣습니다.

## 배포 정책 변경

- [ ] 바뀌는 정책과 실패 조건을 문장으로 확정
- [ ] 기존 검증 중 재사용할 계약 확인
- [ ] 새 규칙을 가장 작은 테스트 층에 추가
- [ ] 실제 흐름에서 필요한 시나리오 한 개만 추가
- [ ] 기존 큰 테스트에서 분리할 책임 기록

AI에게도 “테스트를 추가해 줘”가 아니라, 이 목록을 기준으로 아직 비어 있는 첫 항목부터 처리하게 합니다. 그러면 AI가 테스트 코드를 쓰기 전에 먼저 기존 검증의 의미를 읽고, 새 케이스가 어디에 있어야 하는지를 같이 판단하게 됩니다.

아직 정리 중이다

이 글이 “긴 테스트 파일을 완벽하게 해결했다”는 이야기는 아닙니다. 지금도 큰 운영·배포 검증을 하나씩 읽으면서, 이 테스트가 지키는 계약과 분리할 수 있는 시나리오를 정리하고 있습니다.

다만 이제는 확실히 알게 됐습니다. AI에게 좋은 구조의 기능 코드를 요청하는 것만으로는 부족합니다. 테스트도 별도의 제품처럼 책임, 경계, 준비물, 실패 조건을 설계해야 합니다.

테스트가 많다는 사실보다 중요한 것은, 다음에 바꾸는 사람이 그 테스트를 보고 무엇을 절대 깨뜨리면 안 되는지 바로 이해할 수 있는가입니다. 그 기준으로 다시 정리해 나가고 있습니다.