테스트 코드를 열심히 만들었고, 문제가 없다고 생각했지만 막상 production 환경에 동작시켜 보니 오류가 발생한다. 왜 그럴까? 오늘은 내 테스트 코드가 대상이 되는 개발의 결과물을 얼마나 테스트하고 있을까? 하는 의문에 답이되어 줄 수 있는 Code Coverage 와 테스트 코드 작성을 간단하게 살펴보도록 하겠습니다.
IntelliJ 기반 Coverage 실행부터 테스트 보강까지 실전 가이드
“Spring Boot에서 서비스 로직의 커버리지를 제대로 올리고 싶다면?”
이번 글에서는 Spring Boot 환경에서 JUnit5 + Mockito로 테스트 코드를 작성하고,
IntelliJ IDEA에서 Code Coverage를 실행·분석하며,
실제로 Coverage 지표를 개선하는 방법을 단계별로 안내한다.
📌 Code Coverage란?
Code Coverage는 테스트 코드가 프로덕션 코드를 얼마나 실행했는가를 보여주는 지표다.
주요 항목은 다음과 같다:
- Class Coverage : 전체 클래스 중 실행된 클래스의 비율
- Line Coverage : 전체 코드 라인 중 실행된 라인의 비율
- Branch Coverage : 분기(if/else, switch 등) 중 실행된 경로의 비율 (JetBrains)
- Instruction Coverage : JVM 명령 수준에서 실행된 비율
하지만 Coverage 숫자 그 자체가 목표가 아니다.
정상적인 권장 수준이 약 80% 이상이라는 언급이 있다. (Baeldung on Kotlin)
즉, 숫자는 참고지표이고, 테스트 품질 향상을 위한 도구로 보는 것이 바람직하다.
INFO: 역시 100점은 어려운 수치다;;;;
🚀 IntelliJ에서 Coverage 실행하기
▶ 단일 테스트 클래스 또는 메서드 Coverage 실행
테스트 파일이나 메서드에서 우클릭 → Run ‘…Test’ with Coverage 선택 (JetBrains)

▶ 전체 테스트 Coverage 실행
상단 메뉴 → Run → Run with Coverage 선택 또는 기존 Run Configuration에 Coverage 옵션 활성화 (JetBrains)

▶ Coverage 결과 분석하기
실행 후 IntelliJ 하단의 Coverage 탭이나 에디터 옆 gutter에서 상태를 확인할 수 있다. (JetBrains)
- 초록색: 실행된 코드
- 빨간색: 테스트되지 않은 코드
- 노란색: 부분 실행된 코드 (예: 분기 중 일부만)

