Software Engineering/Test & Quality

[TDD] 2. JUnit 5

foreverWon 2025. 8. 18. 15:34
목차

    1. JUnit 5 구조

    구성 요소  설명 주요 클래스 / 의존성
    JUnit Platform 실행 기반 플랫폼 Launcher, Engine SPI
    JUnit Jupiter JUnit 5 API 및 실행 junit-jupiter-api, junit-jupiter-engine
    JUnit Vintage JUnit 3, 4 호환 계층 junit-vintage-engine + JUnit 4

    1) JUnit Platform

    • 테스트 실행을 위한 런타임 환경
    • Jupiter든 Vintage든 모두 Platfrom위에서 실행
    • 주요 모듈
      모듈 설명
      junit-platform-engine Jupiter, Vintage와 같은 엔진 구현을 위한 SPI 제공
      junit-platform-launcher IDE, Gradle, Maven과 같은 실행기와 연결 역할
      junit-platform-console 커맨드라인에서 JUnit 테스트 실행 가능하게 함
      junit-platform-commons 공통 유틸리티 및 에러 처리 지원

    2) JUnit Jupiter

    • JUnit 5의 새로운 테스트 API
    • 주요 구성
      모듈 설명
      junit-jupiter-api 테스트 작성에 필요한 애노테이션, API 제공
      junit-jupiter-engine 테스트 실행 엔진 (Platform SPI 구현체)
      junit-jupiter-params 파라미터화 테스트 기능 제공
    • 예제 코드
    import org.junit.jupiter.api.*;
    
    class CalculatorTest {
    
        @BeforeAll
        static void setupAll() {
            System.out.println("✔ Before All - 클래스 전체에서 단 한 번 실행");
        }
    
        @BeforeEach
        void setup() {
            System.out.println("✔ Before Each - 각 테스트 전 실행");
        }
    
        @Test
        void additionTest() {
            Assertions.assertEquals(4, 2 + 2);
        }
    
        @AfterEach
        void teardown() {
            System.out.println("✔ After Each - 각 테스트 후 실행");
        }
    
        @AfterAll
        static void teardownAll() {
            System.out.println("✔ After All - 클래스 전체에서 단 한 번 실행");
        }
    }
    • @BeforeAll, @AfterAll은 static이어야 하며 클래스 레벨에서 한 번만 실행됨
    • @Test는 JUnit 5의 org.junit.**jupiter**.api.Test를 사용

    3) JUnit Vintage

    • Unit 3, 4 기반의 테스트를 JUnit 5 환경에서 실행가능하게 하는 호환 계층
    • 예제
    import org.junit.Test;                     // ✅ JUnit 4 전용 애너테이션
    import static org.junit.Assert.*;         // ✅ JUnit 4 전용 Assertion 클래스
    
    public class LegacyTest {
        @Test                                  // ✅ JUnit 4 스타일
        public void testOldWay() {
            assertEquals(2, 1 + 1);            // ✅ JUnit 4의 static import된 메서드 사용
        }
    }
    • 의존성 추가
    // build.gradle
    dependencies {
        testImplementation 'org.junit.vintage:junit-vintage-engine:5.9.3'
        testImplementation 'junit:junit:4.13.2' // JUnit 4도 함께 필요함
    }

     

    • junit-vintage-engine을 추가하면 기존 JUnit 4 테스트를 유지하면서 JUnit 5로 점진적 전환 가능

    2. 기본 어노테이션

    애노테이션 설명 시스템 시작 시점 사용 변수
    @Test 테스트 메서드와 같은 것을 결정 매 테스트 메서드에 대해 -
    @BeforeEach 테스트 전에 실행 매 테스트 시작 전 -
    @AfterEach 테스트 후에 실행 매 테스트 종료 후 -
    @BeforeAll 전체 테스트 시작 전에 1회 한 클래스 시작전 static or PER_CLASS
    @AfterAll 전체 테스트 종료 후 1회 한 클래스 종료 후 static or PER_CLASS
    @DisplayName 테스트 명세화에 사용 - -

    1) @Test: 테스트 메서드 정의

    • return type은 반드시 void
    • 예외 던질 수 있음(= 실패 테스트 가능)

    2) @BeforeEach / @AfterEach: 각 테스트 전/후에 실행

    3) @BeforeAll / @AfterAll: 테스트 전체 전/후에 1회 실행

    • static 메서드로 선언
    • static 생략 위해 추가: @TestInstance(TestInstance.Lifecycle.PER_CLASS)

    4) @DisplayName: 테스트 설명 지정

    • 문서화 도구, 리포트 도구(JUnit HTML Report 등)에서 특히 유용
    import org.junit.jupiter.api.DisplayName;
    import org.junit.jupiter.api.Test;
    
    class DisplayNameTest {
    
        @Test
        @DisplayName("2과 3의 평균값은 2.5이고, 조금의 포인트가 있다")
        void averageCalculationTest() {
            double avg = (2 + 3) / 2.0;
            assert avg == 2.5;
        }
    }

    5) 실무 팁

    • @BeforeEach/@AfterEach
      • 테스트 간 상태 격리에 유리
      • 테스트 환경을 초기화하거나, 로그를 정리하는 데 유용
    • @BeforeAl/@AfterAll
      • DB 연결 또는 외부 API 서버 세팅처럼 비용이 큰 작업을 한 번만 실행할 때 사용
    • @DisplayName
      • 협업 상황에서 테스트 의미를 명확히 전달하고자 할 때 필수적으로 사용

    3. Assertion

    : 특정 조건이 참(true)인지 확인하며, 조건이 만족되지 않으면 테스트를 실패로 처리

    1) BDD와 Given-When-Then 스타일

    • Given: 테스트에 필요한 입력 값 및 전제 조건 설정
    • When: 테스트할 대상 메서드 호출
    • Then: 기대한 결과를 Assertion으로 검증

    2) 기본 검증 메서드

    메서드 설명
    assertEquals(expected, actual) 예상값과 실제값이 같은지 비교
    assertNotEquals(unexpected, actual) 예상하지 않은 값과 다른지 비교
    assertTrue(condition) 조건이 true인지 확
    assertFalse(condition) 조건이 false인지 확인
    assertNull(object) 객체가 null인지 확인
    assertNotNull(object) 객체가 null이 아닌지 확인
    • 예제
    /**
     * 기본적인 assertEquals, assertNotEquals, assertTrue 등을 활용한 단순 검증 테스트
     */
    @Test
    void basicAssertions() {
        // given
        int a = 2;
        int b = 3;
    
        // when
        int result = a + b;
    
        // then
        assertEquals(5, result);              // 예상값과 실제값이 같은지 확인
        assertNotEquals(6, result);           // 예상하지 않은 값과 다른지 확인
        assertTrue(result > 0);               // 조건이 true인지 확인
    
        String value = null;
        assertNull(value);                    // null 여부 확인
    
        value = "Hello";
        assertNotNull(value);                 // null이 아닌지 확인
    }

    3) 그룹 검증: assertAll()

    : 여러 조건을 하나의 테스트 내에서 모두 검증하고자 할 때 사용

    • 예제
    /**
     * assertAll을 활용하여 복수 조건을 한 번에 검증하는 테스트입니다.
     */
    @Test
    void groupAssertions() {
        // given
        String firstName = "Lee";
        String lastName = "Jungmin";
    
        // then
        assertAll(
                "이름 검증",
                () -> assertEquals("Lee", firstName),            // 이름 일치 여부 확인
                () -> assertEquals("Jungmin", lastName),        // 성 일치 여부 확인
                () -> assertTrue(firstName.length() <= 5)       // 길이 제한 확인
        );
    }
    • 하나의 assertion이 실패해도 다른 assertion을 계속 실행하여 전체 검증 결과 제공

    4) 예외 검증: assetThrows()

    : 예외가 실제 발생해야만 테스트가 통과하며, 예외 메시지까지 검증하면 테스트 품질이 높아짐

    • 예제
    /**
     * 예외가 발생해야 하는 상황을 테스트하는 assertThrows 사용 예시입니다.
     */
    @Test
    void exceptionTesting() {
        // when & then
        IllegalArgumentException thrown = assertThrows(
                IllegalArgumentException.class,
                () -> {
                    throw new IllegalArgumentException("잘못된 인자입니다.");
                }
        );
    
        // 예외 메시지까지 함께 검증
        assertEquals("잘못된 인자입니다.", thrown.getMessage());
    }

    5) BDD 스타일 예제

    public class StampCalculator {
        public static int calculateStampCount(int nowCount, int earned) {
            return nowCount + earned;
        }
    
        public static int calculateEarnedStampCount(Order order) {
            return order.getOrderCoffees().stream()
                    .map(OrderCoffee::getQuantity)
                    .mapToInt(Integer::intValue)
                    .sum();
        }
    }

     

    // 테스트 클래스
    import org.junit.jupiter.api.DisplayName;
    import org.junit.jupiter.api.Test;
    
    import java.util.List;
    
    import static org.junit.jupiter.api.Assertions.*;
    
    // 테스트 대상 클래스: StampCalculator
    public class StampCalculatorTest {
    
        @Test
        @DisplayName("현재 스탬프 수와 적립 스탬프 수를 더해 총합을 반환한다.")
        void calculateStampCountTest() {
            // given: 현재 스탬프 개수와 이번 주문으로 적립된 스탬프 수
            int nowCount = 5;
            int earned = 3;
    
            // when: 스탬프 총합 계산
            int actual = StampCalculator.calculateStampCount(nowCount, earned);
    
            // then: 기대값은 8
            int expected = 8;
            assertEquals(expected, actual);
        }
    
        @Test
        @DisplayName("주문에 포함된 모든 커피 수량을 합산해 적립 스탬프 수를 계산한다.")
        void calculateEarnedStampCountTest() {
            // given: 커피 2잔 각각 수량 3, 5로 설정된 주문 생성
            Order order = new Order();
            OrderCoffee coffee1 = new OrderCoffee();
            coffee1.setQuantity(3);
            OrderCoffee coffee2 = new OrderCoffee();
            coffee2.setQuantity(5);
            order.setOrderCoffees(List.of(coffee1, coffee2));
    
            // when: 적립 스탬프 수 계산
            int actual = StampCalculator.calculateEarnedStampCount(order);
    
            // then: 기대값은 3 + 5 = 8
            int expected = 8;
            assertEquals(expected, actual);
        }
    }

    6) 정리

    기능 메서드 예시
    값 비교 assertEquals, assertNotEquals assertEquals(5, result)
    조건 확인 assertTrue, assertFalse assertTrue(a > 0)
    null 확인 assertNull, assertNotNull assertNotNull(obj)
    그룹 검증 assertAll 여러 조건을 한 번에 검사
    예외 검증 assertThrows assertThrows(IllegalArgumentException.class, ...)
    BDD 스타일 given-when-then 테스트 목적 가독성 향상

    4. 테스트 라이프사이클

    1) 테스트 인스턴스의 생명주기

    • PER_METHOD: 기본적으로 테스트 메서드마다 새로운 인스턴스 생성
    • PER_CLASS: 테스트 메서드 간 상태 공유가 필요한 경우, 클래스 단위로 한 번만 인스턴스 생성
    @TestInstance(TestInstance.Lifecycle.PER_CLASS)
    class SampleLifecycleTest {
    
        private int counter = 0;
    
        @Test
        void test1() {
            counter++;
            System.out.println("test1 실행 후 counter = " + counter);
        }
    
        @Test
        void test2() {
            counter++;
            System.out.println("test2 실행 후 counter = " + counter);
        }
    }
    // PER_CLASS
    test1 실행 후 counter = 1
    test2 실행 후 counter = 2
    
    // PER_METHOD
    test1 실행 후 counter = 1
    test2 실행 후 counter = 1

    2) 테스트 전후 후킹 메서드 (Setup/Teardown)

    : 테스트 실행 전후에 특정 메서드를 자동으로 실행해주는 후킹(Hook) 애노테이션

    애노테이션 실행 시점 static 여부 PER_CLASS 필요 여부 설명
    @BeforeAll 모든 테스트 실행 전 1회 기본적으로 static이어야 함 PER_CLASS일 경우 static 생략 가능 테스트 환경 초기화용
    @BeforeEach 각 테스트 직전마다 static 아님 필요 없음 공통 준비 작업 수행
    @AfterEach 각 테스트 직후마다 static 아님 필요 없음 리소스 정리, 로그 등
    @AfterAll 모든 테스트 실행 후 1회 기본적으로 static이어야 함 PER_CLASS일 경우 static 생략 가능 리소스 반환, 마무리 처리 등
    • 후킹 메서드 활용 전략
      목적 활용 애노테이션 설명
      DB 연결 및 공용 자원 초기화 @BeforeAll 테스트 전 단 한 번만 설정
      공통 입력값, 객체 준비 @BeforeEach 각 테스트에서 독립적으로 활용
      로그 출력, 리소스 닫기 @AfterEach 메모리 누수 방지에 효과적
      DB 연결 해제, 종료 로그 @AfterAll 전체 테스트 종료 후 정리 작업

    3) 테스트 순서 결정

    : 테스트 간 의존성을 방지하기 위해, 테스트 메서드의 실행 순서 보장 X

    • 순서를 명시하고 싶은 경우
      애노테이션 설명
      @TestMethodOrder(...) 테스트 메서드의 실행 순서를 정의
      @Order(int) 실행 순서를 지정할 숫자를 설정 (낮을수록 먼저 실행됨)
    • 순서 사용 시점
      상황  권장 여부 설명
      단위 테스트(Unit Test) 비권장 독립적인 실행이 원칙. 순서에 의존하지 말아야 함
      통합 테스트(Integration Test) 제한적 허용 복잡한 준비-검증-정리 과정에서는 순서를 강제하는 경우 존재
      데이터베이스 시나리오 테스트 가능 예: 사용자 생성 → 수정 → 삭제 순서 검증이 필요한 경우

    4) 테스트 그룹화: Nested 개발

    : 테스트 클래스 내에서 내부 클래스를 사용해 관련 테스트들을 그룹화

    • 유사 기능끼리 묶어서 보기 쉽게 만들 수 있음
    • 각 @Nested 내부에도 @BeforeEach, @AfterEach 등을 별도 정의 가능
    • 계층적 구조로 테스트 코드 관리 가능
    class UserServiceTest {
    
        @Nested
        class LoginTest {
            @BeforeEach
            void setupLogin() {
                System.out.println("[Login 준비]");
            }
    
            @Test
            void successLogin() {
                System.out.println("로그인 성공");
            }
        }
    
        @Nested
        class SignupTest {
            @BeforeEach
            void setupSignup() {
                System.out.println("[Signup 준비]");
            }
    
            @Test
            void successSignup() {
                System.out.println("회원가입 성공");
            }
        }
    }
    // 실행 결과
    
    [Login 준비]
    로그인 성공
    [Signup 준비]
    회원가입 성공
    728x90

    'Software Engineering > Test & Quality' 카테고리의 다른 글

    [TDD] 4. Mockito  (0) 2025.08.19
    [TDD] 3. Spring Data JPA: 데이터 액세스 계층 테스트  (1) 2025.08.18
    상황에 맞는 예외 정리  (4) 2025.08.14
    [TDD] 1-2. Spring Test 모듈  (2) 2025.08.13
    [TDD] 1-1. Spring Test  (1) 2025.08.13