1. 초록색 막대의 유혹
개발 초기, 저는 테스트 커버리지 도구(Jest Coverage)가 보여주는 '초록색 막대'에 중독되어 있었습니다.
Branch Coverage: 85%가 거슬렸습니다.
"이걸 100%로 만들면 우리 코드는 완벽해질 거야!"
저는 며칠 밤을 새워가며 테스트 코드를 작성했습니다. 단순한 Getter/Setter 함수부터, 절대 실행될 것 같지 않은 Catch 블록까지 억지로 테스트했습니다. 드디어 100%를 달성한 날, 팀원들에게 자랑스럽게 말했습니다. "이제 버그 걱정은 안 하셔도 됩니다!"
그리고 다음 날, 배포된 서비스에서 결제 로직이 터졌습니다.
2. 100%인데 왜 버그가 나요?
버그의 원인은 간단했습니다. "나눗셈에서 분모가 0이 되는 케이스"를 생각하지 못했기 때문입니다.
/* 실제 코드 (커버리지 100%) */
function calculateDiscount(price, rate) {
return price / rate;
}
/* 테스트 코드 */
test('할인율 계산', () => {
expect(calculateDiscount(1000, 2)).toBe(500);
});
위 코드는 커버리지 100%입니다.
calculateDiscount 함수 내부의 모든 줄이 실행되었으니까요.
하지만 rate가 0일 때 프로그램이 멈추거나 Infinity가 나오는 비즈니스 로직 오류는 잡아내지 못했습니다.
커버리지는 "코드가 실행되었느냐"만 알려줄 뿐, "코드가 올바르게 동작하느냐"를 보장하지 않습니다. 책을 한 글자도 안 빠뜨리고 다 읽었다고 해서(커버리지 100%), 내용을 다 이해했다는 뜻은 아닌 것과 같습니다.
3. 죄악 - 의미 없는 테스트 (The Meaningless Test)
커버리지 숫자를 높이기 위해 작성한 최악의 테스트는 이런 것이었습니다.
/* 아무 의미 없는 테스트 */
class User {
constructor(name) {
this.name = name;
}
getName() {
return this.name;
}
}
test('User 이름 가져오기', () => {
const user = new User('Ratia');
// getName()을 호출했으니 커버리지는 올라감
expect(user.getName()).toBe('Ratia');
});
이 테스트는 getName이 단순히 this.name을 리턴하는지 확인합니다.
이게 깨질 확률이 얼마나 될까요? 거의 0에 수렴합니다.
하지만 이런 테스트들이 쌓이면 "테스트 실행 시간"만 늘어나고, 리팩터링할 때마다 수정해야 하는 짐이 됩니다.
숫자 채우기용 테스트는 유지보수 비용만 높이는 기술 부채입니다.
4. 그러면 커버리지는 쓸모없나요?
아닙니다. 커버리지는 "어디를 테스트하지 않았는지"를 알려주는 지표로는 훌륭합니다.
- 낮은 커버리지 조명: "어? 주문 취소 로직은 테스트가 하나도 없네?" -> 위험 신호 발견.
- 데드 코드 발견: "이 함수는 어떤 테스트에서도 실행이 안 되네? 안 쓰는 코드인가?" -> 삭제 검토.
커버리지는 목표(Target)가 아니라 도구(Tool)여야 합니다. Goodhart의 법칙을 기억하세요.
"측정 지표(Measure)가 목표(Target)가 되는 순간, 그 지표는 더 이상 좋은 지표가 아니다."
5. 적정선은 어디일까요?
구글이나 페이스북 같은 빅테크 기업들도 100%를 목표로 하지 않습니다. 통상적으로 60% ~ 80% 정도면 충분히 건강한 상태입니다.
- 핵심 비즈니스 로직(결제, 로그인): 90% 이상, 엣지 케이스까지 꼼꼼하게.
- 단순 유틸리티/UI: 50% 정도.
- 설정 코드/보일러플레이트: 0%여도 무방.
중요한 건 "몇 퍼센트냐"가 아니라 "무엇을 테스트했느냐"입니다. Happy Path(성공 케이스)만 테스트해서 100%를 만드는 것보다, 커버리지가 낮더라도 실패 케이스(예외 상황)를 테스트하는 것이 훨씬 가치 있습니다.
6. 테스트의 종류와 비용 (Testing Pyramid)
커버리지 숫자에 집착하다 보면, "싸구려 테스트"만 잔뜩 만들게 됩니다. 테스트에도 계급이 있습니다.
6.1. Unit Test (단위 테스트)
- 대상: 함수 하나, 클래스 하나.
- 비용: 매우 저렴. 실행 속도 빠름.
- 특징: 로직 검증에 좋지만, 전체 시스템이 잘 돌아가는지는 모름.
6.2. Integration Test (통합 테스트)
- 대상: API + DB, 프론트엔드 컴포넌트 + 스토어.
- 비용: 중간. DB 셋업 등이 필요함.
- 특징: 가성비 최고. 켄트 벡(Kent Beck)도 "통합 테스트를 많이 짜라"고 했습니다.
6.3. E2E Test (End-to-End 테스트)
- 대상: 실제 브라우저 띄워서 클릭해보기 (Cypress, Playwright).
- 비용: 매우 비쌈. 실행 속도 느림. 잘 깨짐.
- 특징: 사용자 경험과 가장 유사하지만, 유지보수가 지옥입니다. 꼭 필요한 핵심 기능(결제)에만 적용하세요.
추천 비율: 단위(50%) : 통합(40%) : E2E(10%)
7. TDD (Test Driven Development) 찍먹하기
커버리지가 숙제처럼 느껴진다면, 순서를 바꿔보세요. 코드를 짜고 테스트를 넣는 게 아니라, 테스트를 먼저 짜고 코드를 맞추는 겁니다.
- Red: 실패하는 테스트를 짠다. (
expect(add(1, 2)).toBe(3)) - Green: 테스트를 통과할 만큼만 최소한의 코드를 짠다.
- Refactor: 코드를 예쁘게 다듬는다.
이렇게 하면 "테스트가 없는 코드는 존재할 수 없는 상태"가 되므로, 커버리지 100%는 목표가 아니라 당연한 결과가 됩니다.
8. 자주 묻는 질문 (FAQ)
Q. 프라이빗(Private) 메소드도 테스트해야 하나요?
A. 아니요. 프라이빗 메소드는 구현 상세(Implementation Detail)입니다. 퍼블릭 메소드를 통해 간접적으로 테스트되어야 합니다. 이걸 억지로 테스트하려고 export 하는 순간, 캡슐화가 깨지고 리팩토링이 어려워집니다.
Q. 테스트 코드가 더 긴데 이게 맞나요? A. 네, 정상입니다. 보통 비즈니스 로직보다 테스트 코드가 2~3배 깁니다. 그만큼 다양한 엣지 케이스를 방어하고 있다는 뜻이니 자랑스러워 하세요.
Q. 모킹(Mocking)은 언제 해야 하나요? A. 내가 제어할 수 없는 것들에만 하세요. (외부 API, 시간, 난수 등). DB나 파일 시스템 같은 건 요즘 도커(Docker) 띄우기 쉬우니 가능하면 진짜를 쓰는 게 좋습니다.
9. 마무리 - 숫자의 노예가 되지 마세요
지금 테스트 커버리지가 40%라서 불안하신가요? 괜찮습니다. 중요한 로직이 잘 지켜지고 있다면 40%도 훌륭합니다.
반대로 99%라서 안심하고 계신가요? 숫자 뒤에 숨어있는 버그가 당신을 비웃고 있을지 모릅니다.
테스트의 목적은 점수를 따는 게 아니라, "내일 안심하고 배포하기 위해서"입니다. 초록색 막대가 아니라, "자신감(Confidence)"을 지표로 삼으세요.