Cute Light Pink Flying Butterfly 🧪테스트주도개발(TDD) 하기 | JUnit5 단위 테스트 프레임워크 :: 놀면서 돈벌기
본문 바로가기
IT/Backend | All

🧪테스트주도개발(TDD) 하기 | JUnit5 단위 테스트 프레임워크

by esclife_ 2025. 11. 12.
반응형

 

 

"일단 코드를 짜고 보자"는 방식은 그만!


 

 

소프트웨어 개발의 품질과 생산성을 혁신하는 방법, 바로 테스트 주도 개발(Test-Driven Development, TDD)입니다.

이번 포스팅에서는 TDD의 기본 원칙부터 Java 21 환경에서 가장 많이 사용되는 JUnit 5 어노테이션, 그리고 복잡한 의존성을 처리하는 Mocking 기술까지, TDD의 기본 가이드를 작성해 보려고해요.

 


1. 💡 단위 테스트(Unit Test) 클래스를 작성해야 하는 이유

TDD의 핵심인 단위 테스트는 코드를 작성하는 것만큼, 아니 그 이상으로 중요한 작업입니다.

중요성 상세 설명
버그 조기 발견 개발 초기 단계에서부터 버그를 잡아내어, 나중에 발견했을 때보다 수정 비용을 획기적으로 절감합니다.
코드의 설계 개선 테스트 코드를 먼저 작성하면, 자연스럽게 테스트하기 쉬운 코드를 만들게 되어 모듈화되고 깔끔한 설계를 유도합니다.
안전한 리팩터링 기존 코드의 기능을 보장하는 테스트가 있다면, 언제든 안심하고 코드를 개선(리팩토링)하거나 기능을 추가할 수 있습니다.
코드 명세서 역할 테스트 코드 자체가 이 코드는 이렇게 작동해야 한다라는 가장 정확하고 실행 가능한 문서 역할을 합니다.

2. 📝 IntelliJ에서 단위 테스트 파일 만들기 및 구조

단위 테스트 파일은 프로덕션 코드와 분리된 src/test/java 경로에 생성됩니다. IntelliJ IDEA는 이 작업을 매우 빠르고 쉽게 할 수 있도록 도와줍니다.

1) IntelliJ를 이용한 테스트 파일 자동 생성 (가장 빠른 방법)

일반 개발 코드(src/main/java)를 작성한 후, 해당 클래스에 대한 테스트 파일을 바로 생성할 수 있습니다.

  1. 테스트 대상 클래스(UserService.java 등)를 IntelliJ 편집기에서 엽니다.
  2. 클래스 이름 위에서 커서를 두고 Alt + Enter (Mac: ⌥ + ⏎)를 누릅니다.
  3. 팝업 메뉴에서 Create Test (테스트 생성)를 선택합니다.
  4. 열리는 창에서 다음을 설정합니다.
    • Testing library: JUnit5를 선택합니다.
    • File location: 기본값(src/test/java 경로)을 그대로 둡니다.
    • Methods to test: 테스트를 만들고자 하는 메서드들을 체크합니다.
  5. OK를 클릭하면 src/test/java 경로에 UserServiceTest.java 파일이 자동으로 생성됩니다.

2) 기본 테스트 구조

테스트 클래스는 일반적으로 별도의 상속 없이 작성되며, 테스트 코드를 포함하는 테스트 메서드들로 구성됩니다.

package com.example.service; // 프로덕션 코드와 동일한 패키지 구조

import org.junit.jupiter.api.Test; // JUnit 5의 핵심 어노테이션
import static org.junit.jupiter.api.Assertions.*; // 검증을 위한 static import

class UserServiceTest { // 테스트 대상 클래스 이름 + Test

    // 테스트 케이스를 정의하는 메서드
    @Test
    @DisplayName("사용자 ID가 유효할 때, 유저 객체를 반환해야 함") // 🚨 이 이름으로 출력됩니다.
    void shouldReturnUserWhenIdIsValid() {
        // 1. Given (준비): 테스트에 필요한 초기 데이터나 객체를 설정
        UserService service = new UserService();
        
        // 2. When (실행): 테스트 대상 메서드를 실행
        User result = service.findUserById(1);
        
        // 3. Then (검증): 실행 결과를 예상 값과 비교하여 확인
        assertNotNull(result); 
        assertEquals("Alice", result.getName());
    }
}

3. ✨ JUnit 5 핵심 어노테이션

JUnit 5 (JUnit Jupiter)는 유닛테스트(단위테스트)를 작성할 때 쓰이는데요, 기본 @Test, @BeforeEach 외에도 자주 사용되는 유용한 어노테이션들을 소개합니다.

어노테이션 설명 용도
@Test 해당 메서드가 실행 가능한 단위 테스트임을 명시합니다. 개별 테스트 케이스 정의
@DisplayName 테스트 결과에 표시될 사람 친화적인 이름을 지정합니다. 테스트 보고서 가독성 향상
@BeforeAll 해당 클래스의 모든 테스트 실행 전단 한 번 실행됩니다. (반드시 static이어야 함) DB 연결, 공통 리소스 설정 등
@AfterAll 해당 클래스의 모든 테스트 실행 후단 한 번 실행됩니다. (반드시 static이어야 함) DB 연결 해제, 공통 리소스 정리
@ParameterizedTest 여러 인수를 사용하여 동일한 테스트를 반복 실행합니다. 다양한 입력 값에 대한 테스트 자동화
@ValueSource @ParameterizedTest와 함께 사용되며, 단일 인자 목록을 제공합니다. shouldFailForInvalidEmails(@ValueSource(strings = {"a@b", "c@d"}) String email)
@Disabled 해당 테스트 클래스나 메서드를 일시적으로 실행하지 않도록 비활성화합니다. 실패하거나 작업 중인 테스트 임시 제외

