Code/Test

[더 자바, 애플리케이션을 테스트하는 다양한 방법] 1. JUnit 5

noahkim_ 2025. 4. 19. 03:11

백기선 님의 인프런 강의 "더 자바, 애플리케이션을 테스트하는 다양한 방법"를 정리한 글입니다.

 

1. JUnit 5

  • 자바 개발자들이 가장 많이 사용하는 테스트 프레임워크. (93%의 자바 개발자가 JUnit 사용)

 

구조

구성 요소 설명
Platform
테스트 실행을 위한 런처 및 TestEngine API 제공
Jupiter
JUnit 5의 주요 TestEngine 구현체
Vintage
JUnit 4 및 3을 지원하는 TestEngine 구현체

 

시작하기

plugins {
    id 'java'
}

group 'com.example'
version '1.0-SNAPSHOT'

sourceCompatibility = '1.8'

repositories {
    mavenCentral()
}

dependencies {
    testImplementation 'org.junit.jupiter:junit-jupiter:5.10.0'
}

test {
    useJUnitPlatform()
}
  • 버전 요구: Java 8 이상
  • 스프링 부트 프로젝트에서는 JUnit 5 의존성이 기본적으로 포함됨.

 

JUnit 5 확장 모델

  • JUnit 4에서 @RunWith/@Rule을 대체하는 Extension

 

확장팩 등록 방법
등록 방법 설명 사용 위치
선언적 등록 @ExtendWith 애노테이션
테스트 클래스 또는 메서드에 명시적으로 등록
클래스 / 메서드
프로그래밍적 등록 @RegisterExtension 필드를 통해 런타임에 확장 등록 필드 (static/non-static)
자동 등록 ServiceLoader 기반으로 META-INF/services에 확장 클래스 등록 클래스패스

 

예제) 선언적 등록

더보기
@ExtendWith(MySimpleExtension.class)
public class ExtendWithExampleTest {

    @Test
    void testSomething() {
        System.out.println("Test executed");
    }
}

// 간단한 확장 클래스
class MySimpleExtension implements BeforeEachCallback {
    @Override
    public void beforeEach(ExtensionContext context) {
        System.out.println("BeforeEach from MySimpleExtension");
    }
}

 

예제) 프로그래밍적 등록

더보기
public class RegisterExtensionExampleTest {

    @RegisterExtension
    static MySimpleExtension extension = new MySimpleExtension();

    @Test
    void testWithRegisterExtension() {
        System.out.println("Test executed");
    }
}

class MySimpleExtension implements BeforeEachCallback {
    @Override
    public void beforeEach(ExtensionContext context) {
        System.out.println("BeforeEach from MySimpleExtension");
    }
}

 

예제) 자동 등록

더보기
public class AutoExtension implements BeforeEachCallback {
    @Override
    public void beforeEach(ExtensionContext context) {
        System.out.println("BeforeEach from AutoExtension (ServiceLoader)");
    }
}
src/test/resources/META-INF/services/org.junit.jupiter.api.extension.Extension
AutoExtension
public class ServiceLoaderExtensionTest {

    @Test
    void testWithServiceLoader() {
        System.out.println("Test executed");
    }
}

 

JUnit 4와 JUnit 5 비교

항목 JUnit 4 JUnit 5
태그 @Category(Class) @Tag(String)
확장 모델 @RunWith, @Rule
@ExtendWith, @RegisterExtension
테스트 전/후 @Before, @After
@BeforeEach, @AfterEach
테스트 전체 전/후 @BeforeClass, @AfterClass
@BeforeAll, @AfterAll

 

JUnit 4 마이그레이션

  • JUnit 4 테스트 실행: junit-vintage-engine 의존성 추가
  • @Rule 관련: @EnableRuleMigrationSupport 사용하여 ExternalResource, Verifier, ExpectedException 지원

 

SpringBoot 설정 파일 (junit-platform.properties)

junit.jupiter.testinstance.lifecycle.default = per_class
junit.jupiter.extensions.autodetection.enabled = true

 

 

2. 애노테이션

기본 애노테이션

애노테이션 설명
@Test 테스트 메서드 지정
@BeforeAll 모든 테스트 전에 한 번 실행 (static 필요)
@AfterAll 모든 테스트 후에 한 번 실행 (static 필요)
@BeforeEach 각 테스트 전에 실행
@AfterEach 각 테스트 후에 실행
@Disabled
테스트 실행하지 않음

 

예제

더보기
import org.junit.jupiter.api.*;

import static org.junit.jupiter.api.Assertions.assertEquals;

public class ExampleTest {

    @BeforeAll
    static void setUpBeforeClass() {
        System.out.println("🔧 Before All Tests");
    }

    @AfterAll
    static void tearDownAfterClass() {
        System.out.println("🧹 After All Tests");
    }

    @BeforeEach
    void setUp() {
        System.out.println("➡️ Before Each Test");
    }

