🍃SpringBoot

JUnit 5: JUnit 이해하기

limdaeil 2026. 1. 25. 21:24

🔖Contents

1. JUnit 이란

JUnit은 Java 진영에서 독립된 단위 테스트(Unit Test)를 지원해 주는 테스트 프레임워크입니다.

가장 많이 사용하는 JUnit 5는 Java 8 이상부터 지원하고, 2025년 09월에 정식 출시된 JUnit 6는 Java 17 이상부터 지원합니다. 앞으로 정리하는 모든 내용은 특별한 언급이 없는 이상, JUnit 5를 기준으로 설명한다는 점을 꼭 참고하기를 바랍니다. (JUnit 6 등장으로 반드시 JUnit 5를 제대로 공부해야 겠다는 생각으로 시작했어요... 💧)

1.1 JUnit 5 구성요소

JUnit 5에서는 JUnit Platform, JUnit Jupiter, JUnit Vintage 세 개로 분리했습니다. 테스트 실행 엔진(Platform), 최신 테스트 API(Jupiter), 하위 호환성(Vintage)으로 분리되어 있는데, 유연성, 확장성, 호환성을 높이기 위해 구조를 변경했습니다. 최신 자바 기능을 지원하고, 기존 3, 4 버전을 유지하면서도 새로운 기능(테스트 엔진 확장 등)을 쉽게 도입하기 위함입니다. 

 

출처: https://swtestacademy.com/junit-5-architecture/

1.2 JUnit Platform

테스트 실행 엔진(Platform)은 JVM에서 테스트 프레임워크를 ‘발견하고 실행’하기 위한 인터페이스입니다.  

IDE 또는 빌드 도구(Gradle, Maven)가 어떤 테스트 엔진을 사용하든 전부 동일한 방식으로 테스트를 탐색하고 실행할 수 있도록 통합 인터페이스를 제공한다는 점이 핵심입니다. 

 

JUnit Platform 핵심 역할 

JUnit Platform은 "테스트를 찾고, 계획하고, 실행하여, 결과를 리포팅하는 플랫폼의 기반"입니다. 
  • 테스트 엔진 인터페이스(TestEngine API) 제공: JUnit Jupiter(5), Vintage(4, 3)의 표준 인터페이스를 정의합니다.
  • 테스트 발견 및 계획(Discovery & Plan): 테스트 코드 탐색, 실행할 테스트 계획을 생성하여 테스트를 준비합니다.
  • 테스트 실행 런처(Launcher): IDE, 빌드 도구 등 다양한 환경에서 테스트를 실행할 수 있는 표준 런처를 제공합니다.
  • 테스트 엔진 통합: 단일 플랫폼에서 구버전(3, 4)과 신버전(5) 등 테스트 엔진을 동시에 사용할 수 있습니다.

1.3 JUnit Jupiter

JUnit 5를 위한 새로운 확장 기능과 라이브러리로 구성됩니다. Jupiter는 사실 2가지로 구성돼요.

1. JUnit Jupiter API: 테스트를 작성하는 문법, 애너테이션, Assertions은 Jupiter API에서 인터페이스로 제공

2. JUnit Jupiter Engine: 그 테스트를 실행하는 실제 구현체

 

JUnit Jupiter API 어노테이션 목록

분류 어노테이션 설명 및 주요 용도 실행 시점 / 특징
생명주기 (Lifecycle) `@BeforeAll` 전체 테스트 시작 전 공통 자원 설정 가장 먼저 1회 실행 (static)
`@BeforeEach` 각 테스트 메서드 실행 전 데이터 초기화 매 테스트 메서드 실행 전
`@Test` 실제 테스트 로직을 수행하는 메서드 테스트의 본체
`@AfterEach` 각 테스트 종료 후 사용한 자원 정리 매 테스트 메서드 실행 후
`@AfterAll` 전체 테스트 종료 후 공통 자원 해제 가장 나중에 1회 실행 (static)
관리 및 가독성 `@DisplayName` 테스트 결과에 표시될 커스텀 이름 지정 "로그인_성공_테스트" 등 명칭 부여
`@Disabled` 특정 테스트를 실행하지 않도록 비활성화 테스트 제외 (주석 처리와 유사)
`@Tag` 필터링을 위한 태그(라벨) 지정 Smoke, Regression 등 그룹화
`@Nested` 클래스 내부에 중첩 테스트 클래스 선언 계층 구조로 관련 테스트 묶기
고급 기능 `@ExtendWith` 외부 라이브러리나 확장 기능 등록 예: Mockito 연동 시 사용
`@TestFactory` 런타임에 생성되는 동적 테스트 선언 다이나믹 테스트 (Dynamic Test)
💡 팁: 실행 순서 기억하기
@BeforeAll → @BeforeEach → @Test → @AfterEach → @AfterAll 

 

