백기선 님의 인프런 강의 "더 자바, 애플리케이션을 테스트하는 다양한 방법"를 정리한 글입니다.
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.ExtensionAutoExtensionpublic 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");
    }
}
출처
'Code > Test' 카테고리의 다른 글
| [더 자바, 애플리케이션을 테스트하는 다양한 방법] 2. Mockito (0) | 2025.04.19 | 
|---|---|
| [단위 테스트] 9. 목 처리에 대한 모범 사례 (0) | 2025.03.01 | 
| [단위 테스트] 8-2. 통합 테스트를 하는 이유: 인터페이스 (1) | 2025.03.01 | 
| [단위 테스트] 8-1. 통합 테스트를 하는 이유: 통합 테스트 (0) | 2025.02.28 | 
| [단위 테스트] 7-2. 가치 있는 단위 테스트를 위한 리팩터링: 감사 시스템 (0) | 2025.02.28 |