    @AfterEach
    void tearDown() {
        System.out.println("⬅️ After Each Test");
    }

    @Test
    void additionTest() {
        System.out.println("✅ Running addition test");
        assertEquals(2, 1 + 1);
    }

    @Test
    @Disabled("준비 중인 테스트입니다")
    void disabledTest() {
        // 이 테스트는 실행되지 않습니다.
    }
}

 

 

테스트 이름 표시

애노테이션 설명
@DisplayName
개별 테스트 메서드나 클래스에 표시될 사용자 지정 이름 설정
@DisplayNameGeneration
테스트 이름 표기 방식 설정
(예: ReplaceUnderscores, IndicativeSentences)

 

예제) @DisplayNameGeneration

더보기

1. ReplaceUnderscores

@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
public class DisplayNameTest {

    @Test
    void login_should_succeed_with_correct_credentials() {
        assertTrue(true);
    }

    @Test
    @DisplayName("❌ 비밀번호가 틀리면 로그인에 실패해야 한다")
    void login_should_succeed_with_wrong_password() {
        assertTrue(true);
    }
}

 

 

2. IndicativeSentences

@DisplayNameGeneration(DisplayNameGenerator.IndicativeSentences.class)
public class DisplayNameTest {

    @Test
    void login_should_succeed_with_correct_credentials() {
        assertTrue(true);
    }

    @Test
    @DisplayName("❌ 비밀번호가 틀리면 로그인에 실패해야 한다")
    void login_should_succeed_with_wrong_password() {
        assertTrue(true);
    }
}

 

 

3. Assertions

  • Assertions API: org.junit.jupiter.api.Assertions 사용
메서드 설명
assertEquals(expected, actual)
두 값이 같은지 비교
assertNotNull(actual)
값이 null이 아님을 확인
assertTrue(condition)
주어진 조건이 true인지 확인
assertAll(executables...)
여러 조건을 묶어서 동시에 검증
assertThrows(expectedType, executable)
특정 예외가 발생하는지 확인
assertTimeout(duration, executable)
주어진 시간 내에 실행 완료되는지 확인

 

예제

더보기
public class AssertionTest {

    @Test
    void testAssertEquals() {
        int actual = 2 + 3;
        int expected = 5;
        assertEquals(actual, expected);
    }

    @Test
    void testAssertNotNull() {
        assertNotNull("Junit");
    }

    @Test
    void testAssertTrue() {
        assertTrue(20 > 10);
    }

    @Test
    void testAssertAll() {
        String name = "JUnit";
        int age = 10;

        assertAll("grouped assertions",
                () -> assertNotNull(name),
                () -> assertTrue(age > 5),
                () -> assertEquals(name, "JUnit")
        );
    }

    @Test
    void testAssertThrows() {
        assertThrows(IllegalArgumentException.class, () -> {
            throw new IllegalArgumentException("Invalid arguments");
        });
    }

    @Test
    void testAssertTimeout() {
        assertTimeout(Duration.ofSeconds(2), () -> {
            Thread.sleep(1000);
        });
    }
}

 

4. Assumptions

  • 특정 조건에서만 테스트 실행
항목 설명
assumeTrue(condition)
조건이 true일 때만 테스트 실행
assumeFalse(condition)
조건이 false일 때만 테스트 실행
assumingThat(condition, executable)
조건이 true일 때만 해당 블록 실행, 나머지는 실행됨
@EnabledOnOs(OS)
지정된 운영체제에서만 테스트 실행
@DisabledOnOs(OS)
지정된 운영체제에서는 테스트 실행 안 함
@EnabledIfSystemProperty(...)
시스템 속성이 지정된 값일 때만 테스트 실행
@EnabledIfEnvironmentVariable(...)
환경변수가 지정된 값일 때만 테스트 실행 (VM Options)

 

예제

더보기
public class AssumptionTest {
    @Test
    void testAssumeTrue() {
        assumeTrue(1+1==2, "조건이 true일 때만 실행됨");
    }

    @Test
    void testAssumeFalse() {
        assumeFalse(1+1==3, "조건이 false일 때만 실행됨");
    }

    @Test
    void testAssumingThat() {
        String env = "dev";
        assumingThat("dev".equals(env), () -> System.out.println("dev 환경에서만 실행됨"));
    }

    @Test
    @EnabledOnOs(OS.MAC)
    void onlyOnMac() {
        System.out.println("Mac OS 환경에서만 실행됨");
    }

    @Test
    @DisabledOnOs(OS.WINDOWS)
    void notOnlyWindows() {
        System.out.println("Window OS 환경에서는 실행되지 않음");
    }

    @Test
    @EnabledIfSystemProperty(named = "user.country", matches="KR")
    void onlyIfSystemPropertyMatches() {
        System.out.println("user.country 시스템 속성이 'KR' 일 때만 실행됨");
    }