1.4 JUnit Vintage

JUnit VintageJUnit 3, 4 테스트를 JUnit 5 플랫폼 위에서 실행하기 위한 엔진입니다.

기존 레거시 테스트(org.junit.Test 같은 JUnit 4 스타일)를 유지한 채, 실행 인프라는 JUnit 5(Platform)로 통일하고 싶을 때 사용합니다. Vintage는 “새로운 테스트 작성 방식”이 아니라, 레거시(JUnit 3, 4) 테스트를 실행하기 위한 호환 엔진입니다.

 

정리: 테스트 실행 엔진(Platform), 최신 테스트 API(Jupiter), 하위 호환성(Vintage)

  • Platform: 테스트를 발견하고 실행하는 공통 기반 (Launcher + Engine 구조)
  • Jupiter: JUnit 5의 새 테스트 모델 (API + 그걸 실행하는 Engine)
  • Vintage: JUnit 3, 4를 Platform 위에서 실행시키는 호환 Engine

2. JUnit 테스트 실행 흐름

앞에서 JUnit 5의 구조(JUnit Platform / Jupiter / Vintage)를 살펴봤다면, 이제는 JUnit이 실제로 테스트를 어떻게 실행하는지를 이해할 차례입니다. JUnit을 제대로 이해하려면 반드시 “IDE에서 테스트를 실행했을 때, 내부적으로 어떤 순서로 동작하는가?” 를 한 번 쯤은 살펴봐야 합니다.

JUnit 5 공식 홈페이지에서 라이브러리 다이어그램의 흐름은 다음과 같은 단계로 이루어집니다.

출처: https://docs.junit.org/5.3.2/user-guide/index.html

 

개발자가 테스트 코드를 작성 후 JUnit 5 실행의 전체 흐름

1. Launcher 실행

JUnit 테스트는 JUnit Platform의 Launcher를 통해 시작됩니다. IDE나 Gradle은 직접 테스트를 실행하지 않고 Launcher에게 위임합니다. 참고로, IntelliJ, Eclipse, Gradle, Maven 모두 JUnit Platform Launcher를 공통으로 사용합니다. Launcher의 핵심 역할은 다음과 같습니다.

  • 테스트 탐색 (Discovery)
  • 어떤 TestEngine(5, 4, other)을 사용할지 결정
  • 테스트 실행 요청
  • 실행 결과 수집 후 리스너를 통해 즉시 응답

2. 테스트 탐색 (Discovery)

Launcher는 테스트 코드를 찾습니다. 테스트를 찾는 기준은 다음과 같습니다.

  • `@Test`가 붙은 메서드
  • `@TestFactory`
  • `@Nested` 내부 테스트
  • Engine이 인식할 수 있는 클래스 구조

 3. Test Plan 생성

테스트가 발견되면, JUnit Platform은 실행할 테스트 목록을 기반으로 Test Plan(실행 계획) 을 생성합니다.

JUnit Platform은 테스트를 직접 실행하지 않습니다. 대신, TestEngine 인터페이스를 구현한 엔진에게 실행을 위임합니다.

  • Jupiter Engine: JUnit 5 테스트 실행
  • Vintage Engine: JUnit 3, 4 테스트 실행

4. 테스트 실행 및 결과 수집

Test Plan이 완성되면, 각 TestEngine이 실제 테스트를 실행합니다. 이때 실행 순서는 다음과 같습니다.

@BeforeAll → @BeforeEach → @Test → @AfterEach →  @AfterAll
 