이렇게 빨간색 부분을 줄일 수 있도록 테스트 코드를 추가해서 실행합니다. 그렇게 테스트 코드를 추가추가 하면 테스트 코드 전체를 실행해서 빨간색 부분은 줄고, 초록색 부분은 늘어나 80%를 넘기게 됩니다. 전 개인적으로 Class 사용은 100%, Method, Line은 80%을 넘길 수 있도록 테스트 코드를 만들려고 합니다.
지금 회산 커버리지 이런걸 생각하지 않아 저 혼자 정해서 적용하고 있습니다. 일반론 적으로....^^
🧩 Spring Boot 실전 비즈니스 로직
조건문과 예외 흐름이 많은 서비스 레이어 코드는 Coverage 향상에 좋다.
@Service
public class OrderService {
private final MemberRepository memberRepository;
private final DiscountPolicy discountPolicy;
public OrderService(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
this.memberRepository = memberRepository;
this.discountPolicy = discountPolicy;
}
public int calculateFinalPrice(Long memberId, int price) {
if (price < 0) {
throw new IllegalArgumentException("Price cannot be negative");
}
Member member = memberRepository.findById(memberId)
.orElseThrow(() -> new IllegalArgumentException("Member not found"));
int discount = discountPolicy.discount(member, price);
if (discount < 0) {
throw new IllegalStateException("Discount cannot be negative");
}
if (discount > price) {
throw new IllegalStateException("Discount cannot exceed price");
}
return price - discount;
}
public String classifyUser(int purchaseTotal) {
if (purchaseTotal >= 1_000_000) return "PLATINUM";
if (purchaseTotal >= 500_000) return "GOLD";
if (purchaseTotal >= 100_000) return "SILVER";
if (purchaseTotal >= 50_000) return "BRONZE";
return "BASIC";
}
}
🧪 JUnit5 + Mockito 기반 테스트 코드
외부 의존성을 Mockito로 격리하고 다양한 분기와 예외 흐름을 테스트한다.
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
@Mock
MemberRepository memberRepository;
@Mock
DiscountPolicy discountPolicy;
@InjectMocks
OrderService orderService;
// calculateFinalPrice 정상 흐름
@Test
void finalPrice_normal_case() {
Member member = new Member(1L, "user");
when(memberRepository.findById(1L)).thenReturn(Optional.of(member));
when(discountPolicy.discount(member, 10_000)).thenReturn(2_000);
int result = orderService.calculateFinalPrice(1L, 10_000);
assertEquals(8_000, result);
}
// price < 0 예외 흐름
@Test
void finalPrice_negative_price() {
assertThrows(IllegalArgumentException.class,
() -> orderService.calculateFinalPrice(1L, -100));
}
// 회원 미존재 예외 흐름
@Test
void finalPrice_member_not_found() {
when(memberRepository.findById(1L)).thenReturn(Optional.empty());
assertThrows(IllegalArgumentException.class,
() -> orderService.calculateFinalPrice(1L, 10_000));
}
// 할인액이 음수일 때 예외
@Test
void finalPrice_negative_discount() {
Member member = new Member(1L, "user");
when(memberRepository.findById(1L)).thenReturn(Optional.of(member));
when(discountPolicy.discount(member, 10_000)).thenReturn(-1);
assertThrows(IllegalStateException.class,
() -> orderService.calculateFinalPrice(1L, 10_000));
}
// discount > price 예외
@Test
void finalPrice_discount_too_big() {
Member member = new Member(1L, "user");
when(memberRepository.findById(1L)).thenReturn(Optional.of(member));
when(discountPolicy.discount(member, 10_000)).thenReturn(20_000);
assertThrows(IllegalStateException.class,
() -> orderService.calculateFinalPrice(1L, 10_000));
}
// classifyUser 각 등급 테스트
@Test
void classifyUser_platinum() {
assertEquals("PLATINUM", orderService.classifyUser(1_000_000));
}
@Test
void classifyUser_gold() {
assertEquals("GOLD", orderService.classifyUser(500_000));
}
@Test
void classifyUser_silver() {
assertEquals("SILVER", orderService.classifyUser(100_000));
}
@Test
void classifyUser_bronze() {
assertEquals("BRONZE", orderService.classifyUser(50_000));
}
@Test
void classifyUser_basic() {
assertEquals("BASIC", orderService.classifyUser(49_999));
}
}
위 코드로 대부분의 분기와 예외 흐름을 커버할 수 있다.
📈 Coverage 퍼센트 올리는 핵심 팁
✔ 조건문/예외 흐름 먼저 테스트하기
분기가 많은 Service 로직을 먼저 테스트하면 Branch Coverage 개선 효과가 크다.
✔ 정상 흐름 + 예외 흐름 “쌍으로” 테스트하기
“정상 케이스 + 예외 케이스” 모두 작성해야 Branch Coverage가 온전해진다. (DEV Community)
✔ 외부 의존성은 Mockito로 격리하기
Service 내부 로직만 테스트 대상이 되어야 빠르고 안정적인 단위 테스트가 된다. (Medium)
✔ Coverage 숫자는 참고지표이다
80% 이상을 권장하는 언급이 있지만, 숫자 그 자체가 목표가 되어선 안 된다. (Baeldung on Kotlin)
✔ IntelliJ 설정에서 Branch Coverage 활성화하기
IntelliJ 설정의 Coverage → “Branch coverage” 옵션 활성화하면 분기 기반 Coverage 측정이 가능하다. (JetBrains)
🎯 마무리
Coverage는 숫자 그 자체보다는 테스트 품질을 높이기 위한 도구이다. 이 도구를 잘 활용하면 코드 이해도가 올라가고 리팩토링도 수월해집니다. 그리고 production 환경에서도 오류를 줄이는 좋은 통로가 되어 줄 것입니다. 물론 조만간 바이브 코딩으로 짠 코드는 테스트하지 않아도 되는 코드를 만들어낼 수 있지 않을까?
필요하다면 블로그용 이미지 캡처 가이드, CI/CD에서 Coverage 검증하는 설정 예시, JaCoCo 리포트 자동 생성 스크립트도 같이 만들어줄까?
'Develop Story' 카테고리의 다른 글
| IntelliJ + Java 디버거 완전 정복: 원리부터 사용하는 법까지 (0) | 2025.11.28 |
|---|---|
| Mac OS X에서 pip install MySQL-python 설치 오류 문제 해결 (0) | 2020.12.14 |
| HA Proxy - Apache Jakarta - Apache Tomcat 구성에서 Tomcat에 실제 Client IP 확인하기(X-Forwarded-For) (3) | 2014.04.22 |
| Apache Cordova 크로스 플랫폼 HTML5 어플리케이션 만들기-4장 (0) | 2012.10.04 |
| Apache Cordova 크로스 플랫폼 HTML5 어플리케이션 만들기-3장 (0) | 2012.09.20 |