티스토리 뷰

 

현대 소프트웨어 개발 생태계에서 '테스트 코드'는 선택이 아닌 생존의 문제입니다. 서비스의 규모가 커질수록 사람이 직접 모든 기능을 확인하는 것은 불가능에 가깝고, 작은 코드 수정이 예상치 못한 곳에서 장애를 일으키는 '회귀 버그'는 개발자의 밤을 지새우게 만듭니다.

그중에서도 JUnit5는 자바 진영의 표준 테스트 프레임워크로서, 코드 한 줄이 비즈니스 요구사항을 정확히 충족하는지 검증하는 가장 강력한 도구입니다. 오늘은 단순히 테스트를 '돌리는' 수준을 넘어, 유지보수가 쉽고 신뢰할 수 있는 테스트 환경을 구축하는 방법을 깊이 있게 살펴보겠습니다.


1. JUnit5 Deep Dive: 왜 JUnit5인가?

JUnit5는 이전 버전과 달리 단일 라이브러리가 아닌 JUnit Platform, JUnit Jupiter, JUnit Vintage라는 세 가지 모듈의 결합체입니다. 이러한 구조적 변화 덕분에 확장성이 비약적으로 상승했습니다.

테스트를 '요리'에 비유한다면

테스트를 작성하는 과정은 요리사가 음식을 내놓기 전 간을 보는 과정과 같습니다.

  • JUnit Platform: 요리를 할 수 있는 '주방(인프라)'입니다.
  • JUnit Jupiter: 최신 레시피가 담긴 '조리 도구'입니다. 우리가 사용하는 @Test 같은 최신 어노테이션이 여기 속합니다.
  • Assertions (검증): 완성된 요리의 맛이 레시피와 일치하는지 확인하는 '시식' 단계입니다.

2. 실전 Hands-on: 이커머스 장바구니 로직 테스트

흔한 "Hello World" 대신, 실제 비즈니스 로직에서 발생할 수 있는 장바구니 할인 및 합계 계산 로직을 통해 실전 감각을 익혀보겠습니다.

테스트 대상 코드 (SUT)

Java
 
public class ShoppingCart {
    private List<Item> items = new ArrayList<>();

    public void addItem(Item item) {
        this.items.add(item);
    }

    public int calculateTotal(double discountRate) {
        // 할인율은 0과 1 사이여야 함
        if (discountRate < 0 || discountRate > 1) {
            throw new IllegalArgumentException("Invalid discount rate");
        }

        int sum = items.stream().mapToInt(Item::getPrice).sum();
        return (int) (sum * (1 - discountRate));
    }
}

JUnit5 테스트 코드 작성

Java
 
import org.junit.jupiter.api.*;
import static org.junit.jupiter.api.Assertions.*;

@DisplayName("장바구니 기능 테스트")
class ShoppingCartTest {

    private ShoppingCart cart;

    @BeforeEach
    void setUp() {
        // 각 테스트 시작 전 독립적인 장바구니 객체 생성 (Isolation)
        cart = new ShoppingCart();
    }

    @Test
    @DisplayName("아이템을 추가하면 총 금액이 정확히 계산되어야 한다")
    void calculateTotal_Success() {
        // Given: 상품 준비
        cart.addItem(new Item("맥북 프로", 3000000));
        cart.addItem(new Item("매직 마우스", 100000));

        // When: 10% 할인 적용하여 계산
        int total = cart.calculateTotal(0.1);

        // Then: (3,000,000 + 100,000) * 0.9 = 2,790,000
        assertEquals(2790000, total, "10% 할인된 금액이 일치해야 합니다.");
    }

    @Test
    @DisplayName("할인율이 범위를 벗어나면 예외가 발생해야 한다")
    void calculateTotal_Exception() {
        // Given: 상품 추가
        cart.addItem(new Item("아이폰", 1500000));

        // When & Then: 잘못된 할인율(1.5 = 150%) 입력 시 예외 검증
        assertThrows(IllegalArgumentException.class, () -> {
            cart.calculateTotal(1.5);
        }, "할인율 초과 시 IllegalArgumentException이 발생해야 합니다.");
    }
}

핵심 포인트 해설

  1. @DisplayName: 테스트 결과를 리포트로 볼 때 "calculateTotal_Success" 같은 메서드명 대신 사람이 읽기 쉬운 문장으로 표시해 줍니다. 협업 시 가독성을 극대화합니다.
  2. Given-When-Then 패턴: 테스트 구조를 준비(Given), 실행(When), 검증(Then)의 3단계로 나누어 흐름을 명확히 합니다.
  3. assertThrows: 단순히 성공 케이스만 보는 것이 아니라, 잘못된 입력에 대해 시스템이 '의도된 대로 실패'하는지 확인하는 것도 시니어급 개발자의 필수 덕목입니다.

3. 트러블슈팅: 흔히 겪는 실수와 해결책

Q. 테스트를 돌릴 때마다 결과가 달라져요 (Non-deterministic Test)

  • 원인: 테스트 간에 공유 자원(Static 변수, 로컬 파일, DB 데이터)을 사용하기 때문입니다.
  • 해결: @BeforeEach나 @AfterEach를 활용해 테스트 환경을 초기화하세요. 각 테스트는 반드시 **독립적(Isolated)**이어야 합니다.

Q. Private 메서드는 어떻게 테스트하나요?

  • 해결: 원칙적으로 Private 메서드는 직접 테스트하지 않습니다. 해당 메서드를 사용하는 Public 메서드를 통해 간접적으로 검증하는 것이 객체지향 설계에 부합합니다. 만약 Private 메서드 로직이 너무 복잡하다면, 별도의 클래스로 분리(Extract Class)해야 한다는 설계적 신호일 수 있습니다.

4. Trade-offs: 무엇을 얻고 무엇을 잃는가?

단위 테스트가 만능은 아닙니다. 장단점을 명확히 이해해야 합니다.

  • 장점
    • 빠른 피드백: 코드 수정 후 몇 초 안에 버그 유무를 확인할 수 있습니다.
    • 설계 개선: 테스트하기 쉬운 코드를 짜다 보면 자연스럽게 결합도가 낮아지고 응집도가 높아집니다.
  • 단점 및 한계
    • 초기 비용: 코드 작성 시간이 늘어납니다. (하지만 장기적인 디버깅 시간을 대폭 줄여줍니다.)
    • 통합 관점의 부재: 개별 부품(단위)은 잘 작동해도, 부품 간의 연결(통합) 과정에서 발생하는 문제는 찾아낼 수 없습니다.

결론 및 제언

JUnit5를 활용한 단위 테스트는 단순히 '버그를 찾는 작업'이 아니라, **'내가 짠 코드에 대한 품질 보증서'**를 발행하는 과정입니다. 좋은 테스트 코드는 그 자체로 훌륭한 API 문서가 되며, 동료 개발자들에게 여러분의 코드 의도를 명확히 전달합니다.

반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2026/04   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30
글 보관함