각 테스트 결과는 성공, 실패, 스킵 여부, 예외 발생 여부, 실행 시간, 실패 원인(Stack Trace) 정보입니다.

IDE 또는 CI 도구에서 시각적으로 출력됩니다. 

3. Assertions (검증)

Assertions는 기대값(Expected)과 실제값(Actual)을 비교하여 테스트의 성공 여부를 판단하는 도구입니다.
JUnit에서 테스트의 핵심은 단연 검증으로, 아무리 테스트 메서드를 실행해도 검증이 없다면 테스트라고 할 수 없습니다.

JUnit 5에서는 다음 클래스에서 Assertion 기능을 제공합니다.

org.junit.jupiter.api.Assertions

3.1 Assertions 메서드 구조

Assertions는 대부분 다음 형태를 가집니다.

메서드 결과는 테스트가 실패하면 즉시 예외가 발생하고, IDE나 빌드 도구에서 실패 원인을 출력해 줍니다.

assertXxx(expected, actual);
assertXxx(condition);

 

1. `assertEquals(expected, actual);`

  - 사용 용도: 기대값(expeted)과 실제값(actual)이 같은지 비교

  - 실패 시: `AssertionError` 발생

@Test
void 더하기_테스트() {
    int result = 10 + 20;
    assertEquals(30, result);
}

 

2. `assertNotEquals(unexpected, actual);`

  -  사용 용도: 기대값(expeted)과 실제값(actual)이 다른지 비교

  -  실패 시: `AssertionError` 발생

assertNotEquals(0, result);

 

3. `assertNull(object), assertNotNull(object)`

  -  assertNull: 값이 null인지 확인

  -  assertNotNull: 값이 null이 아닌지 확인

assertNotNull(user);
assertNull(user.getDeletedAt());

 

4. `assertTrue(condition);, assertFalse(condition);`

  -  조건식이 true 또는 false 인지를 검증

  -  복잡한 조건 검증 시 자주 사용

assertTrue(user.isActive());
assertFalse(user.isDeleted());

 

5. `assertThrows(ExceptionClass.class, executable);`

  -  JUnit 5에서 가장 중요한 Assertion 중 하나

  -  ` ExceptionClass.class` 지정한 예외가 발생하면 테스트 성공

  -  ` ExceptionClass.class ` 예외가 발생하지 않거나, 다른 예외면 실패

  -  `try-catch`를 직접 쓰지 않아도 됨

  -  가독성, 안정성 모두 뛰어남

@Test
void 예외_발생_테스트() {
    assertThrows(IllegalArgumentException.class, () -> {
        service.create(null);
    });
}
Exception e = assertThrows(IllegalArgumentException.class, () -> {
    service.create(null);
});
// 예외 메시지까지 검증
assertEquals("값이 null입니다.", e.getMessage());

 

6. `assertAll()`

  -  여러 검증을 한 번에 실행

  -  하나가 실패해도 나머지 검증을 모두 실행

  -  모든 실패 결과를 한 번에 확인 가능

  -  DTO 테스트, 엔티티 테스트에 매우 유용

assertAll(
    () -> assertEquals("kim", user.getName()),
    () -> assertEquals(20, user.getAge()),
    () -> assertTrue(user.isActive())
);

 

7. ` assertTimeout()`

  -  성능 테스트

  -  무한 루프 방지

  -  특정 시간 내 실행 보장

  -  테스트가 지정 시간 초과 시 실패

assertTimeout(Duration.ofSeconds(1), () -> {
    heavyTask();
});

3.2 Assertions 정리 표

메서드 설명 사용 예
assertEquals(expected, actual) 값 비교 결과 검증
assertNotEquals(unexpected, actual) 값 불일치 예외 케이스
assertNull(object) null 확인 조회 실패
assertNotNull(object) null 아님 객체 생성
assertTrue(condition) 조건 true 상태 검증
assertFalse(condition) 조건 false 플래그 검증
assertThrows(ExceptionClass.class, excutable) 예외 발생 예외 테스트
assertAll() 다중 검증 객체 필드 검증
assertTimeout() 시간 제한 성능 테스트

4. JUnit 테스트 생명주기