    @Test
    @EnabledIfEnvironmentVariable(named = "ENV", matches="staging")
    void onlyIfEnvVarMatches() {
        System.out.println("ENV 환경변수가 'staging'일 때만 실행됨");
    }
}

 

5. 태깅 및 필터링

@Tag

  • 실행 시 해당 태그를 기준으로 필터링 가능
  • 테스트 메서드 or 클래스에 추가

 

예제

더보기
public class TagTest {
    @Test
    @Tag("fast")
    void fastTest() {
        System.out.println("빠른 테스트 실행");
    }

    @Test
    @Tag("slow")
    void slowTest() {
        System.out.println("느린 테스트 실행");
    }
}
test {
    useJUnitPlatform {
        includeTags 'fast'
        excludeTags 'slow'
    }
}

./gradlew test

 

6. 커스텀 태그

  • 애노테이션을 조합하여 커스텀 태그 생성

 

예시

더보기
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Tag("fast")
@Test
public @interface FastTest { }

 

7. 테스트 반복하기

애노테이션 설명
@RepeatedTest(int)
지정한 횟수만큼 테스트를 반복 실행
@ParameterizedTest
매개변수를 사용하여 여러 값으로 반복 테스트
- @ValueSource
단일 값 목록을 소스로 제공
- @CsvSource
CSV 형식으로 여러 인자 제공
- @MethodSource
메서드로부터 인자 목록을 받아 테스트

 

예시

더보기
public class RepeatedAndParameterizedTest {

    @RepeatedTest(value = 3, name = "반복 테스트 {currentRepetition} / {totalRepetitions}")
    void repeatedTest(RepetitionInfo repetitionInfo) {
//        System.out.println("실행 번호: " + repetitionInfo.getCurrentRepetition());
    }

    @ParameterizedTest
    @ValueSource(strings = {"apple", "banana", "cherry"})
    void testWithValueSource(String fruit) {
        System.out.println("과일: " + fruit);
        assertNotNull(fruit);
    }

    @ParameterizedTest
    @CsvSource({
            "apple, 5",
            "banana, 3",
            "cherry, 7"
    })
    void testWithCsvSource(String fruit, int cnt) {
        System.out.println("과일: " + fruit + " 개수: " + cnt);
        assertTrue(cnt > 0);
    }

    @ParameterizedTest
    @MethodSource("provideStringsForTest")
    void testWithMethodSource(String language, int version) {
        System.out.println(language + " 버전: " + version);
    }

    static Stream<Arguments> provideStringsForTest() {
        return Stream.of(
                Arguments.of("java", 8),
                Arguments.of("kotlin", 1),
                Arguments.of("spring", 5)
        );
    }
}

 

8. 테스트 인스턴스

항목 설명
기본 동작
테스트 메소드마다 새로운 인스턴스를 생성
테스트 간 상태 공유 없음
@TestInstance(Lifecycle.PER_CLASS)
테스트 클래스 전체에서 하나의 인스턴스를 사용하여 상태 공유 가능
테스트 간 상태 공유가 필요할 경우, @BeforeEach 또는 @AfterEach로 초기화

 

예시

더보기
public class LifecycleTest {

    private int count = 0;

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

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

test1 count: 1
test2 count: 1

 

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class LifecyclePerClassTest {

    private int count = 0;

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

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

test1 count: 1
test2 count: 2

 

9. 테스트 순서

@TestMethodOrder

  • 테스트 메소드의 실행 순서를 정의할 때 사용
  • MethodOrderer.OrderAnnotation
  • MethodOrderer.Alphanumeric
  • MethodOrderer.Random
  • 커스텀

 

예시) MethodOrderer.OrderAnnotation

더보기
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class OrderedTest {

    @Test
    @Order(2)
    void secondTest() {
        System.out.println("second test");
    }

    @Test
    @Order(1)
    void firstTest() {
        System.out.println("first test");
    }

    @Test
    @Order(3)
    void thirdTest() {
        System.out.println("third test");
    }
}

 

예시) Custom

더보기
public class CustomSpeedOrdered implements MethodOrderer {

    @Override
    public void orderMethods(MethodOrdererContext methodOrdererContext) {
        methodOrdererContext.getMethodDescriptors().sort(Comparator.comparingInt(descriptor -> {
            String name = descriptor.getMethod().getName();

            if (name.endsWith("fast")) return 1;
            else if (name.endsWith("medium")) return 2;
            else if (name.endsWith("slow")) return 3;

            return Integer.MAX_VALUE;
        }));
    }
}
@TestMethodOrder(CustomSpeedOrdered.class)
public class CustomOrderTest {

    @Test
    void test_medium() {
        System.out.println("medium test");
    }

    @Test
    void test_fast() {
        System.out.println("fast test");
    }

    @Test
    void test_slow() {
        System.out.println("slow test");
    }
}

 

 

출처