JUnit 5
2.2+ 버전의 스프링 부트 프로젝트를 만든다면 기본으로 JUnit 5 의존성 추가 됨.
자바 개발자가 가장 많이 사용하는 테스팅 프레임워크.
- “단위 테스트를 작성하는 자바 개발자 93% JUnit을 사용함.”
- 자바 8 이상을 필요로 함.
-
대체제: TestNG, Spock, …
- Platform: 테스트를 실행해주는 런처 제공. TestEngine API 제공.
- Jupiter: TestEngine API 구현체로 JUnit 5를 제공.
- Vintage: JUnit 4와 3을 지원하는 TestEngine 구현체.
테스트 이름 표시하기
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
- Method와 Class 레퍼런스를 사용해서 테스트 이름을 표기하는 방법 설정.
- 기본 구현체로 ReplaceUnderscores 제공
@DisplayName("스터디 만들기")
- 어떤 테스트인지 테스트 이름을 보다 쉽게 표현할 수 있는 방법을 제공하는 애노테이션.
- @DisplayNameGeneration 보다 우선 순위가 높다.
Assertion
org.junit.jupiter.api.Assertions.*
@Test
@DisplayName("스터디 만들기")
void create_new_study() {
Study study = new Study(5);
assertNotNull(study);
// messages 를 람다식으로 작성하는 이유는?
assertEquals(StudyStatus.DRAFT, study.getStatus(), () -> "스터디를 처음 만들면" + StudyStatus.DRAFT + "여야 한다.");
assertTrue(study.getLimit() > 0, () -> "스터디 최대 참석 인원은 0 보다 커야 한다.");
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> new Study(-10));
assertEquals("limit 은 0보다 커야 한다.", exception.getMessage());
assertTimeout(Duration.ofMillis(100), ()-> {
new Study(10);
Thread.sleep(50);
});
assertTimeoutPreemptively(Duration.ofMillis(100), ()-> {
new Study(10);
Thread.sleep(50);
});
assertAll(
() -> assertNotNull(study),
() -> assertEquals(StudyStatus.DRAFT, study.getStatus(), () -> "스터디를 처음 만들면" + StudyStatus.DRAFT + "여야 한다."),
() -> assertTrue(study.getLimit() > 0, () -> "스터디 최대 참석 인원은 0 보다 커야 한다."),
() -> assertThrows(IllegalArgumentException.class, () -> new Study(-10))
);
}
실제 값이 기대한 값과 같은지 확인 | assertEqulas(expected, actual) |
값이 null이 아닌지 확인 | assertNotNull(actual) |
다음 조건이 참(true)인지 확인 | assertTrue(boolean) |
모든 확인 구문 확인 | assertAll(executables…) |
예외 발생 확인 | assertThrows(expectedType, executable) |
특정 시간 안에 실행이 완료되는지 확인 | assertTimeout(duration, executable) |
마지막 매개변수로 Supplier
- 복잡한 메시지 생성해야 하는 경우 사용하면 실패한 경우에만 해당 메시지를 만들게 할 수 있다.
조건에 따라 테스트 실행하기
@Test
@DisplayName("조건에 따라 테스트 실행하기")
void create_con_study() {
String test_env = System.getenv("TEST_ENV");
System.out.println("TEST_ENV:" + test_env);
assumeTrue("LOCAL".equalsIgnoreCase(test_env));
assumingThat("LOCAL".equalsIgnoreCase(test_env), ()->{
System.out.println("local");
Study actual = new Study(100);
assertThat(actual.getLimit()).isGreaterThan(0);
});
}
@Test
@DisplayName("조건에 따라 테스트 실행하기 annotation")
@EnabledOnOs({OS.MAC, OS.LINUX})
@EnabledOnJre(JRE.JAVA_8)
// @DisabledOnOs(OS.MAC)
@EnabledIfEnvironmentVariable(named = "TEST_ENV", matches = "LOCAL")
void create_con1_study() {
String test_env = System.getenv("TEST_ENV");
System.out.println("TEST_ENV:" + test_env);
assumeTrue("LOCAL".equalsIgnoreCase(test_env));
}
태깅
@Test
@Tag("fast")
void test_tagging() {
System.out.println("local 환경에서 테스트 fast");
}
@Test
@Tag("slow")
void test_tagging2() {
System.out.println("ci 환경에서 테스트 slow");
}
- 테스트 메소드에 태그를 추가할 수 있다.
- 하나의 테스트 메소드에 여러 태그를 사용할 수 있다.
- Intellij, Maven, Gradle 에서 사용하는 방법 구글 검색
커스텀태그
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Test
@Tag("fast")
public @interface FastTest {
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Test
@Tag("slow")
public @interface FastTest {
}
@Test
@FastTest
void test_annotation() {
System.out.println("local 환경에서 테스트 fast");
}
@Test
@SlowTest
void test_annotation2() {
System.out.println("ci 환경에서 테스트 slow");
}
테스트 반복하기
@DisplayName("반복 테스트")
@RepeatedTest(value = 10, name = "{currentRepetition}/{totalRepetitions}")
void repeatTest(RepetitionInfo repetitionInfo) {
System.out.println("test" + repetitionInfo.getCurrentRepetition() + repetitionInfo.getTotalRepetitions());
}
@ParameterizedTest(name = "{index} / {0}")
@ValueSource(strings = {"날씨가", "많이", "추워지고", "있네요."})
@NullAndEmptySource
void parameterizedTest(String message){
System.out.println("message = " + message);
}
테스트 인스턴스
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
JUnit은 테스트 메소드 마다 테스트 인스턴스를 새로 만든다.
- 이것이 기본 전략.
- 테스트 메소드를 독립적으로 실행하여 예상치 못한 부작용을 방지하기 위함이다.
- 이 전략을 JUnit 5에서 변경할 수 있다.
@TestInstance(Lifecycle.PER_CLASS)
- 테스트 클래스당 인스턴스를 하나만 만들어 사용한다.
- 경우에 따라, 테스트 간에 공유하는 모든 상태를 @BeforeEach 또는 @AfterEach에서 초기화 할 필요가 있다.
- @BeforeAll과 @AfterAll을 인스턴스 메소드 또는 인터페이스에 정의한 default 메소드로 정의할 수도 있다.
테스트 순서
실행할 테스트 메소드 특정한 순서에 의해 실행되지만 어떻게 그 순서를 정하는지는 의도적으로 분명히 하지 않는다. (테스트 인스턴스를 테스트 마다 새로 만드는 것과 같은 이유)
경우에 따라, 특정 순서대로 테스트를 실행하고 싶을 때도 있다. 그 경우에는 테스트 메소드를 원하는 순서에 따라 실행하도록 @TestInstance(Lifecycle.PER_CLASS)와 함께 @TestMethodOrder를 사용할 수 있다.
MethodOrderer 구현체를 설정한다.
기본 구현체
- Alphanumeric
- OrderAnnoation
- Random
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@Order(1)
junit-platform.properties
JUnit 설정 파일로, 클래스패스 루트 (src/test/resources/)에 넣어두면 적용된다.
테스트 인스턴스 라이프사이클 설정
junit.jupiter.testinstance.lifecycle.default = per_class
확장팩 자동 감지 기능
junit.jupiter.extensions.autodetection.enabled = true
@Disabled 무시하고 실행하기
junit.jupiter.conditions.deactivate = org.junit.*DisabledCondition
테스트 이름 표기 전략 설정
junit.jupiter.displayname.generator.default =
org.junit.jupiter.api.DisplayNameGenerator$ReplaceUnderscores
확장 모델
JUnit 4의 확장 모델은 @RunWith(Runner), TestRule, MethodRule.
JUnit 5의 확장 모델은 단 하나, Extension.
확장팩 등록 방법
- 선언적인 등록 @ExtendWith
- 프로그래밍 등록 @RegisterExtension
- 자동 등록 자바 ServiceLoader 이용
확장팩 만드는 방법
- 테스트 실행 조건
- 테스트 인스턴스 팩토리
- 테스트 인스턴스 후-처리기
- 테스트 매개변수 리졸버
- 테스트 라이프사이클 콜백
- 예외 처리
- ….
package me.khjzzm.thejavatest;
import org.junit.jupiter.api.extension.AfterTestExecutionCallback;
import org.junit.jupiter.api.extension.BeforeTestExecutionCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import java.lang.reflect.Method;
public class FindSlowTestExtension implements BeforeTestExecutionCallback, AfterTestExecutionCallback {
private static final long THRESHOLD = 1000L;
@Override
public void beforeTestExecution(ExtensionContext context) throws Exception {
ExtensionContext.Store store = store(context);
store.put("START_TIME", System.currentTimeMillis());
}
@Override
public void afterTestExecution(ExtensionContext context) throws Exception {
Method requiredTestMethod = context.getRequiredTestMethod();
String testMethodName = requiredTestMethod.getName();
SlowTest annotation = requiredTestMethod.getAnnotation(SlowTest.class);
ExtensionContext.Store store = store(context);
long start_time = store.remove("START_TIME", long.class);
long duration = System.currentTimeMillis() - start_time;
if(duration > THRESHOLD && annotation == null){
System.out.printf("Please consider mark method [%s] with @SlowTest.\n", testMethodName);
}
}
private static ExtensionContext.Store store(ExtensionContext context) {
String testClassName = context.getRequiredTestClass().getName();
String testMethodName = context.getRequiredTestMethod().getName();
ExtensionContext.Store store = context.getStore(ExtensionContext.Namespace.create(testClassName, testMethodName));
return store;
}
}
@SlowTest
@Tag("slow")
@DisplayName("Extension")
void findSlowTestExtension_pass() throws InterruptedException {
Thread.sleep(1005L);
System.out.println("ci 환경에서 테스트 slow");
}
@Test
@Tag("slow")
@DisplayName("Extension")
void findSlowTestExtension() throws InterruptedException {
Thread.sleep(1005L);
System.out.println("ci 환경에서 테스트 slow");
}