지난 시간에 배운 @BeforeAll → @BeforeEach → @Test → @AfterEach → @AfterAll는 JUnit의 테스트 생명주기입니다. JUnit의 테스트 생명주기란 테스트가 실행되기 전부터 종료될 때까지 어떤 순서로 메서드가 호출되는지를 의미합니다. 테스트 생명주기를 이해하면 다음을 명확하게 알 수 있습니다.

 

1. `@BeforeAll`

  -  전체 테스트 실행 전 한 번만 실행

  -  테스트 클래스에서 가장 먼저 호출됨

  -  static 메서드여야 함 (기본 설정 기준)

@BeforeAll
static void setUpAll() {
    System.out.println("전체 테스트 시작 전 1회 실행");
}
  • `@BeforeAll` 용도: DB 연결, 공통 리소스 초기화, 테스트 환경 설정

 

2. `@BeforeEach`

  -  각 테스트 메서드 실행 전마다 호출

  -  테스트 간 독립성 보장에 핵심 역할

@BeforeEach
void setUp() {
    System.out.println("각 테스트 실행 전");
}
  • `@BeforeEach` 용도: 테스트용 객체 초기화, 공통 데이터 세팅

 

3. `@Test`

  -  실제 테스트 로직이 작성되는 메서드

  -  하나의 테스트 케이스를 의미

@Test
void 회원_생성_테스트() {
    assertNotNull(userService.create());
}

 

4. `@AfterEach`

  -  각 테스트 실행이 끝난 후 호출

  -  테스트에서 사용한 자원 정리

@AfterEach
void tearDown() {
    System.out.println("각 테스트 종료 후");
}
  • `@ AfterEach` 용도:메모리 정리, Mock 초기화, 테스트 데이터 제거

 

5. `@AfterAll`

  -  모든 테스트가 끝난 뒤 한 번만 실행

  -  static 필수 (기본 설정 기준)

@AfterAll
static void tearDownAll() {
    System.out.println("전체 테스트 종료 후 1회 실행");
}
  • `@ AfterAll` 용도: DB 연결 종료, 공용 리소스 해제

 

테스트 실행 순서 예시

class SampleTest {

    @BeforeAll
    static void beforeAll() {
        System.out.println("BeforeAll");
    }

    @BeforeEach
    void beforeEach() {
        System.out.println("BeforeEach");
    }

    @Test
    void test1() {
        System.out.println("Test1");
    }

    @Test
    void test2() {
        System.out.println("Test2");
    }

    @AfterEach
    void afterEach() {
        System.out.println("AfterEach");
    }

    @AfterAll
    static void afterAll() {
        System.out.println("AfterAll");
    }
}

 

테스트 실행 결과

  • `@BeforeAll, @AfterAll`은 딱 1번
  • `@BeforeEach, @AfterEach`는 테스트마다 반복
BeforeAll
BeforeEach
Test1
AfterEach
BeforeEach
Test2
AfterEach
AfterAll

 

4.1 JUnit 5의 테스트 인스턴스 생성 방식

`@BeforeAll, @AfterAll` 은 static이 아니면 호출할 수 없습니다. 그 이유는 JUnit 5의 기본 테스트 인스턴스 생성 방식은 테스트 메서드마다 새로운 인스턴스를 생성합니다. 그래서 테스트 인스턴스들이 생성되기 전에 실행되는 `@BeforeAll`은 반드시 static이어야만 합니다.

 

`@TestInstance`

JUnit 5에서는 테스트 인스턴스 생성 전략을 `@TestInstance`으로 변경할 수 있습니다.

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
  • `@TestInstance(PER_METHOD)`: 기본값
    • 테스트마다 새 인스턴스 생성
    • 가장 안전한 방식
    • 상태 공유 ❌
  • `@TestInstance(TestInstance.Lifecycle.PER_CLASS)`: PER_CLASS
    • 테스트 클래스당 인스턴스 1개
    • `@BeforeAll, @AfterAll`을 static 없이 사용 가능
    • 상태 공유 가능 (주의 필요)
@TestInstance(TestInstance.Lifecycle.PER_CLASS) // 클래스당 하나만 인스턴스 생성
class TestClass {