4. 🧩 외부 의존성 격리: Mocking 기술과 도구

Mocking은 테스트 대상 객체가 의존하는 외부 객체(예: DB 연결 클래스, 외부 API 클라이언트)의 행동을 흉내 내는 가짜 객체를 만들어 사용하는 기술입니다.

 

테스트 코드를 작성할 때 가장 큰 장애물인 외부 의존성을 극복하는 방법을 배워야 합니다.

대부분의 비즈니스 로직은 데이터베이스, 외부 API, 파일 시스템 등 외부에 의존합니다. 단위 테스트는 이런 외부 환경을 배제하고 순수한 로직만을 테스트해야 합니다. 이때 필요한 기술이 바로 Mocking (모의 객체화)입니다.

1) Mocking의 핵심 목적

목적 설명
테스트 격리 테스트 대상 코드(System Under Test, SUT)만 실행되도록 외부 의존성을 차단하고, 순수한 비즈니스 로직만 검증합니다.
환경 통제 "DB 연결 실패 시", "외부 API가 특정 에러 코드를 반환할 시"와 같이, 실제 환경에서 만들기 어려운 특정 시나리오를 강제로 재현할 수 있습니다.
실행 속도 실제 네트워크 통신이나 디스크 I/O 없이 메모리에서만 빠르게 실행되어 테스트 시간을 단축합니다.

2) Mocking 도구의 종류

도구 역할 사용 예시
Mockito 객체 Mocking (단위) 자바 클래스의 메서드 호출을 가로채서 원하는 값을 반환하도록 설정합니다. (가장 일반적인 단위 테스트 도구)
WireMock HTTP Mocking (통합) 실제 웹 서버처럼 작동하는 가짜 서버를 띄워, 외부 REST API 호출 시 Mocking 응답을 보내도록 설정합니다.

3) 🔬 Mockito 활용법: @Mock과 @InjectMocks

Java 단위 테스트의 표준 Mocking 프레임워크인 Mockito와, JUnit 5에서 이를 편리하게 사용할 수 있도록 도와주는 어노테이션들을 알아봅니다.

  •  Mockito 핵심 어노테이션
어노테이션 용도 설명
@Mock 가짜 객체 생성 테스트 대상이 의존하는 클래스의 모의 객체를 만듭니다. (예: UserRepository의 가짜 객체)
@InjectMocks 테스트 대상 정의 실제로 테스트하려는 서비스/컨트롤러 객체를 생성하고, 여기에 위에 정의된 @Mock 객체들을 자동으로 주입합니다.
@ExtendWith Mockito 확장 JUnit 5 환경에서 Mockito 어노테이션을 사용할 수 있도록 환경을 설정합니다. (@ExtendWith(MockitoExtension.class))

 

  • Mockito를 사용한 테스트 예시

다음 코드는 OrderService가 OrderRepository에 의존할 때, DB 접속 없이 OrderService의 로직만 테스트하는 방법을 보여줍니다.

// OrderServiceTest.java

import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import static org.mockito.Mockito.when;
import org.mockito.junit.jupiter.MockitoExtension; 
import org.junit.jupiter.api.extension.ExtendWith; // 🚨 반드시 필요

@ExtendWith(MockitoExtension.class) // Mockito 기능을 사용하도록 JUnit 5 확장
class OrderServiceTest {

    @Mock // 가짜 DB 레포지토리
    OrderRepository orderRepository; 

    @InjectMocks // 테스트 대상 (가짜 레포지토리가 자동으로 주입됨)
    OrderService orderService; 

    @Test
    void shouldReturnTrueIfOrderIsPlacedSuccessfully() {
        // Given (준비)
        long orderId = 101L;

        // 🚨 When (행동 지정: Stubbing)
        // 'orderRepository.save(any())'가 호출되면, true를 반환하도록 가짜 행동을 지정
        when(orderRepository.save(any())).thenReturn(true); 

        // When (실행)
        boolean isSuccess = orderService.placeOrder(orderId, 10000);

        // Then (검증)
        assertTrue(isSuccess);
        // verify(orderRepository).save(any()); // 🚨 (선택) 실제로 save 메서드가 호출되었는지 검증할 수도 있습니다.
    }
}

 

  • 목 서버 (Mock Server)의 필요성

단순한 객체 주입(Dependency Injection)을 넘어서, 서비스가 HTTP 통신을 통해 외부 시스템과 복잡하게 통신할 때는 WireMock 같은 목 서버가 필요합니다.

  • Mock Server 역할: 실제 외부 API의 URL을 가로채서, 개발자가 미리 정의한 JSON 응답을 즉시 반환해 줍니다.
  • 장점: RestTemplate이나 WebClient 같은 HTTP 클라이언트 코드를 건드리지 않고, 네트워크 레벨에서 테스트를 격리할 수 있습니다.

 

작성하게 된 계기는 최근에 단위테스트를 보통 어떻게 작성하냐는 질문에 intelliJ에서 주구장창 만드는게 @UnitTest인데도 설명을 잘 못 하겠더라는거죠... 음.. 목서버.. 디스플레이네임.. 어노테이션붙여서 쪼개서... 테스트 돌려서...

그래서 기본기를 다시 잡고가자는 느낌으로 유닛테스트 작성 가이드문서를 쓰게되었답니다 !

 

 

반응형