본문 바로가기

Develop Story

Spring Boot + JUnit5 + Mockito로 Code Coverage 올리기

728x90

테스트 코드를 열심히 만들었고, 문제가 없다고 생각했지만 막상 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)

run-with-coverage

▶ 전체 테스트 Coverage 실행

상단 메뉴 → Run → Run with Coverage 선택 또는 기존 Run Configuration에 Coverage 옵션 활성화 (JetBrains)

run-menu-coverage

▶ Coverage 결과 분석하기

실행 후 IntelliJ 하단의 Coverage 탭이나 에디터 옆 gutter에서 상태를 확인할 수 있다. (JetBrains)

  • 초록색: 실행된 코드
  • 빨간색: 테스트되지 않은 코드
  • 노란색: 부분 실행된 코드 (예: 분기 중 일부만)

coverage-result

이렇게 빨간색 부분을 줄일 수 있도록 테스트 코드를 추가해서 실행합니다. 그렇게 테스트 코드를 추가추가 하면 테스트 코드 전체를 실행해서 빨간색 부분은 줄고, 초록색 부분은 늘어나 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 리포트 자동 생성 스크립트도 같이 만들어줄까?

반응형