    int count = 0; // 테스트 간 상태 공유 가능

    @BeforeAll
    void setup() { // static일 필요 없음
        System.out.println("Setup");
    }

    @Test
    void test1() {
        count++;
        System.out.println("Test1: " + count); // 1
    }

    @Test
    void test2() {
        count++;
        System.out.println("Test2: " + count); // 2
    }
}

PER_CLASS 방식은 테스트 클래스 안에 작성된 모든 테스트 메서드가 단일 인스턴스를 공유하므로, 테스트 간에 인스턴스 필드(변수)를 공유하여 상태를 유지할 수 있습니다. 대신 테스트 간 의존성이 생기므로, 독립적인 테스트가 필요한 경우에는 기본값인 PER_METHOD 사용이 권장됩니다. 

 

5. 매개변수화 테스트

JUnit의 Parameterized Test란, 같은 테스트 로직을 여러 입력값으로 반복 실행하고 싶을 때 사용하는 기능입니다.

 

Parameterized Test 기본 구조

@ParameterizedTest
@XXXSource(...)
void testName(파라미터) {
    // 테스트 로직
}

- `@Test` 대신 `@ParameterizedTest` 사용
- 입력 값은 `@ValueSource, @CsvSource` 등으로 전달

 

`@ValueSource`

- 가장 기본적인 방식으로 하나의 타입만 전달할 때 사용

@ParameterizedTest
@ValueSource(ints = {2, 4, 6, 8})
void 짝수_테스트(int number) {
    assertTrue(number % 2 == 0);
}

- @ValueSource 지원 타입: ints, longs, doubles, strings, booleans

- @ValueSource 단점: 여러 개의 파라미터를 전달할 수 없음

 

`@CsvSource`

- 가장 많이 쓰이는 방식으로 여러 개의 값을 한 번에 전달이 가능

@ParameterizedTest
@CsvSource({
    "1, 1",
    "2, 4",
    "3, 9"
})
void 제곱_테스트(int input, int expected) {
    assertEquals(expected, input * input);
}

- @CsvSource는 실무에서 가장 많이 사용되고 가독성 좋음
- 다양한 케이스 테스트 가능

 

`@CsvFileSource`

- 외부 파일 기반 테스트로, CSV 파일에서 테스트 데이터를 불러와서 테스트 수행

@ParameterizedTest
@CsvFileSource(resources = "/data.csv", numLinesToSkip = 1)
void 파일_기반_테스트(String name, int age) {
    assertNotNull(name);
}

- @CsvFileSource는 대량 테스트에 적합하고, 테스트 데이터와 코드 분리 가능

- QA, 기획 데이터 활용 가능

 

`@NullSource, @EmptySource, @NullAndEmptySource`

@ParameterizedTest
@NullSource // null 값 테스트
void null_테스트(String value) {
    assertNull(value);
}

@ParameterizedTest
@EmptySource // 빈 값 테스트
void empty_테스트(String value) {
    assertTrue(value.isEmpty());
}

@ParameterizedTest
@NullAndEmptySource // 둘 다 테스트
void null_or_empty_테스트(String value) {
    assertTrue(value == null || value.isEmpty());
}

 

`@MethodSource`

- 가장 강력하고 실무에서 많이 쓰이는 방식입니다.

@ParameterizedTest
@MethodSource("provideNumbers")
void 테스트(int a, int b, int expected) {
    assertEquals(expected, a + b);
}

static Stream<Arguments> provideNumbers() {
    return Stream.of(
        Arguments.of(1, 2, 3),
        Arguments.of(3, 4, 7),
        Arguments.of(5, 5, 10)
    );
}

- 복잡한 객체 전달과 조건 분기 가능

- 가독성 좋고 실무에서 가장 많이 사용됨

5.1 Parameterized Test 정리 표

방식 사용 목적 특징
@ValueSource 단일 값 테스트 가장 단순
@CsvSource 여러 값 전달 가장 많이 사용
@CsvFileSource 파일 기반 대량 데이터
@NullSource null 테스트 예외 처리
@EmptySource 빈 값 테스트 문자열 검증
@MethodSource 복잡한 테스트 실무 최강