1. What is Junit 5?
JUnit5 은 3개의 서브 모듈로 구성되어 있다.
Junit5 = JUnit platform + JUnit jupiter + JUnit Vintage
이들을 간략하게 정리하자면 JUni5 는 테스트를 작성하고 실행하기 위한 테스팅 프레임워크를 제공한다.
JUnit Vintage 는 JUnit 의 이전 버전 (= 3, 4) 과 호환시키기 위한 모듈이고, JUnit Jupiter 는 새로운 기능을 지원하기 위한 모듈이고,
JUnit Platform 은 이들을 통합하기 위한 기반을 제공하는 모듈이다.
2. Writing Tests
가장 간략하게 테스트 케이스를 작성하는 코드는 다음과 같다.
import static org.junit.jupiter.api.Assertions.assertEquals;
import example.util.Calculator;
import org.junit.jupiter.api.Test;
class MyFirstJUnitJupiterTests {
private final Calculator calculator = new Calculator();
@Test
void addition() {
assertEquals(2, calculator.add(1, 1));
}
}
2.1 Annotations
JUnit jupiter 는 다음과 같은 애노테이션들을 지원한다.
Annotation | Description |
---|---|
@Test | 이 애노테이션이 붙은 메소드는 "테스트 케이스" 임을 가리킨다. |
@ParameterizedTest | 이 애노테이션이 붙은 메소드는 "Parameterized Test" 임을 가리킨다. 테스트를 생성할 때 @Test 를 붙혀서 생성할 수 있지만 @ParameterizedTest 를 붙혀서 생성할 수 있다. (테스트 케이스 생성 방법은 다양하다) 자세한 설명은 이후의 글을 참고 |
@RepeatedTest | 이 애노테이션은 붙은 메소드는 반복해서 실행되는 테스트 케이스임을 가리킨다. |
@TestFactory | 이 애노테이션이 붙은 메소드는 "dynamic tests" 를 위한 "Test Factory" 임을 가리킨다. 자세한 설명은 이후의 글을 참고 |
@TestTemplate | 이 애노테이션이 붙은 메소드는 하나의 테스트 케이스를 여러 환경 (= 매개변수, 라이프사이클 메소드들, DisplayName 등) 에서 실행되는 테스트임을 가리킨다. |
@TestClassOrder | 이 애노테이션이 붙은 클래스는 테스트 케이스를 실행하는 순서를 클래스 레벨에서 지정할 수 있다. |
@TestMethodOrder | 이 애노테이션이 붙은 클래스는 테스트 케이스를 실행하는 순서를 메소드 레벨에서 지정할 수 있다. |
@TestInstance | 이 애노테이션이 붙은 클래스는 Test Instance Lifecycle 을 지정할 수 있다. 자세한 설명은 이후의 글을 참고 |
@DisplayName | 이 애노테이션은 테스트 클래스나 메소드에 붙어서 커스텀한 이름이나 설명을 나타낼 때 쓰인다. |
@DisplayNameGeneration | 이 애노테이션은 테스트 클래스나 메소드에 붙어서 이름을 커스텀하게 만들 수 있다. |
@BeforeEach | 이 애노테이션이 붙은 메소드는 테스트 케이스들 (= "@Test", "@ParameterizedTest", "@RepeatedTest", "@TestFactory") 보다 먼저 실행된다. |
@AfterEach | 이 애노테이션이 붙은 메소드는 테스트 케이스들 ("@Test", "@ParameterizedTest", "@RepeatedTest", "@TestFactory") 이 끝나고 실행된다. |
@BeforeAll | 이 애노테이션이 붙은 메소드는 테스트 클래스에 있는 모든 테스트 케이스들이 실행되기 전에 먼저 실행된다. |
@AfterAll | 이 애노테이션이 붙은 메소드는 테스트 클래스에 있는 모든 테스트 케이스들이 실행되고 난 후 실행된다. |
@Nested | 이 애노테이션이 붙은 클래스는 "Nested Test Class " 임을 가리킨다. 자바 8-15 에서는 @Nested 애노테이션이 붙은 테스트 클래스는 이 안에서 @TestInstance(LifeCycle.PER_CLASS) 이 붙어야만 @BeforeAll 과 @AfterAll 을 사용할 수 있었다. 자바 16 부터는 Test Instance Lifecycle 없이 static method 로 @BeforeAll 과 @AfterAll 을 사용할 수 있다. "Nested Test Class" 에 대한 설명은 이후의 글 참고 |
@Tag | 이 애노테이션을 테스트 클래스나 메소드에 붙혀서 해당 태그가 붙은 테스트 케이스를 실행되지 않고 필터링되게 만들 수 있다. Tag Expression 을 참고하면 어떻게 필터링을 만드는 지 알 수 있다. |
@Disabled | 이 애노테이션이 붙은 테스트 클래스나 메소드는 실행이 되지 않는다. 이 애노테이션을 사용하는 유즈 케이스로는 버그로 인해서 일시적으로 문제가 있을 경우에 테스트 케이스 실행을 막기 위해서 사용된다. |
@Timeout | 이 애노테이션이 붙은 테스트 케이스가 실행될 때 애노테이션에 지정한 매개변수 시간을 초과해서 실행된다면 테스트 케이스는 실패한다. |
@ExtendWith | 테스트 클래스나 케이스에 다양한 확장 기능들을 제공하고 싶을 때 이 애노테이션을 쓰면 된다. 자세한 설명은 이후의 글을 참고 |
@RegisterExtension | @ExtendWith 가 선언적으로 확장 기능을 제공할 수 있는 반면에 @RegisterExtension 은 동적으로 추가할 수 있다. |
@TempDir | Temporary Directory 를 제공하기 위해서 사용하는 애노테이션이다. 파라미터 injetion 이나 필드 injection 을 통해서 사용할 수 있다. |
2.2 Parameterized Tests
Paramterized Test 는 @Test
애노테이션으로 생성한 테스트와 달리 여러개의 명시한 매개변수들을 이용해서 그 만큼 실행되는 테스트이다.
테스트를 실행할 매개변수로 올 값을 명시해야하므로 @ValueSource
와 같은 Source
애노테이션을 추가로 선언해야만한다.
예시로보면 다음과 같다.
@ParameterizedTest
@ValueSource(strings = { "racecar", "radar", "able was I ere I saw elba" })
void palindromes(String candidate) {
assertTrue(StringUtils.isPalindrome(candidate));
}
@ValueSource
는 간단한 값들만을 넣는 애노테이션이다. 그래서 다음과 같은 타입만 지원한다.
- int, long, short, byte, float, double, char, boolean, java.lang.String, java.lang.Class
복잡한 객체의 값들을 Input 으로 명시하려면 어떻게 해야할까?
@MethodSource
:- 해당 애노테이션으로 지정한 메소드에서 생성한 객체를 매개변수로 가지고 올 수 있다.
- 여러개의 매개변수를 생성하기 위해서 지정한 메소드는 Stream 형식으로 값을 리턴했다는 점에 주목하자.
@ArgumentsSource
:ArgumentProvider
인터페이스를 상속한 객체를 사용자가 구현하고 애노테이션에서 이를 지정하면 커스텀 Provider 에서 생성한 객체들을 매개변수로 가지고 올 수 있다.
@MethodSource
의 예시:
@ParameterizedTest
@MethodSource("provideComplexObjects")
void testWithComplexObjects(MyComplexObject object) {
// perform test logic with object
}
static Stream<MyComplexObject> provideComplexObjects() {
return Stream.of(new MyComplexObject("data1"), new MyComplexObject("data2"));
}
@ArgumentSource
의 예시:
@ParameterizedTest
@ArgumentsSource(MyArgumentsProvider.class)
void testWithArgumentsSource(String argument) {
assertNotNull(argument);
}
static class MyArgumentsProvider implements ArgumentsProvider {
@Override
public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
return Stream.of("apple", "banana").map(Arguments::of);
}
}
이외에 @CsvSoucrce
또는 @CsvFileSource
를 이용해서 CSV 형식으로 값을 입력해서 매개변수로 읽어오거나, CSV 파일로부터 읽어서 매개변수로 가져올 수 있다.
2.3 Dynamic Tests
Dynamic Test
는 테스트 케이스와 테스트 케이스를 위한 매개변수들 그리고 테스트 이름을 런타임 때 만들 수 있다.
이는 정적 테스트와 가장 다른 점이다.
- 정적 테스트는
@Test
로 테스트 케이스를 만들던 것들을 말한다. - 이 차이로 인해서
Dynamic Test
에서 할 수 있는 것은 하나의 테스트 케이스를 여러개의 매개변수로 실행되는 것을 할 수 있다. (물론@ParameterizedTest
로도 가능하다.) - 아니면 외부에서 가져온 데이터를 바탕으로 테스트 케이스를 만드는 경우에는
Dynamic Test
로만 가능하다.
Dynamic Test
를 생성하기 위해서 @TestFactory
가 이용된다.
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.DynamicTest.dynamicTest;
class DynamicTestsDemo {
@TestFactory
Collection<DynamicTest> dynamicTestsFromCollection() {
// 테스트 입력 및 예상 결과를 준비합니다.
int[] input = {1, 2, 3};
int[] expected = {2, 4, 6};
return Arrays.asList(
dynamicTest("1st dynamic test", () -> assertTrue(expected[0] == input[0] * 2)),
dynamicTest("2nd dynamic test", () -> assertTrue(expected[1] == input[1] * 2)),
dynamicTest("3rd dynamic test", () -> assertTrue(expected[2] == input[2] * 2))
);
}
@TestFactory
Stream<DynamicTest> dynamicTestsFromStream() {
// 테스트 입력을 스트림으로 생성합니다.
return Stream.of("A", "B", "C")
.map(str -> dynamicTest("test" + str, () -> {
// 실제 테스트 로직
assertTrue(str.length() > 0);
}));
}
}
@TestFactory
가 생성할 수 있는 리턴 타입은 다음과 같다:
DynamicNode
,Stream
,Collection
,Iterable
,Iterator
, array ofDynamic Node
@Test
와 @TestFactory
모두 static
메소드이거나 private
메소드이면 안된다.
@BeforeEach
와 @AfterEach
와 같이 테스트 케이스의 라이프 사이클에 해당하는 메소드들은 @TestFactory
의 실행 전과 후에 실행은 되지만 각각의 dynamicTest()
의 실행과는 상관없다.
2.4 Test Templates
@TestTemplate
은 @Test
로 만든 테스트 케이스에다가 다양한 환경들 (= 테스트 케이스를 위한 매개변수, DisplayName, BeforeEach, AfterEach 와 같은 콜백, 테스트 실행 전 설정 주입 등) 을 가지고 여러번 실행할 수 있도록 하는 것이다.
- 단순하게 매개변수만 다르게 여러번 테스트를 실행하고 싶다면
@ParamterizedTest
를 이용하면 된다.
@TestTemplate
은 TestTemplateInvocationContextProvider
인터페이스를 구현한 프로바이더에 의해서 실행된다. (이건 개발자가 작성해야함.)
이 프로바이더는 TestTemplateInvocationContext
를 통해서 테스트가 실행될 수 있는 컨택스트를 제공하면서 실행한다.
이 TestTemplateInvocationContext
에 테스트 케이스마다 다를 수 있는 환경 정보들 (= 매개변수, 콜백, 테스트 실행 전 설정 주입 등) 이 포함된다.
@TestTemplate
을 이용한 예제 코드는 다음과 같다.
- 이건 사용자 계정에 대해 권한 별로 테스트 케이스를 실행하는 코드다.
public class UserPermissionTest {
@TestTemplate
@ExtendWith(UserPermissionTestProvider.class)
void userPermissionTest(UserPermission permission) {
// 여기에 테스트 로직을 작성합니다.
// 예: 특정 API가 사용자 권한에 따라 올바르게 반응하는지 확인합니다.
String result = performApiCall(permission);
Assertions.assertTrue(result.contains("Success") || result.contains("Access Denied"));
}
// 실제 환경에서는 여기에 API 호출 로직을 작성합니다.
private String performApiCall(UserPermission permission) {
// 여기서는 단순한 문자열 반환으로 시뮬레이션합니다.
if (permission == UserPermission.ADMIN) {
return "Success";
} else {
return "Access Denied";
}
}
enum UserPermission {
ADMIN, USER, GUEST
}
static class UserPermissionTestProvider implements TestTemplateInvocationContextProvider {
@Override
public boolean supportsTestTemplate(ExtensionContext context) {
return true;
}
@Override
public Stream<TestTemplateInvocationContext> provideTestTemplateInvocationContexts(ExtensionContext context) {
return Stream.of(
userPermissionContext(UserPermission.ADMIN),
userPermissionContext(UserPermission.USER),
userPermissionContext(UserPermission.GUEST)
);
}
private TestTemplateInvocationContext userPermissionContext(UserPermission permission) {
return new TestTemplateInvocationContext() {
@Override
public String getDisplayName(int invocationIndex) {
return "Test with user permission: " + permission;
}
@Override
public void beforeEach(ExtensionContext context) throws Exception {
// 필요한 경우 여기에서 테스트 전에 실행할 커스텀 로직을 구현할 수 있습니다.
}
@Override
public void afterEach(ExtensionContext context) throws Exception {
// 필요한 경우 여기에서 테스트 후에 실행할 커스텀 로직을 구현할 수 있습니다.
}
};
}
}
}
2.5 Test Execution Order
일반적으로 테스트 케이스들이 서로의 순서에 의존성을 가지지 않도록 해야하는 것이 옳다.
하지만 빌드 시간을 최적화 하기 위해서 테스트 케이스를 실행하는 순서를 정하는 것이 때로는 유용하다:
- 이전에 실패한 테스트를 먼저 실행하거나, 빠른 테스트를 먼저 실행하는 것.
- 테스트 실행을 병렬 모드로 해놓고 테스트 실행이 오래 걸리는 테스트를 먼저 실행하는 것
여기서는 테스트 케이스에서 순서를 지정하는 방법으로 @TestMethodOrder
와 @TestClassOrder
를 소개한다.
2.5.1 @TestMethodOrder
테스트 클래스 내의 테스트 케이스 메소드들의 순서를 지정하는 방법을 말한다.
실행 순서를 결정하는 옵션은 다음과 같다:
- Alphanumeric:
- 테스트 케이스의 이름의 알파벳 순서에 따라서 실행된다.
- OrderAnnotation:
@Order
애노테이션으로 지정한 순서에 따라서 실행된다.
- Random:
- 무작위 순서로 실행된다.
예제 코드는 다음과 같다.
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class OrderedTest {
@Test
@Order(1)
void firstTest() {
// 이 테스트가 첫 번째로 실행됩니다.
}
@Test
@Order(2)
void secondTest() {
// 이 테스트가 두 번째로 실행됩니다.
}
@Test
@Order(3)
void thirdTest() {
// 이 테스트가 세 번째로 실행됩니다.
}
}
2.5.2 @TestClassOrder
테스트 케이스들이 포함되어 있는 Top Level 테스트 클래스에서 테스트 케이스가 내부에 중첩되어 있는 클래스인 Nested Class
가
있을 때 이들의 순서를 정할 수 있다.
@TestClassOrder
는 ClassOrderer
구현체를 통해서 순서를 정할 수 있다:
- Alphanumeric:
- 클래스 이름의 알파벳순으로 순서가 결정된다.
- OrderAnnotation:
- @Order 애노테이션을 사용하여 명시적인 순서가 결정된다.
- Random:
- 무작위 순서로 실행합니다.
예시 코드는 다음과 같다.
@TestClassOrder(ClassOrderer.OrderAnnotation.class)
public class OrderedClassesTest {
@Nested
@Order(1)
class FirstNestedTestClass {
// 이 중첩 클래스의 테스트들이 첫 번째로 실행됩니다.
}
@Nested
@Order(2)
class SecondNestedTestClass {
// 이 중첩 클래스의 테스트들이 두 번째로 실행됩니다.
}
@Nested
@Order(3)
class ThirdNestedTestClass {
// 이 중첩 클래스의 테스트들이 세 번째로 실행됩니다.
}
}
2.6 Test Instance Lifecycle
Test Instance Lifecycle 을 이해하기 위해서는 @Test
애노테이션이 붙은 테스트 케이스가 어떻게 실행되는지 과정을 알아야 한다.
@Test
애노테이션이 붙은 메소드는 기본적으로 메소드 실행 전마다 새로운 테스트 클래스 객체 (= Test Instance) 를 생성해서 해당 메소드를 실행한다.
즉 Test Instance 의 Lifecycle 은 @Test
메소드마다 Test Instance 를 생성하고 폐기하는 과정을 가진다.
근데 이 Lifecycle 을 @TestInstance(Lifecycle.PER_CLASS)
로 변경하면 하나의 Test Instance 를 생성하고 @Test
가 붙은 테스트 케이스들을 모두 실행한뒤 폐기하는 과정으로 된다.
(기본 동작이 per-method 마다 Test Instance 를 생성하는 이유는 테스트 클래스끼리 서로의 공유하는 변수의 상태를 변경시켜서 사이드 이펙트를 내지 않도록 하기 위함이다.)
@TestInstance(Lifecycle.PER_CLASS)
로 변경하면 얻을 수 있는 이점으로 @BeforeAll
과 @AfterAll
그리고 @MethodSource
와 같은 메소드들을 static
메소드로 만들지 않고 그냥 메소드로 만들어도 된다.
2.7 Display Name Generators
테스트 케이스 메소드나 클래스에 이 애노테이션을 붙어서 커스텀하게 이름을 생성할 수 있다.
이름을 생성하기 위한 전략은 다음과 같이 제공한다:
DisplayNameGenerator.Standard
:- 기본 설정이다. 아무런 변경하지 않는다.
DisplayNameGenerator.ReplaceUnderscores
:- 테스트 케이스나 클래스의 이름에 있는 모든 밑줄(_) 을 공백으로 대체한다.
DisplayNameGenerator.IndicativeSentences
:- 테스트 클래스와 메소드의 이름을 조합해서 문장으로 생성한다.
DisplayNameGenerator.Simple
:- 간단하게 클래스나 메소드 이름만 출력된다.
- standard 로 사용했을 경우에는 메소드의 매개변수와 같은 것들도 출력되겠지만 simple 을 이용하면 진짜 딱 이름만 출력된다.
2.8 Nested Test Class
@Nested
애노테이션을 이용하면 관련 있는 테스트 케이스들을 그룹핑 할 수 있고, 테스트 구조를 계층적으로 보여줄 수 있다.
@Nested
애노테이션을 이용한 예제:
- (Stack 의 생성에 대한 테스트 케이스도 생성하네)
- (Stack 을 새로 생성한 이후와 Push 하는 연산을 다른 계층으로 효과적으로 표현하고 있음)
import java.util.EmptyStackException;
import java.util.Stack;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
@DisplayName("A stack")
class TestingAStackDemo {
Stack<Object> stack;
@Test
@DisplayName("is instantiated with new Stack()")
void isInstantiatedWithNew() {
new Stack<>();
}
@Nested
@DisplayName("when new")
class WhenNew {
@BeforeEach
void createNewStack() {
stack = new Stack<>();
}
@Test
@DisplayName("is empty")
void isEmpty() {
assertTrue(stack.isEmpty());
}
@Test
@DisplayName("throws EmptyStackException when popped")
void throwsExceptionWhenPopped() {
assertThrows(EmptyStackException.class, stack::pop);
}
@Test
@DisplayName("throws EmptyStackException when peeked")
void throwsExceptionWhenPeeked() {
assertThrows(EmptyStackException.class, stack::peek);
}
@Nested
@DisplayName("after pushing an element")
class AfterPushing {
String anElement = "an element";
@BeforeEach
void pushAnElement() {
stack.push(anElement);
}
@Test
@DisplayName("it is no longer empty")
void isNotEmpty() {
assertFalse(stack.isEmpty());
}
@Test
@DisplayName("returns the element when popped and is empty")
void returnElementWhenPopped() {
assertEquals(anElement, stack.pop());
assertTrue(stack.isEmpty());
}
@Test
@DisplayName("returns the element when peeked but remains not empty")
void returnElementWhenPeeked() {
assertEquals(anElement, stack.peek());
assertFalse(stack.isEmpty());
}
}
}
IDE 에서 위의 예제를 실행하면 이처럼 보일 것이다.

이렇게 계층적으로 구성할 때 Outer 테스트 클래스에서 정의한 @BeforeEach
와 같은 setUp 코드는 Inner 테스트 클래스에서도 독립적으로 사용된다.
그리고 @Nested
테스트 클래스는 기본적으로 @TestInstance(LifeCycle.PER_CLASS)
없이는 @BeforeAll
과 @AfterAll
을 사용하지 못했다. (자바 16 이전에 해당)
- @BeforeAll 같은 콜백은 스태틱 메소드로 선언되어지는데, 자바 16 이전에는 자바가 inner class 의 static member 에 대해서 알수 없었기 때문이라고 함.
2.9 Disabling Tests
@Disabled
가 사용되는 예시 코드:
- 버그로 인해서 테스트 케이스가 깨지는 상황을 나타낼 때 쓰인다.
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
@Disabled("Disabled until bug #99 has been fixed")
class DisabledClassDemo {
@Test
void testWillBeSkipped() {
}
}
2.10 Declarative Extension Registration
@ExtendWith
애노테이션을 통해서 다양한 확장 기능들을 테스트 케이스나 클래스에 추가할 수 있다.
확장 기능은 다음과 같다:
- 테스트 케이스에 추가 파라미터 주입
- 테스트 실행 전/후에 커스텀한 로직 실행
- 테스트 인스턴스의 생성에 관여
- 테스트 메소드의 실행 조건 변경
- 등등
@ExtendWith
와 SpringExtension
를 사용하는 예시:
- 이 확장 기능은 테스트 클래스를 생성할 떄 테스트 클래스에 빈을 주입하게 해주는 기능이다.
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.test.context.junit.jupiter.SpringExtension;
@ExtendWith(SpringExtension.class)
class MySpringBootTest {
@Test
void testWithSpringContext() {
// Spring 컨텍스트가 로드되고, 빈이 주입된 상태에서 테스트 실행
}
}
@ExtendWith
와 WebServerExtension
을 사용하는 예시:
- 이 확장 기능은 로컬 서버를 띄우고 로컬 서버의 URL 을 테스트 케이스의 파라미터로 전달해준다.
@Test
@ExtendWith(WebServerExtension.class)
void getProductList(@WebServerUrl String serverUrl) {
WebClient webClient = new WebClient();
// Use WebClient to connect to web server using serverUrl and verify response
assertEquals(200, webClient.get(serverUrl + "/products").getResponseStatus());
}
만약 WebServerExtension
을 테스트 클래스 레벨에 등록한다면 모든 테스트 케이스에서 사용할 수 있다.
@ExtendWith(WebServerExtension.class)
class MyTests {
// ...
}
Multiple ExtendWith 를 사용하고 싶다면 다음과 같이 코드를 구성하면 된다.
@Random
확장 애노테이션을 만들어서 재사용하는 것도 있다. 신기하네.
@ExtendWith({ DatabaseExtension.class, WebServerExtension.class })
class MyFirstTests {
// ...
}
@ExtendWith
의 확장 기능들의 조합을 재사용하고 싶다면 애노테이션으로 만들면된다.
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@ExtendWith({ DatabaseExtension.class, WebServerExtension.class })
public @interface DatabaseAndWebServerExtension {
}
@Target({ ElementType.FIELD, ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
@ExtendWith(RandomNumberExtension.class)
public @interface Random {
}
class RandomNumberDemo {
// Use static randomNumber0 field anywhere in the test class,
// including @BeforeAll or @AfterEach lifecycle methods.
@Random
private static Integer randomNumber0;
// Use randomNumber1 field in test methods and @BeforeEach
// or @AfterEach lifecycle methods.
@Random
private int randomNumber1;
RandomNumberDemo(@Random int randomNumber2) {
// Use randomNumber2 in constructor.
}
@BeforeEach
void beforeEach(@Random int randomNumber3) {
// Use randomNumber3 in @BeforeEach method.
}
@Test
void test(@Random int randomNumber4) {
// Use randomNumber4 in test method.
}
}
RandomNumberExtesnion
은 Extension API 를 이용해서 기능을 구현한 것이다. 실제 내부 구현은 아래와 같은 것.
- BeforeAllCallback:
- static field injection 을 위해서 사용했다.
- BeforeEachCallback:
- non-static field injection 을 위해서 사용했다.
TestInstancePostProcessor
를 이용해서도 non-static field injection 을 구현할 수 있다고 함. 이게 이상적이라고 하는듯.
- ParameterResolver:
- 생성자와 메소드 injection 을 위해서 사용했다.
class RandomNumberExtension
implements BeforeAllCallback, BeforeEachCallback, ParameterResolver {
private final java.util.Random random = new java.util.Random(System.nanoTime());
/**
* Inject a random integer into static fields that are annotated with
* {@code @Random} and can be assigned an integer value.
*/
@Override
public void beforeAll(ExtensionContext context) {
Class<?> testClass = context.getRequiredTestClass();
injectFields(testClass, null, ModifierSupport::isStatic);
}
/**
* Inject a random integer into non-static fields that are annotated with
* {@code @Random} and can be assigned an integer value.
*/
@Override
public void beforeEach(ExtensionContext context) {
Class<?> testClass = context.getRequiredTestClass();
Object testInstance = context.getRequiredTestInstance();
injectFields(testClass, testInstance, ModifierSupport::isNotStatic);
}
/**
* Determine if the parameter is annotated with {@code @Random} and can be
* assigned an integer value.
*/
@Override
public boolean supportsParameter(ParameterContext pc, ExtensionContext ec) {
return pc.isAnnotated(Random.class) && isInteger(pc.getParameter().getType());
}
/**
* Resolve a random integer.
*/
@Override
public Integer resolveParameter(ParameterContext pc, ExtensionContext ec) {
return this.random.nextInt();
}
private void injectFields(Class<?> testClass, Object testInstance,
Predicate<Field> predicate) {
predicate = predicate.and(field -> isInteger(field.getType()));
findAnnotatedFields(testClass, Random.class, predicate)
.forEach(field -> {
try {
field.setAccessible(true);
field.set(testInstance, this.random.nextInt());
}
catch (Exception ex) {
throw new RuntimeException(ex);
}
});
}
private static boolean isInteger(Class<?> type) {
return type == Integer.class || type == int.class;
}
}
JUnit5 Jupiter 5.8 부터 @ExtendWith
는 필드, 생성자 파라미터, 메소드 그리고 lifecycle (= @BeforeEach
, @AfterEach
등) 에서도 사용할 수 있다.
2.11 Programmatic Extension Registration
@RegisterExtension
은 @ExtendWith
처럼 확장 기능을 등록하는 것이지만 방식에 차이가 있다.
@ExtendWith
는 선언적으로 등록하는 반면에 @RegisterExtension
은 프로그래밍 방식 (= 확장 객체를 직접 인스턴스화 하고 필요한 설정 기능을 등록해서 사용하는 방식) 으로 사용한다.
- (프로그래밍 방식이란 표현은 이런걸 의미하네)
@RegisterExtension은 필드 레벨에서 사용되며, 해당 필드는 static (클래스 레벨 확장을 위해) 또는 인스턴스 레벨 (각 테스트 인스턴스마다 다른 확장 인스턴스를 위해)에 사용될 수 있다.
- 이 필드는 public이거나 protected이어야 하며, final 또는 effectively final이어야 한다.
다음은 @RegisterExtension
을 사용하는 간단한 예제이다.
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.BeforeEachCallback;
class MyExtension implements BeforeEachCallback {
@Override
public void beforeEach(ExtensionContext context) {
// 테스트 실행 전에 할 작업
}
}
class MyTests {
@RegisterExtension
MyExtension myExtension = new MyExtension(); // 인스턴스 생성과 함께 확장을 등록
@Test
void testSomething() {
// myExtension이 초기화되고, beforeEach 콜백을 통해 테스트 전에 로직이 실행됨
}
}
@ExtendWith
와 @RegisterExtension
은 확장을 등록하는 순서가 내부적으로 정해져있다. 이래야 테스트를 여러번 실행시켜도 일관성을 지킬 수 있으니.
확장을 등록하는 순서를 변경하고 싶다면 @Order
를 이용해야한다.
@RegisterExtension
확장을 이용하는 방법으로 Static Field
와 Instance Field
가 있는데 이것들에 대해서 살펴보겠다.
확장 기능이 등록되는 순서가 중요한 이유는 뭘까?
- 확장을 하나만 쓰기 보다는 여러개를 쓰는 경우가 많아서 각 확장의 기능이 충돌될 수 있으니까 순서를 명확히 아는게 중요하다고 한다.
2.11.1 Static Fields
@RegisterExtension
으로 등록하는 확장의 순서는 클래스 레벨에서 선언한 @ExtendWith
를 통해 등록된 확장보다 나중에 등록된다.
static
필드에 대한 확장은 Extension API 에 제약이 없어서 클래스 레벨이나 인스턴스 레벨, 메소드 레벨의 확장 모두 이용해서 구현할 수 있다.
Extension API
로 확장 기능을 구현할 수 있다.- 확장 기능은 콜백 메소드들에 의해서 제공될 수 있는데 이게 클래스 레벨과 메소드 레벨 그리고 인스턴스 레벨로 구별된다.
- 클래스 수준 API:
- 이들은 테스트 클래스 전체의 생명주기에 걸쳐 호출된다.
- 예를 들면,
BeforeAllCallback
과AfterAllCallback
이 이에 해당한다. - 이들은 각각 테스트 클래스의 모든 테스트 메소드가 실행되기 전과 후에 한 번씩 호출된다.
- 인스턴스 수준 API:
- 이들은 테스트 인스턴스의 생성과 소멸에 관련된 콜백이며, 예로
TestInstancePostProcessor
와TestInstancePreDestroyCallback
이 있다. - 이들은 테스트 인스턴스가 생성된 직후와 소멸되기 전에 각각 호출됩니다.
- 이들은 테스트 인스턴스의 생성과 소멸에 관련된 콜백이며, 예로
- 메소드 수준 API:
- 이들은 개별 테스트 메소드의 실행 전후에 관련된 콜백이다.
BeforeEachCallback
과AfterEachCallback
이 여기에 해당하며, 각각 테스트 메소드가 실행되기 전과 후에 호출된다.
- 클래스 수준 API:
- 정적 필드 (Static Field) 에
@RegisterExtension
을 써서 제공하는 확장 기능은 모든 콜백 메소드들을 이용해서 구현할 수 있다는 것.
@RegisterExtension
을 이용하는 예시 코드:
server
필드는BeforeAllCallback
을 구현해서 모든 테스트 케이스 실행 전에 로컬 서버가 부팅될 것이고AfterAllCallback
콜백에 의해서 모든 테스트 케이스 실행이 끝나면 종료될 것이다.
class WebServerDemo {
@RegisterExtension
static WebServerExtension server = WebServerExtension.builder()
.enableSecurity(false)
.build();
@Test
void getProductList() {
WebClient webClient = new WebClient();
String serverUrl = server.getServerUrl();
// Use WebClient to connect to web server using serverUrl and verify response
assertEquals(200, webClient.get(serverUrl + "/products").getResponseStatus());
}
}
2.11.2 Instance Fields
@RegisterExtension
을 Instance Field 에서 사용한다면 기본적으로 메소드 레벨에서 확장 기능이 등록된다.
여기서 말하는 메소드 레벨은 테스트 케이스를 실행하는 전체 라이프 사이클에서 @Test
가 붙은 메소드를 실행할 때 확장 기능이 적용된다는 것이다.
즉 확장 기능을 등록하기 위해서는 Extension API 중 메소드 레벨의 수준인 것만 (= BeforeEachCallback
, AfterEachCallback
, ParameterResolver
) 이용해서 구현해야만 한다.
- 더 설명하자면
@RegisterExtension
을 Instance Field 에서 사용한다면 확장 기능은 Test Instance 가 생성된 이후부터 Extension 기능을 넣을 수 있다. - 콜백으로 치자면
TestInstancePostProcessor
처리 후에 등록을 할 수 있음. 즉BeforeAllCallback
과AfterAllCallback
그리고TestInstancPostProcessor
는 사용할 수 없다.
확장 기능이 등록되는 순서는 메소드 레벨에 붙은 @ExtendWith
이후에 Instance Field 의 확장이 등록된다.
- 근데
@TestInstance(Lifecycle.PER_CLASS)
로 테스트 인스턴스의 라이프사이클을 설정했다면 Instance Field 에 대한 확장 기능이 메소드 레벨에 붙언@Extendwith
보다 먼저 등록된다고 함.
Instance Field 에서 사용하는 @RegisterExtension
예제를 보자.
- docs 변수는 Instance Field 에서
@RegisterExtension
을 이용해 확장 기능을 등록했다. - docs 변수는
@Test
가 붙은 테스트 케이스를 실행할 때마다 매번 초기화 될 것이다.
class DocumentationDemo {
static Path lookUpDocsDir() {
// return path to docs dir
}
@RegisterExtension
DocumentationExtension docs = DocumentationExtension.forPath(lookUpDocsDir());
@Test
void generateDocumentation() {
// use this.docs ...
}
}
The TempDirectory Extension
TempDirectory Extension 을 이용하는 @TempDir
은 테스트 케이스나 클래스마다 임시 디렉토리를 만들고 정리하는 역할을 제공하기 위해서 사용한다고 한다.
임시 디렉토리를 만들어야 하는 이유는 뭔데?
- 파일이나 디렉토리와 관련된 테스트들을 할 떄 유용하다. (e.g 파일 쓰기/읽기 등)
@TempDir 를 사용해서 얻은 임시 디렉토리의 Path 에는 처음에 아무것도 없다. 그러나 다음과 같은 예제 코드를 보면 읽어온 파일에 내용을 추가해서 사용한다.
new ListWriter(soruceFile).write("a", "b", "c")
를 통해서 파일에 내용을 써주고 있음.
@Test
void copyFileFromSourceToTarget(@TempDir Path source, @TempDir Path target) throws IOException {
Path sourceFile = source.resolve("test.txt");
new ListWriter(sourceFile).write("a", "b", "c");
Path targetFile = Files.copy(sourceFile, target.resolve("test.txt"));
assertNotEquals(sourceFile, targetFile);
assertEquals(singletonList("a,b,c"), Files.readAllLines(targetFile));
}