728x90
[Junit5] Junit5 정리 1 - Junit5에서 제공하는 어노테이션
JUnit 5는 크게 JUnit Platform(실행 기반), JUnit Jupiter(새 테스트 API/엔진), JUnit Vintage(JUnit4 호환)로 구성됩니다. 실무에서 우리가 쓰는 어노테이션은 대부분 Jupiter에 있습니다.
- JUnit Platform: 테스트 실행 플랫폼(런너).
- JUnit Jupiter: JUnit 5 API와 엔진(우리가 쓰는 어노테이션 대부분).
- JUnit Vintage: JUnit4 테스트 실행 호환.
표 정리
| 어노테이션 | 역할 |
| @Test | 일반 테스트 |
| @DisplayName | 테스트 이름 꾸미기 |
| @Disabled | 임시 비활성화 |
| @Timeout | 시간 제한 |
| @RepeatedTest | 반복 실행 |
| @BeforeAll / @AfterAll | 전체 전/후 1회 |
| @BeforeEach / @AfterEach | 각 테스트 전/후 |
| @Nested | 테스트 계층화 |
| @TestInstance | 인스턴스 라이프사이클 |
| @TestMethodOrder / @Order | 실행 순서 지정 |
| @Tag | 태깅/필터링 |
| @ParameterizedTest | 파라미터화 테스트 |
| @TestFactory | 동적 테스트 |
| @ExtendWith | 확장 등록 |
1. @Test
- 역할: 표준 테스트 메서드 지정
- 적용 위치: 메서드
- 언제: 단위/슬라이스/통합 전반
- 실무 팁: 메서드명 행위_should결과; 실패 메시지는 구체적으로
- 특징/제약
- 가시성 public 강제 아님(패키지 프라이빗/protected도 OK)
- @Test와 @ParameterizedTest는 동시에 사용 불가
- 예외 기대 시 assertThrows/assertDoesNotThrow 사용
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class CalculatorTest {
// @Test: 단위 테스트의 기본 어노테이션
@Test
void add_shouldReturnSum() {
// given
int a = 2, b = 3;
// when
int result = a + b;
// then
assertEquals(5, result); // 2 + 3 = 5 검증
}
}
2. @DisplayName / @DisplayNameGeneration
- 역할: 테스트/클래스 이름을 사람이 읽기 좋게
- 적용 위치: 클래스, 메서드
- 언제: CI 로그/리포트 가독성 중요할 때
- 실무 팁: 요구사항/티켓 ID 포함(추적성↑)
- 특징/제약
- @DisplayName이 있으면 @DisplayNameGeneration보다 우선
- built-in 제너레이터(예: ReplaceUnderscores, IndicativeSentences) 활용
import org.junit.jupiter.api.*;
@DisplayName("계산기 모듈 테스트")
class DisplayNameExampleTest {
@Test
@DisplayName("1) 양수+양수 덧셈은 정상 동작")
void testAddPrettyName() {}
}
3. @Disabled
- 역할: 테스트/클래스 임시 비활성화
- 적용 위치: 클래스, 메서드
- 언제: 외부 의존 장애, 릴리즈 직전 임시 제외
- 실무 팁: 사유/재활성 조건(티켓 번호 등) 반드시 기록
- 특징/제약
- 조건부 비활성은 @Disabled*(환경/OS 등) 계열 사용 권장
import org.junit.jupiter.api.*;
@Disabled("릴리즈 2.3에서 다시 활성화 예정 (JIRA-1234)")
class DisabledExampleTest {
@Test
void willNotRun() {}
}
4. @Timeout
- 역할: 최대 수행 시간 제한
- 적용 위치: 클래스, 메서드
- 언제: IO/DB/외부 API 의존·성능 가드레일
- 실무 팁: 통합 테스트에 보수적으로 적용, 타임아웃 발생 시 로깅 강화
- 특징/제약
- 타임아웃은 테스트 스레드를 인터럽트하는 방식이라, 네이티브/블로킹 호출은 즉시 중단이 보장되지 않을 수 있음
- 코드 블록 단위 제어가 필요하면 assertTimeout(…)/assertTimeoutPreemptively(…) 대안 고려
import org.junit.jupiter.api.*;
import java.util.concurrent.TimeUnit;
class TimeoutTest {
@Test
@Timeout(value = 800, unit = TimeUnit.MILLISECONDS)
void shouldFinishUnder800ms() throws Exception { Thread.sleep(100); }
}
5. @RepeatedTest
- 역할: 동일 테스트 N회 반복
- 적용 위치: 메서드
- 언제: 비결정성/레이스 조건/캐시 워밍업 검증
- 실무 팁: 랜덤 관련 테스트는 시드 고정으로 재현성 확보
- 특징/제약
- @Test와 병행 사용 불가(대체 관계)
- 반복 인덱스를 파라미터 주입(RepetitionInfo)로 받을 수 있음
import org.junit.jupiter.api.RepeatedTest;
class RepeatExample {
@RepeatedTest(5)
void flaky_shouldStabilize() { /* ... */ }
}
6. @TestMethodOrder / @Order
- 역할: 테스트 실행 순서 지정
- 적용 위치: 클래스(@TestMethodOrder), 메서드(@Order)
- 언제: 시나리오/마이그레이션 순차 검증(단위 테스트에선 지양)
- 실무 팁: 순서 의존은 테스트 독립성 저해하므로 최소화 해야 함
- 특징/제약
- 다양한 Orderer 제공(Annotation, DisplayName, MethodName, Random)
- 순서 결정은 엔진/런타임에 의존 → 결과에 의존 로직 두지 말 것
import org.junit.jupiter.api.*;
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class OrderedExample {
@Test @Order(1) void step1() {}
@Test @Order(2) void step2() {}
}
7. @TestInstance
- 역할: 테스트 클래스 인스턴스 라이프사이클
- 적용 위치: 클래스
- 언제: 생성 비용이 비쌀경우 사용, 그 외는 @BeforeAll/@AfterAll을 인스턴스 메서드로 사용
- PER_METHOD : 기본 값, 테스트 메서드마다 새 인스턴스를 만들고 실행(테스트 간 상태 고립 ↑)
- PER_CLASS : 테스트 클래스당 인스턴스 하나로 모든 테스트 메서드를 실행(공유 상태 가능, 비싼 초기화 1회만)
- 실무 팁: PER_CLASS는 상태 공유 부작용 주의(필드 변경 최소화)
- 특징/제약
- PER_CLASS일 때만 비-static @BeforeAll/@AfterAll 허용
import org.junit.jupiter.api.*;
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class PerClassExample {
@BeforeAll void initOnce() { /* 비싼 초기화 */ }
}
8. @BeforeAll / @AfterAll
- 역할: 전체 테스트 전/후 1회
- 적용 위치: 메서드
- 언제: 컨테이너/서버 기동, 대량 픽스처 로드/정리
- 실무 팁: 리소스 누수 방지(정상 종료 보장), 로그로 소요 시간 측정
- 특징/제약
- 기본 라이프사이클(PER_METHOD)에서는 static 필요
- @Nested에서 사용 제약 존재 → 상위에서 처리하거나 PER_CLASS 고려
import org.junit.jupiter.api.*;
class LifecycleExampleTest {
@BeforeAll
static void initAll() {
// 전체 테스트에 공통 1회 초기화 (예: WireMock 서버 부팅)
}
@AfterAll
static void tearDownAll() {
// 전체 테스트 종료 후 1회 정리
}
@Test
void sample() {}
}
9. @BeforeEach / @AfterEach
- 역할: 각 테스트 직전/직후
- 적용 위치: 메서드
- 언제: 트랜잭션 시작/롤백, 공통 픽스처 준비/정리
- 실무 팁: 격리 보장(공유 상태 제거, 테스트 간 영향 차단)
- 특징/제약
- 예외 발생 시 해당 테스트는 실패로 집계, 후속 훅/테스트 흐름에 영향
import org.junit.jupiter.api.*;
class LifecycleExampleTest {
@BeforeEach
void initEach() {
// 각 테스트 전에 공통 준비 (예: 트랜잭션 시작)
}
@AfterEach
void tearDownEach() {
// 각 테스트 후 정리 (예: 트랜잭션 롤백)
}
@Test
void sample() {}
}
10. @Nested
- 역할: 컨텍스트/상태 기반 계층화
- 적용 위치: 내부 클래스
- 언제: 중첩(내부) 테스트 클래스를 선언해서, 시나리오/컨텍스트별로 테스트를 계층적으로 그룹화할 때 사용
- 실무 팁: 공통 픽스처는 상위에, 상태 변경은 하위에 최소화
- 특징/제약
- 내부 클래스는 non-static이어야 함
- 상위 클래스의 라이프사이클/픽스처 영향을 받음
import org.junit.jupiter.api.*;
import static org.assertj.core.api.Assertions.*;
@DisplayName("회원 서비스")
class MemberServiceTest {
MemberService sut;
@BeforeEach
void setUp() {
sut = new MemberService(/* stub/repo/mock */);
}
@Nested
@DisplayName("가입 시")
class SignUp {
@BeforeEach
void given_basicProfile() {
// 공통 사전조건
}
@Test
@DisplayName("정상 입력이면 가입 성공")
void ok() {
boolean result = sut.signUp("jay@email.com", "1234");
assertThat(result).isTrue();
}
@Test
@DisplayName("중복 이메일이면 실패")
void duplicate_email() {
// when
// then
}
@Nested
@DisplayName("비밀번호 정책 위반")
class PasswordPolicy {
@Test
@DisplayName("너무 짧으면 실패")
void too_short() {}
}
}
@Nested
@DisplayName("조회 시")
class Find {
@Test
@DisplayName("존재하지 않으면 예외")
void not_found() {}
}
}
11. @ParameterizedTest (+ 소스 어노테이션들)
- 역할: 동일 로직을 다양한 입력으로 반복
- 적용 위치: 메서드
- 언제: 경계값/조합 폭발/규칙 검증/대량 케이스
- 실무 팁: CSV/파일 소스로 리뷰 친화적 테스트 데이터 관리
- 특징/제약
- @Test와 동시 사용 불가
- @MethodSource 공급자 메서드는 기본 static(단, 클래스가 PER_CLASS면 인스턴스 메서드 허용)
- @ValueSource는 null 미지원 → @NullSource/@EmptySource/@NullAndEmptySource 사용
- @CsvFileSource는 클래스패스에서 파일 로딩, numLinesToSkip로 헤더 스킵
- @EnumSource는 names/mode(INCLUDE|EXCLUDE|MATCH_*)로 필터 가능
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.*;
import static org.junit.jupiter.api.Assertions.*;
class ParamTest {
@ParameterizedTest
@ValueSource(strings = {"SPRING","JPA"})
void uppercase(String s){ assertTrue(s.chars().allMatch(Character::isUpperCase)); }
@ParameterizedTest
@NullAndEmptySource
@ValueSource(strings = {" "})
void blankCases(String s){ assertTrue(s == null || s.trim().isEmpty()); }
@ParameterizedTest
@CsvSource({"1,3,4","2,5,7"})
void add(int a,int b,int expected){ assertEquals(expected, a+b); }
static java.util.stream.Stream<String> ids(){ return java.util.stream.Stream.of("U001","U002"); }
@ParameterizedTest @MethodSource("ids")
void idRule(String id){ assertTrue(id.startsWith("U")); }
}
12. @ExtendWith
- 역할: Jupiter 확장(Extension) 연결(주입·훅·조건)
- 적용 위치: 클래스
- 언제: 공통 로깅/타임아웃/리소스/커스텀 주입
- 실무 팁: Spring 환경에선 @SpringBootTest가 내부적으로 SpringExtension 사용
@ExtendWith(MockitoExtension.class) // ★ Mockito 확장 활성화
class PaymentServiceTest {
@Mock OrderRepository orderRepository; // 의존성 목
@Mock PaymentGateway paymentGateway; // 외부 결제 목
@InjectMocks PaymentService sut; // 테스트 대상(목 주입 대상)
...
}
13. @Tag
- 역할: 테스트 분류/필터링
- 적용 위치: 클래스, 메서드
- 언제: fast/slow/db/integration/smoke 등 파이프라인 분리
- 실무 팁: Gradle/Jenkins에서 includeTags/excludeTags로 빌드 시간 최적화
- 특징/제약
- 태그 이름은 간단명료(공백/특수문자 지양)
- 중복 태그 가능, 상하위 개념 설계로 필터 전략 수립
import org.junit.jupiter.api.*;
class TagExampleTest {
@Test
@Tag("fast") // 빠른 단위 테스트
void unitFast() {}
@Test
@Tag("integration") // 통합 테스트
void integrationDb() {}
}
// build.gradle (예시)
// 특정 태그만 포함/미포함하여 실행
test {
useJUnitPlatform {
includeTags 'fast', 'smoke'
// excludeTags 'slow'
}
}
마무리
위에 정리한 것보다 더 많은 어노테이션들이 존재하지만 제가 생각하기에 사용하지 않을 것 같은 어노테이션들은 제외시켰습니다. 위에 정리한 어노테이션외에 더 많은 정보를 알고 싶으시다면 Junit5 Docs
를 참고하시길 바랍니다.
728x90