
테스트 커버리지 100%의 함정 (숫자에 속지 마세요)
한때 저는 테스트 커버리지 100%를 달성하기 위해 집착했습니다. 모든 줄에 초록색 불이 들어오자 안심하고 배포했지만, 결과는 치명적인 버그였습니다. 커버리지가 알려주지 않는 것들과 의미 없는 테스트의 위험성, 그리고 진짜 중요한 테스트 지표에 대해 이야기합니다.

한때 저는 테스트 커버리지 100%를 달성하기 위해 집착했습니다. 모든 줄에 초록색 불이 들어오자 안심하고 배포했지만, 결과는 치명적인 버그였습니다. 커버리지가 알려주지 않는 것들과 의미 없는 테스트의 위험성, 그리고 진짜 중요한 테스트 지표에 대해 이야기합니다.
코드를 먼저 짜고 테스트하는 게 아닙니다. 테스트를 먼저 짜고, 그걸 통과하기 위해 코딩하는 것. 순서를 뒤집으면 버그가 사라집니다.

코드 푸시하면 로봇이 테스트하고(CI), 로봇이 배포합니다(CD). '내 컴퓨터에서는 잘 됐는데'라는 변명은 이제 안 통합니다. 자동화 파이프라인으로 하루 100번 배포하기.

유닛 테스트가 다 통과해도 배포하면 에러가 나는 이유는 뭘까요? 사용자가 실제로 사용하는 흐름 그대로를 검증하는 E2E(End-to-End) 테스트가 필요합니다. Cypress와 Playwright의 장단점 비교, 깨지기 쉬운(Flaky) 테스트를 방지하는 전략, 그리고 테스트 피라미드 속 E2E의 역할을 정리합니다.

복잡한 페이지 깊숙이 있는 컴포넌트를 수정할 때마다 로그인을 다시 하고 클릭을 5번 해야 하나요? Storybook으로 컴포넌트를 격리(Isolation)해서 개발하는 CDD 방법론.

개발 초기, 저는 테스트 커버리지 도구(Jest Coverage)가 보여주는 '초록색 막대'에 중독되어 있었습니다.
Branch Coverage: 85%가 거슬렸습니다.
"이걸 100%로 만들면 우리 코드는 완벽해질 거야!"
저는 며칠 밤을 새워가며 테스트 코드를 작성했습니다. 단순한 Getter/Setter 함수부터, 절대 실행될 것 같지 않은 Catch 블록까지 억지로 테스트했습니다. 드디어 100%를 달성한 날, 팀원들에게 자랑스럽게 말했습니다. "이제 버그 걱정은 안 하셔도 됩니다!"
그리고 다음 날, 배포된 서비스에서 결제 로직이 터졌습니다.
버그의 원인은 간단했습니다. "나눗셈에서 분모가 0이 되는 케이스"를 생각하지 못했기 때문입니다.
/* 실제 코드 (커버리지 100%) */
function calculateDiscount(price, rate) {
return price / rate;
}
/* 테스트 코드 */
test('할인율 계산', () => {
expect(calculateDiscount(1000, 2)).toBe(500);
});
위 코드는 커버리지 100%입니다.
calculateDiscount 함수 내부의 모든 줄이 실행되었으니까요.
하지만 rate가 0일 때 프로그램이 멈추거나 Infinity가 나오는 비즈니스 로직 오류는 잡아내지 못했습니다.
커버리지는 "코드가 실행되었느냐"만 알려줄 뿐, "코드가 올바르게 동작하느냐"를 보장하지 않습니다. 책을 한 글자도 안 빠뜨리고 다 읽었다고 해서(커버리지 100%), 내용을 다 이해했다는 뜻은 아닌 것과 같습니다.
커버리지 숫자를 높이기 위해 작성한 최악의 테스트는 이런 것이었습니다.
/* 아무 의미 없는 테스트 */
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에 수렴합니다.
하지만 이런 테스트들이 쌓이면 "테스트 실행 시간"만 늘어나고, 리팩터링할 때마다 수정해야 하는 짐이 됩니다.
숫자 채우기용 테스트는 유지보수 비용만 높이는 기술 부채입니다.
아닙니다. 커버리지는 "어디를 테스트하지 않았는지"를 알려주는 지표로는 훌륭합니다.
커버리지는 목표(Target)가 아니라 도구(Tool)여야 합니다. Goodhart의 법칙을 기억하세요.
"측정 지표(Measure)가 목표(Target)가 되는 순간, 그 지표는 더 이상 좋은 지표가 아니다."
구글이나 페이스북 같은 빅테크 기업들도 100%를 목표로 하지 않습니다. 통상적으로 60% ~ 80% 정도면 충분히 건강한 상태입니다.
중요한 건 "몇 퍼센트냐"가 아니라 "무엇을 테스트했느냐"입니다. Happy Path(성공 케이스)만 테스트해서 100%를 만드는 것보다, 커버리지가 낮더라도 실패 케이스(예외 상황)를 테스트하는 것이 훨씬 가치 있습니다.
커버리지 숫자에 집착하다 보면, "싸구려 테스트"만 잔뜩 만들게 됩니다. 테스트에도 계급이 있습니다.
추천 비율: 단위(50%) : 통합(40%) : E2E(10%)
커버리지가 숙제처럼 느껴진다면, 순서를 바꿔보세요. 코드를 짜고 테스트를 넣는 게 아니라, 테스트를 먼저 짜고 코드를 맞추는 겁니다.
expect(add(1, 2)).toBe(3))이렇게 하면 "테스트가 없는 코드는 존재할 수 없는 상태"가 되므로, 커버리지 100%는 목표가 아니라 당연한 결과가 됩니다.
Q. 프라이빗(Private) 메소드도 테스트해야 하나요?
A. 아니요. 프라이빗 메소드는 구현 상세(Implementation Detail)입니다. 퍼블릭 메소드를 통해 간접적으로 테스트되어야 합니다. 이걸 억지로 테스트하려고 export 하는 순간, 캡슐화가 깨지고 리팩토링이 어려워집니다.
Q. 테스트 코드가 더 긴데 이게 맞나요? A. 네, 정상입니다. 보통 비즈니스 로직보다 테스트 코드가 2~3배 깁니다. 그만큼 다양한 엣지 케이스를 방어하고 있다는 뜻이니 자랑스러워 하세요.
Q. 모킹(Mocking)은 언제 해야 하나요? A. 내가 제어할 수 없는 것들에만 하세요. (외부 API, 시간, 난수 등). DB나 파일 시스템 같은 건 요즘 도커(Docker) 띄우기 쉬우니 가능하면 진짜를 쓰는 게 좋습니다.
지금 테스트 커버리지가 40%라서 불안하신가요? 괜찮습니다. 중요한 로직이 잘 지켜지고 있다면 40%도 훌륭합니다.
반대로 99%라서 안심하고 계신가요? 숫자 뒤에 숨어있는 버그가 당신을 비웃고 있을지 모릅니다.
테스트의 목적은 점수를 따는 게 아니라, "내일 안심하고 배포하기 위해서"입니다. 초록색 막대가 아니라, "자신감(Confidence)"을 지표로 삼으세요.