※ 본 포스트는 JUnit 5 공식 가이드 문서를 개인적으로 정리한 문서입니다.
JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage
build.gradle
예시plugins {
id 'java'
}
repositories {
mavenCentral()
}
dependencies {
testImplementation(platform('org.junit:junit-bom:5.7.2'))
testImplementation('org.junit.jupiter:junit-jupiter')
}
test {
useJUnitPlatform()
}
build.gradle
예시plugins {
id 'org.springframework.boot' version '2.5.1'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'java'
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
test {
useJUnitPlatform()
}
애노테이션 | 대상 | 설명 | 상속 여부 |
---|---|---|---|
@Test | 메서드 | 테스트 메서드임을 나타낸다. | O |
@ParameterizedTest | 메서드 | 파라미터화 된 테스트임을 나타낸다. | O |
@RepeatedTest | 메서드 | 반복 테스트를 위한 테스트 템플릿임을 나타낸다. | O |
@TestFactory | 메서드 | 다이나믹 테스트를 위한 테스트 팩토리임을 나타낸다. | O |
@TestTemplate | 메서드 | 테스트 케이스에 대한 템플릿임을 나타낸다. | O |
@TestMethodOrder | 클래스 | 테스트 클래스의 테스트 메서드 실행 순서를 설정하는데 사용한다. | O |
@TestInstance | 클래스 | 테스트 클래스의 테스트 인스턴스 라이프사이클을 설정하는데 사용한다. | O |
@DisplayName | 클래스, 메서드 | 테스트 클래스 또는 테스트 메서드에 대한 표시 이름을 지정한다. | X |
@DisplayNameGeneration | 클래스 | 테스트 클래스에서 사용할 표시 이름 생성기를 지정한다. | O |
@BeforeEach | 메서드 | 현재 클래스의 각 테스트 메서드를 실행하기 전에 실행되어야 하는 메서드임을 나타낸다. | O |
@AfterEach | 메서드 | 현재 클래스의 각 테스트 메서드를 실행한 후에 실행되어야 하는 메서드임을 나타낸다. | O |
@BeforeAll | 메서드 | 현재 클래스의 모든 테스트 메서드를 실행하기 전에 실행되어야 하는 메서드임을 나타낸다. | O |
@AfterAll | 메서드 | 현재 클래스의 모든 테스트 메서드를 실행한 후에 실행되어야 하는 메서드임을 나타낸다. | O |
@Nested | 클래스 | 해당 클래스가 non-static 내부 테스트 클래스임을 나타낸다. 내부 테스트 클래스에서는 @BeforeAll 과 @AfterAll 메서드를 직접적으로 사용할 수 없다. | X |
@Tag | 클래스, 메서드 | 필터링 테스트를 위한 태그를 선언하는데 사용한다. | O |
@Disabled | 클래스, 메서드 | 해당 테스트 클래스 또는 테스트 메서드를 비활성화 하는데 사용한다. | X |
@Timeout | 메서드 | 테스트, 테스트 팩토리, 테스트 템플릿 또는 라이프사이클 메서드가 주어진 실행 시간을 초과하면 실패시키기 위해 사용한다. | O |
@ExtendWith | 클래스 | Extension을 선언적으로 등록하는데 사용한다. | O |
@RegisterExtension | 클래스 | 프로그래밍적으로 Extension을 등록하는데 사용한다. | O |
@TempDir | 클래스 | 라이프사이클 메서드 또는 테스트 메서드에서 필드 또는 파라미터 주입을 통해 임시 디렉토리를 제공하는데 사용한다. | X |
@Tag("fast")
@Test
void myTest1() {
}
@Tag("fast")
@Test
void myTest2() {
}
@Tag("fast")
@Test
void myTest3() {
}
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Tag("fast")
public @interface Fast {
}
@Fast
@Test
void myTest1() {
}
@Fast
@Test
void myTest2() {
}
@Fast
@Test
void myTest3() {
}
@Test
애노테이션도 조합하여 메타 애노테이션에 포함시킬 수 있다.@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Tag("fast")
@Test
public @interface FastTest {
}
@FastTest
void myTest1() {
}
@FastTest
void myTest2() {
}
@FastTest
void myTest3() {
}
static
) 멤버 클래스, @Nested
클래스, 테스트 메서드 등을 포함하고 있는 최상위 클래스abstract
) 클래스일 수 없다.@Test
, @RepeatedTest
, @ParameterizedTest
, @TestFactory
, @TestTemplate
@BeforeAll
, @AfterAll
, @BeforeEach
, @AfterEach
.abstract
) 메서드일 수 없다.void
여야 한다.public
일 필요는 없지만, private
이면 안된다.class StandardTests {
@BeforeAll
static void setUpAll() {
System.out.println("setUpAll");
}
@BeforeEach
void setUp() {
System.out.println("setUp");
}
@Test
void succeedingTest() {
System.out.println("succeedingTest");
assertTrue(true);
}
@Test
void failingTest() {
System.out.println("failingTest");
fail("의도적으로 실패시킨 테스트");
}
@Test
@Disabled("의도적으로 비활성화한 테스트")
void skippedTest() {
System.out.println("skippedTest");
}
@Test
void abortedTest() {
assumeTrue(false);
System.out.println("abortedTest");
}
@AfterEach
void tearDown() {
System.out.println("tearDown");
}
@AfterAll
static void tearDownAll() {
System.out.println("tearDownAll");
}
}
@DisplayName("A special test case")
class DisplayNameDemo {
@Test
@DisplayName("Custom test name containing spaces")
void testWithDisplayNameContainingSpaces() {
}
@Test
@DisplayName("╯°□°)╯")
void testWithDisplayNameContainingSpecialCharacters() {
}
@Test
@DisplayName("😱")
void testWithDisplayNameContainingEmoji() {
}
}
@DisplayNameGeneration
애노테이션을 통해 표시 이름 생성기를 설정할 수 있다.DisplayNameGenerator
를 구현해서 만들 수 있다.@DisplayName
에 제공된 표시 이름이 우선 순위가 높다.DisplayNameGenerator.Standard.class
: 메서드 이름과 괄호를 그대로 표시한다. (JUnit 5 기본값)DisplayNameGenerator.Simple.class
: 테스트 메서드에 파라미터가 없다면, 메서드 이름 뒤의 괄호(()
)를 제거한다.DisplayNameGenerator.ReplaceUnderScores.class
: 언더스코어(_
)를 공백(``)으로 치환한다.DisplayNameGenerator.IndicativeSentences.class
: 테스트 클래스 이름과 테스트 이름을 이어서 표시한다.@IndicativeSentencesGeneration
애노테이션을 이용하면 구분자를 추가로 명시할 수 있다.class DisplayNameGeneratorDemo {
@Nested
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
class A_year_is_not_supported {
@Test
void if_it_is_zero() {
}
@DisplayName("A negative value for year is not supported by the leap year computation.")
@ParameterizedTest(name = "For example, year {0} is not supported.")
@ValueSource(ints = { -1, -4 })
void if_it_is_negative(int year) {
}
}
@Nested
@IndicativeSentencesGeneration(separator = " -> ", generator = DisplayNameGenerator.ReplaceUnderscores.class)
class A_year_is_a_leap_year {
@Test
void if_it_is_divisible_by_4_but_not_by_100() {
}
@ParameterizedTest(name = "Year {0} is a leap year.")
@ValueSource(ints = { 2016, 2020, 2048 })
void if_it_is_one_of_the_following_years(int year) {
}
}
}
+-- DisplayNameGeneratorDemo [OK]
+-- A year is not supported [OK]
| +-- A negative value for year is not supported by the leap year computation. [OK]
| | +-- For example, year -1 is not supported. [OK]
| | '-- For example, year -4 is not supported. [OK]
| '-- if it is zero() [OK]
'-- A year is a leap year [OK]
+-- A year is a leap year -> if it is divisible by 4 but not by 100. [OK]
'-- A year is a leap year -> if it is one of the following years. [OK]
+-- Year 2016 is a leap year. [OK]
+-- Year 2020 is a leap year. [OK]
'-- Year 2048 is a leap year. [OK]
junit.jupiter.displayname.generator.default
프로퍼티에 기본으로 사용할 표시 이름 생성기의 FQCN(Fully Qualified Class Name)을 지정할 수 있다.src/test/resources/junit-platform.properties
에 다음과 같이 기본 생성기를 지정할 수 있다.junit.jupiter.displayname.generator.default=org.junit.jupiter.api.DisplayNameGenerator$ReplaceUnderscores
@DisplayName
으로 지정한 표시 이름@DisplayNameGenerator
로 지정한 생성기에 의해 생성된 표시 이름org.junit.jupiter.api.DisplayNameGenerator.Standard
)에 의해 생성된 표시 이름org.junit.jupiter.api.Assertions
클래스는 다양한 정적 단언 메서드를 제공한다.fail(String|Supplier?, Throwable?)
Throwable
을 cause
로 넘길 수 있다.assertTrue(boolean|BooleanSupplier, String|Supplier?)
true
인지 검사한다.assertFalse(boolean|BooleanSupplier, String|Supplier?)
false
인지 검사한다.assertNull(Object, String|Supplier?)
:null
인지 검사한다.assertNotNull(Object, String|Supplier?)
null
이 아닌지 검사한다.assertSame(Object, Object, String|Supplier?)
assertNotSame(Object, Object, String|Supplier?)
assertEquals(T, S, String|Supplier?)
assertNotEquals(T, S, String|Supplier?)
assertArrayEquals(T[], T[], String|Supplier?)
assertIterableEquals(Iterable, Iterable, String|Supplier?)
Iterable
(기댓값)이 두번째 Iterable
(실제값)과 동등한지 검사한다.assertLinesMatch(List, List, String|Supplier?)
assertAll(String?, Executable...|Collection|Stream)
Executable
목록을 가변인자로 전달받아 각 Executable
을 모두 실행한 뒤, 모두 성공했는지 검사한다.Executable
이 있다면, 실패한 검증 결과를 모아서 에러 메시지로 보여준다.assertThrows(Class, Executable, String|Supplier?)
Executable
을 실행했을 때, 첫번째 인자로 명시한 타입의 예외가 발생하는지 검사한다.assertDoesNotThrow(Executable|ThrowingSupplier, String|Supplier?)
Executable
을 실행했을 때 예외가 발생하지 않는지 검사한다.assertTimeout(Duration, Executable|ThrowingSupplier, String|Supplier?)
Executable
또는 ThrowingSupplier
를 실행했을 때 명시한 시간 안에 실행이 완료되는지 확인한다.assertTimeoutPreemptively(Duration, Executable|ThrowingSupplier, String|Supplier?)
Executable
또는 ThrowingSupplier
를 실행했을 때 명시한 시간 안에 실행이 완료되는지 확인한다.assertTimeoutPreemptively
는 제공된 Executable
또는 ThrowingSupplier
를 다른 스레드에서 실행한다.Executable
또는 ThrowingSupplier
의 코드들이 ThreadLocal
에 의존하면 부작용이 발생할 수 있다.@Transactional
테스트를 진행할 때, 제공된 Executable
또는 ThrowingSupplier
가 트랜잭션에 참여하는 스프링 컴포넌트를 호출하면, 이 컴포넌트는 테스트가 종료된 이후 롤백되지 않을 수 있다. assertEquals
, assertNotEquals
, assertArrayEquals
의 타입이 부동 소수점 타입인 경우, 세 번째 파라미터로 delta
값(float|double
)을 전달할 수 있다.AssertionFailedError
예외를 발생시킨다.class AssertionsDemo {
private final Calculator calculator = new Calculator();
private final Person person = new Person("Jane", "Doe");
@Test
void standardAssertions() {
assertEquals(2, calculator.add(1, 1));
assertEquals(4, calculator.multiply(2, 2),
"The optional failure message is now the last parameter");
assertTrue('a' < 'b', () -> "Assertion messages can be lazily evaluated -- "
+ "to avoid constructing complex messages unnecessarily.");
}
@Test
void groupedAssertions() {
// In a grouped assertion all assertions are executed, and all
// failures will be reported together.
assertAll("person",
() -> assertEquals("Jane", person.getFirstName()),
() -> assertEquals("Doe", person.getLastName())
);
}
@Test
void dependentAssertions() {
// Within a code block, if an assertion fails the
// subsequent code in the same block will be skipped.
assertAll("properties",
() -> {
String firstName = person.getFirstName();
assertNotNull(firstName);
// Executed only if the previous assertion is valid.
assertAll("first name",
() -> assertTrue(firstName.startsWith("J")),
() -> assertTrue(firstName.endsWith("e"))
);
},
() -> {
// Grouped assertion, so processed independently
// of results of first name assertions.
String lastName = person.getLastName();
assertNotNull(lastName);
// Executed only if the previous assertion is valid.
assertAll("last name",
() -> assertTrue(lastName.startsWith("D")),
() -> assertTrue(lastName.endsWith("e"))
);
}
);
}
@Test
void exceptionTesting() {
Exception exception = assertThrows(ArithmeticException.class, () ->
calculator.divide(1, 0));
assertEquals("/ by zero", exception.getMessage());
}
@Test
void timeoutNotExceeded() {
// The following assertion succeeds.
assertTimeout(ofMinutes(2), () -> {
// Perform task that takes less than 2 minutes.
});
}
@Test
void timeoutNotExceededWithResult() {
// The following assertion succeeds, and returns the supplied object.
String actualResult = assertTimeout(ofMinutes(2), () -> {
return "a result";
});
assertEquals("a result", actualResult);
}
@Test
void timeoutNotExceededWithMethod() {
// The following assertion invokes a method reference and returns an object.
String actualGreeting = assertTimeout(ofMinutes(2), AssertionsDemo::greeting);
assertEquals("Hello, World!", actualGreeting);
}
@Test
void timeoutExceeded() {
// The following assertion fails with an error message similar to:
// execution exceeded timeout of 10 ms by 91 ms
assertTimeout(ofMillis(10), () -> {
// Simulate task that takes more than 10 ms.
Thread.sleep(100);
});
}
@Test
void timeoutExceededWithPreemptiveTermination() {
// The following assertion fails with an error message similar to:
// execution timed out after 10 ms
assertTimeoutPreemptively(ofMillis(10), () -> {
// Simulate task that takes more than 10 ms.
new CountDownLatch(1).await();
});
}
private static String greeting() {
return "Hello, World!";
}
}
org.junit.jupiter.api.Assumptions
클래스는 다양한 정적 가정 메서드를 제공한다.assumeTrue(boolean|BooleanSupplier, String|Supplier)
true
인 경우에는 테스트를 이어서 진행하고, false
인 경우 테스트를 취소한다.assumeFalse(boolean|BooleanSupplier, String|Supplier)
false
인 경우에는 테스트를 이어서 진행하고, true
인 경우 테스트를 취소한다.assumingThat(boolean|BooleanSupplier, Executable)
true
인 경우에만 Executable
을 실행한다.Executable
실행 도중 예외가 발생하면, unchecked
예외로 다시 던진다.class AssumptionsDemo {
private final Calculator calculator = new Calculator();
@Test
void testOnlyOnCiServer() {
assumeTrue("CI".equals(System.getenv("ENV")));
// remainder of test
}
@Test
void testOnlyOnDeveloperWorkstation() {
assumeTrue("DEV".equals(System.getenv("ENV")),
() -> "Aborting test: not on developer workstation");
// remainder of test
}
@Test
void testInAllEnvironments() {
assumingThat("CI".equals(System.getenv("ENV")),
() -> {
// perform these assertions only on the CI server
assertEquals(2, calculator.divide(4, 2));
});
// perform these assertions in all environments
assertEquals(42, calculator.multiply(6, 7));
}
}
@Disabled
애노테이션을 이용해서 비활성화 할 수 있다.@Disabled
애노테이션에 값으로 설명을 전달해서 해당 테스트가 왜 비활성화 되었는지에 대한 설명을 명시할 수 있다.ExecutionCondition
API를 이용해서 특정 조건에 따라 테스트를 실행할 수 있다.ExecutionCondition
이 여러 개라면, 조건 중 하나만 만족해도 테스트가 비활성화된다.disabledReason
으로 명시할 수 있다.@EnabledOnOs
: 특정 운영체제에서만 테스트를 실행한다.@DisabledOnOs
: 특정 운영체제에서 테스트를 비활성화한다.@Test
@EnabledOnOs({ LINUX, MAC })
void onLinuxOrMac() {
}
@Test
@DisabledOnOs(WINDOWS)
void notOnWindows() {
}
@EnabledOnJre
: 특정 자바 런타임 버전에서만 테스트를 실행한다.@DisabledOnJre
: 특정 자바 런타임 버전에서 테스트를 비활성화한다.@EnabledForJreRange
: min
≤ 자바 런타임 버전 ≤ max
일 경우에만 테스트를 실행한다. @DisabledForJreRange
: min
≤ 자바 런타임 버전 ≤ max
일 경우 테스트를 비활성화한다.min
의 기본값은 JRE.JAVA_8
, max
의 기본값은 JRE.OTHER
이다.@Test
@EnabledOnJre(JAVA_8)
void onlyOnJava8() {
}
@Test
@EnabledOnJre({ JAVA_9, JAVA_10 })
void onJava9Or10() {
}
@Test
@EnabledForJreRange(min = JAVA_9, max = JAVA_11)
void fromJava9to11() {
}
@Test
@EnabledForJreRange(min = JAVA_9)
void fromJava9toCurrentJavaFeatureNumber() {
}
@Test
@EnabledForJreRange(max = JAVA_11)
void fromJava8To11() {
}
@Test
@DisabledOnJre(JAVA_9)
void notOnJava9() {
}
@Test
@DisabledForJreRange(min = JAVA_9, max = JAVA_11)
void notFromJava9to11() {
}
@Test
@DisabledForJreRange(min = JAVA_9)
void notFromJava9toCurrentJavaFeatureNumber() {
}
@Test
@DisabledForJreRange(max = JAVA_11)
void notFromJava8to11() {
}
@EnabledIfSystemProperty
: named
로 제공된 JVM 시스템 속성 값이 matches
정규식과 일치하면 테스트를 실행한다.@DisabledIfSystemProperty
: named
로 제공된 JVM 시스템 속성 값이 matches
정규식과 일치하면 테스트를 비활성화 한다.@Test
@EnabledIfSystemProperty(named = "os.arch", matches = ".*64.*")
void onlyOn64BitArchitectures() {
}
@Test
@DisabledIfSystemProperty(named = "ci-server", matches = "true")
void notOnCiServer() {
}
@EnabledIfEnvironmentVariable
: named
로 제공된 운영체제 시스템의 환경 변수 값이 matches
정규식과 일치하면 테스트를 실행한다.@DisabledIfEnvironmentVariable
: named
로 제공된 운영체제 시스템의 환경 변수 값이 matches
정규식과 일치하면 테스트를 비활성화 한다.@Test
@EnabledIfEnvironmentVariable(named = "ENV", matches = "staging-server")
void onlyOnStagingServer() {
}
@Test
@DisabledIfEnvironmentVariable(named = "ENV", matches = ".*development.*")
void notOnDeveloperWorkstation() {
}
@EnabledIf
: 명시된 조건 메서드의 boolean
리턴 값이 true
라면 테스트를 실행한다.@DisabledIf
: 명시된 조건 메서드의 boolean
리턴 값이 true
라면 테스트를 비활성화 한다.ExtensionContext
타입의 단일 파라미터를 받을 수 있다.@EnabledIf
또는 @DisabledIf
가 클래스 레벨에 사용될 경우, 조건 메서드는 반드시 정적(static
) 메서드여야 한다.@Test
@EnabledIf("customCondition")
void enabled() {
}
@Test
@DisabledIf("customCondition")
void disabled() {
}
boolean customCondition() {
}
@Tag
애노테이션으로 태그 할 수 있다.null
또는 공백일 수 없다.trim()
)된 태그는 다음과 같은 문자를 포함하면 안된다:,
)(
, )
,)&
)|
)!
)@TestMethodOrder
애노테이션을 이용해 실행 순서를 지정할 수 있다.@TestMethodOrder
애노테이션에는 MethodOrderer
의 구현체를 제공해야 한다.MethodOrderer
구현체로는 다음과 같은 것들이 있다:MethodOrderer.DisplayName.class
: 테스트의 표시 이름 순으로 실행한다. (alphanumerically)MethodOrderer.MethodName.class
: 테스트 메서드의 이름 순으로 실행한다. (alphanumerically)MethodOrderer.OrderAnnotation.class
: @Order
애노테이션에 지정된 값 오름차순으로 실행한다. (numerically)MethodOrderer.Random.class
: 특정 seed
값에 따른 임의 순서로 실행한다. (pseudo-randomly)@TestMethodOrder(OrderAnnotation.class)
class OrderedTestsDemo {
@Test
@Order(1)
void nullValues() {
}
@Test
@Order(2)
void emptyValues() {
}
@Test
@Order(3)
void validValues() {
}
}
junit.jupiter.testmethod.order.default
프로퍼티에 기본으로 사용할 표시 이름 생성기를 지정할 수 있다.src/test/resources/junit-platform.properties
에 다음과 같이 기본 생성기를 지정할 수 있다.junit.jupiter.testmethod.order.default=org.junit.jupiter.api.MethodOrderer$OrderAnnotation
@TestInstance(Lifecycle.PER_CLASS)
애노테이션을 사용하면 된다.@BeforeAll
또는 @AfterAll
애노테이션을 정적 메서드가 아닌 메서드에서 붙일 수 있다.@Nested
테스트 클래스에서도 @BeforeAll
또는 @AfterAll
애노테이션을 사용할 수 있다.@TestInstance
를 지정하지 않았다면, JUnit은 기본값인 PER_METHOD
모드를 사용한다.junit.jupiter.testinstance.lifecycle.default
에 TestInstance.Lifecycle
에 정의된 enum
값을 설정하면 된다.-Djunit.jupiter.testinstance.lifecycle.default=per_class
src/test/resources/junit-platform.properties
에 다음과 같이 지정한다.junit.jupiter.testinstance.lifecycle.default = per_class
@Nested
테스트는 계층적인 테스트 구조를 통해 여러 테스트 간의 관계를 표현하는데 도움을 준다.@Nested
를 붙일 테스트 클래스는 정적 클래스가 아니어야 한다.@DisplayName("A stack")
class TestingAStackDemo {
Stack 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());
}
}
}
}
ParameterResolver
는 런타임에 파라미터를 동적으로 결정(resolve)하는 테스트 익스텐션에 대한 API를 정의한다.ParameterResolver
에 의해 런타임에 결정된다.ParameterResolver
는 세 종류가 있다.TestInfoParameterResolver
TestInfo
타입이라면, TestInfoParameterResolver
는 TestInfo
인스턴스를 제공해준다.TestInfo
는 현재 컨테이너 또는 테스트 표시 이름, 테스트 클래스, 테스트 메서드, 태그 등의 정보를 제공한다.RepetitionInfoParameterResolver
@RepeatedTest
, @BeforeEach
, @AfterEach
메서드 파라미터의 타입이 RepetitionInfo
라면, RepetitionInfoParameterResolver
는 RepetitionInfo
인스턴스를 제공한다.RepetitionInfo
는 현재 반복 횟수와 총 반복 횟수 등의 정보를 제공한다.TestReporterParamterResolver
TestReporter
타입이라면, TestReporterParamterResolver
는 TestReporter
인스턴스를 제공해준다.TestReporter
는 현재 테스트 실행에 대한 추가적인 데이터를 발행(publish)하는데 사용할 수 있다.TestExecutionListener
의 reportingEntryPublished()
메서드에 의해 사용(consume)된다.@DisplayName("TestInfo Demo")
class TestInfoDemo {
TestInfoDemo(TestInfo testInfo) {
assertEquals("TestInfo Demo", testInfo.getDisplayName());
}
@BeforeEach
void init(TestInfo testInfo) {
String displayName = testInfo.getDisplayName();
assertTrue(displayName.equals("TEST 1") || displayName.equals("test2()"));
}
@Test
@DisplayName("TEST 1")
@Tag("my-tag")
void test1(TestInfo testInfo) {
assertEquals("TEST 1", testInfo.getDisplayName());
assertTrue(testInfo.getTags().contains("my-tag"));
}
@Test
void test2() {
}
}
class TestReporterDemo {
@Test
void reportSingleValue(TestReporter testReporter) {
testReporter.publishEntry("a status message");
}
@Test
void reportKeyValuePair(TestReporter testReporter) {
testReporter.publishEntry("a key", "a value");
}
@Test
void reportMultipleKeyValuePairs(TestReporter testReporter) {
Map values = new HashMap<>();
values.put("user name", "dk38");
values.put("award year", "1974");
testReporter.publishEntry(values);
}
}
ParameterResolver
이외의 ParameterResolver
를 사용하고 싶다면 @ExtendWith
애노테이션으로 명시적으로 ParameterResolver
를 지정해주어야 한다.@ExtendWith(MockitoExtension.class)
, @ExtendWith(SpringExtension.class)
TypeBasedParameterResolver
추상 클래스 구현한 후 이를 @ExtendWith
애노테이션으로 등록해서 사용할 수 있다.default
) 메서드에는 @Test
, @RepeatedTest
, @ParameterizedTest
, @TestFactory
, @TestTemplate
, @BeforeEach
, @AfterEach
등을 선언할 수 있다.@TestInstance(Lifecycle.PER_CLASS)
로 선언되어 있다면, static
메서드 또는 default
메서드에 @BeforeAll
, @AfterAll
을 선언할 수 있다. @ExtendWith
과 @Tag
는 테스트 인터페이스에 선언할 수 있고, 해당 인터페이스를 구현한 테스트 클래스는 자동으로 태그와 익스텐션을 상속받는다.@TestInstance(Lifecycle.PER_CLASS)
interface TestLifecycleLogger {
static final Logger logger = Logger.getLogger(TestLifecycleLogger.class.getName());
@BeforeAll
default void beforeAllTests() {
logger.info("Before all tests");
}
@AfterAll
default void afterAllTests() {
logger.info("After all tests");
}
@BeforeEach
default void beforeEachTest(TestInfo testInfo) {
logger.info(() -> String.format("About to execute [%s]",
testInfo.getDisplayName()));
}
@AfterEach
default void afterEachTest(TestInfo testInfo) {
logger.info(() -> String.format("Finished executing [%s]",
testInfo.getDisplayName()));
}
}
interface TestInterfaceDynamicTestsDemo {
@TestFactory
default Stream dynamicTestsForPalindromes() {
return Stream.of("racecar", "radar", "mom", "dad")
.map(text -> dynamicTest(text, () -> assertTrue(text,equals(text)))); }
}
public interface Testable {
T createValue();
}
public interface EqualsContract extends Testable {
T createNotEqualValue();
@Test
default void valueEqualsItself() {
T value = createValue();
assertEquals(value, value);
}
@Test
default void valueDoesNotEqualNull() {
T value = createValue();
assertFalse(value.equals(null));
}
@Test
default void valueDoesNotEqualDifferentValue() {
T value = createValue();
T differentValue = createNotEqualValue();
assertNotEquals(value, differentValue);
assertNotEquals(differentValue, value);
}
}
public interface ComparableContract> extends Testable {
T createSmallerValue();
@Test
default void returnsZeroWhenComparedToItself() {
T value = createValue();
assertEquals(0, value.compareTo(value));
}
@Test
default void returnsPositiveNumberWhenComparedToSmallerValue() {
T value = createValue();
T smallerValue = createSmallerValue();
assertTrue(value.compareTo(smallerValue) > 0);
}
@Test
default void returnsNegativeNumberWhenComparedToLargerValue() {
T value = createValue();
T smallerValue = createSmallerValue();
assertTrue(smallerValue.compareTo(value) < 0);
}
}
class StringTests implements ComparableContract, EqualsContract {
@Override
public String createValue() {
return "banana";
}
@Override
public String createSmallerValue() {
return "apple"; // 'a' < 'b' in "banana"
}
@Override
public String createNotEqualValue() {
return "cherry";
}
}
@RepeatedTest
애노테이션을 이용하면 원하는 횟수만큼 반복 테스트를 할 수 있다.@Test
메서드와 동일한 라이프사이클 콜백과 익스텐션을 지원한다.@RepeatedTest(10)
void repeatedTest() {
}
name
속성을 사용해서 사용자 지정 표시 이름도 설정할 수 있다.DisplayName
: @RepeatedTest
메서드의 표시 이름{currentRepetition}
: 현재 반복 횟수{totalRepetition}
: 총 반복 횟수"repetition {currentRepetition} of {totalRepetitions}"
이다.RepeatedTest.LONG_DISPLAY_NAME
패턴을 사용할 수 있다.RepeatedTest.LONG_DISPLAY_NAME
: "DisplayName:: repetition {currentRepetition} of {totalRepetitions}"
@RepeatedTest
, @BeforeEach
, @AfterEach
메서드에서 RepetitionInfo
타입의 인스턴스를 주입받을 수 있다.import static org.junit.jupiter.api.Assertions.assertEquals;
import java.util.logging.Logger;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.RepeatedTest;
import org.junit.jupiter.api.RepetitionInfo;
import org.junit.jupiter.api.TestInfo;
class RepeatedTestsDemo {
@BeforeEach
void beforeEach(TestInfo testInfo, RepetitionInfo repetitionInfo) {
int currentRepetition = repetitionInfo.getCurrentRepetition();
int totalRepetitions = repetitionInfo.getTotalRepetitions();
String methodName = testInfo.getTestMethod().get().getName();
logger.info(String.format("About to execute repetition %d of %d for %s", //
currentRepetition, totalRepetitions, methodName));
}
@RepeatedTest(10)
void repeatedTest() {
}
@RepeatedTest(5)
void repeatedTestWithRepetitionInfo(RepetitionInfo repetitionInfo) {
assertEquals(5, repetitionInfo.getTotalRepetitions());
}
@RepeatedTest(value = 1, name = "{displayName} {currentRepetition}/{totalRepetitions}")
@DisplayName("Repeat!")
void customDisplayName(TestInfo testInfo) {
assertEquals("Repeat! 1/1", testInfo.getDisplayName());
}
@RepeatedTest(value = 1, name = RepeatedTest.LONG_DISPLAY_NAME)
@DisplayName("Details...")
void customDisplayNameWithLongPattern(TestInfo testInfo) {
assertEquals("Details... :: repetition 1 of 1", testInfo.getDisplayName());
}
@RepeatedTest(value = 5, name = "Wiederholung {currentRepetition} von {totalRepetitions}")
void repeatedTestInGerman() {
}
}
├─ RepeatedTestsDemo ✔
│ ├─ repeatedTest() ✔
│ │ ├─ repetition 1 of 10 ✔
│ │ ├─ repetition 2 of 10 ✔
│ │ ├─ repetition 3 of 10 ✔
│ │ ├─ repetition 4 of 10 ✔
│ │ ├─ repetition 5 of 10 ✔
│ │ ├─ repetition 6 of 10 ✔
│ │ ├─ repetition 7 of 10 ✔
│ │ ├─ repetition 8 of 10 ✔
│ │ ├─ repetition 9 of 10 ✔
│ │ └─ repetition 10 of 10 ✔
│ ├─ repeatedTestWithRepetitionInfo(RepetitionInfo) ✔
│ │ ├─ repetition 1 of 5 ✔
│ │ ├─ repetition 2 of 5 ✔
│ │ ├─ repetition 3 of 5 ✔
│ │ ├─ repetition 4 of 5 ✔
│ │ └─ repetition 5 of 5 ✔
│ ├─ Repeat! ✔
│ │ └─ Repeat! 1/1 ✔
│ ├─ Details... ✔
│ │ └─ Details... :: repetition 1 of 1 ✔
│ └─ repeatedTestInGerman() ✔
│ ├─ Wiederholung 1 von 5 ✔
│ ├─ Wiederholung 2 von 5 ✔
│ ├─ Wiederholung 3 von 5 ✔
│ ├─ Wiederholung 4 von 5 ✔
│ └─ Wiederholung 5 von 5 ✔
@ParameterizedTest
애노테이션을 이용한 파라미터화된 테스트는 하나의 테스트를 각기 다른 인수로 여러 번 실행할 수 있게 해준다.@ParameterizedTest
@ValueSource(strings = { "racecar", "radar", "able was I ere I saw elba" })
void palindromes(String candidate) {
assertTrue(StringUtils.isPalindrome(candidate));
}
palindromes(String) ✔
├─ [1] candidate=racecar ✔
├─ [2] candidate=radar ✔
└─ [3] candidate=able was I ere I saw elba ✔
TestInfo
, TestReporter
등의 추가적인 인자는 ParameterResolver
에 의해 제공받을 수 있다.ParameterResolver
에 의해 제공되는 인자@ValueSource
@ValueSource
로 리터럴 값의 단일 배열을 소스로 지정할 수 있다.@ValueSource
의 리터럴 값으로 사용할 수 있는 타입은 short
, byte
, int
, long
, float
, double
, char
, boolean
, String
, Class
등이 있다.@ParameterizedTest
@ValueSource(ints = { 1, 2, 3 })
void testWithValueSource(int argument) {
assertTrue(argument > 0 && argument < 4);
}
@NullSource
null
값을 인자로 제공한다.@EmptySource
String
, List
, Set
, Map
, 원시 타입 배열(ex. int[]
, char[][]
등), 객체 배열(ex. String[]
, Integer[][]
등) 중 하나여야 한다.@NullAndEmptySource
@NullSource
와 @EmptySource
의 기능을 결합한 조합(composed) 애노테이션정리:
@ValueSource(strings = {" ", " ", "\t", "\n"})
와 같은 형태를 사용할 수 있다.@ParameterizedTest
@NullSource
@EmptySource
@ValueSource(strings = { " ", " ", "\t", "\n" })
void nullEmptyAndBlankStrings(String text) {
assertTrue(text == null || text.trim().isEmpty());
}
@ParameterizedTest
@NullAndEmptySource
@ValueSource(strings = { " ", " ", "\t", "\n" })
void nullEmptyAndBlankStrings(String text) {
assertTrue(text == null || text.trim().isEmpty());
}
nullEmptyAndBlankStrings
메서드는 총 6번 호출된다.null
문자열 호출" "
, " "
, "\t"
, "\n"
)@EnumSource
value
로 테스트에 사용할 enum
클래스를 지정할 수 있다.value
가 제공되지 않은 경우, 첫 번째 메서드 파라미터의 타입이 사용된다.enum
타입이 아닐 경우 테스트는 실패한다.names
속성으로 어떤 상수를 사용할 지 지정할 수 있다. names
를 지정하지 않을 경우, 모든 상수가 사용된다.mode
속성으로 names
로 제공된 값을 어떻게 처리할 것인지 선언할 수 있다.EXLUDE
, MATCH_ALL
등@ParameterizedTest
@EnumSource(ChronoUnit.class)
void testWithEnumSource(TemporalUnit unit) {
assertNotNull(unit);
}
@ParameterizedTest
@EnumSource
void testWithEnumSourceWithAutoDetection(ChronoUnit unit) {
assertNotNull(unit);
}
@ParameterizedTest
@EnumSource(names = { "DAYS", "HOURS" })
void testWithEnumSourceInclude(ChronoUnit unit) {
assertTrue(EnumSet.of(ChronoUnit.DAYS, ChronoUnit.HOURS).contains(unit));
}
@ParameterizedTest
@EnumSource(mode = EXCLUDE, names = { "ERAS", "FOREVER" })
void testWithEnumSourceExclude(ChronoUnit unit) {
assertFalse(EnumSet.of(ChronoUnit.ERAS, ChronoUnit.FOREVER).contains(unit));
}
@ParameterizedTest
@EnumSource(mode = MATCH_ALL, names = "^.*DAYS$")
void testWithEnumSourceRegex(ChronoUnit unit) {
assertTrue(unit.name().endsWith("DAYS"));
}
@MethodSource
@MethodSource
로 하나 이상의 팩토리 메서드를 지정할 수 있다.@ParameterizedMethod
와 동일한 이름의 팩토리 메서드를 찾게 된다.static
이어야 한다.@TestInstance(Lifecycle.PER_CLASS)
를 사용할 경우 테스트 클래스에 위치한 팩토리 메서드는 static
이 아닐 수도 있다.Stream
으로 변환될 수 있다면 여러 타입이 가능하다.Stream
, IntStream
, LongStream
, DoubleStream
, Collection
, Iterator
, Iterable
, 객체 배열, 원시 값 배열 등Arguments
의 인스턴스, 객체 배열, 또는 단일 값으로 제공된다.Stream
을 리턴해도 된다.Arguments
인스턴스의 콜렉션, 스트림, 배열, 또는 객체 배열을 리턴해야 한다.@ParameterizedTest
@MethodSource("stringProvider")
void testWithExplicitLocalMethodSource(String argument) {
assertNotNull(argument);
}
static Stream stringProvider() {
return Stream.of("apple", "banana");
}
@ParameterizedTest
@MethodSource
void testWithDefaultLocalMethodSource(String argument) {
assertNotNull(argument);
}
static Stream testWithDefaultLocalMethodSource() {
return Stream.of("apple", "banana");
}
@ParameterizedTest
@MethodSource("range")
void testWithRangeMethodSource(int argument) {
assertNotEquals(9, argument);
}
static IntStream range() {
return IntStream.range(0, 20).skip(10);
}
@ParameterizedTest
@MethodSource("stringIntAndListProvider")
void testWithMultiArgMethodSource(String str, int num, List list) {
assertEquals(5, str.length());
assertTrue(num >=1 && num <=2);
assertEquals(2, list.size());
}
static Stream stringIntAndListProvider() {
return Stream.of(
arguments("apple", 1, Arrays.asList("a", "b")),
arguments("lemon", 2, Arrays.asList("x", "y"))
);
}
class ExternalMethodSourceDemo {
@ParameterizedTest
@MethodSource("example.StringsProviders#tinyStrings")
void testWithExternalMethodSource(String tinyString) {
// test with tiny string
}
}
class StringsProviders {
static Stream tinyStrings() {
return Stream.of(".", "oo", "OOO");
}
}
@CsvSource
@CsvSource
를 이용하면 인자의 리스트를 CSV(comma-seperated values)로 표현 할 수 있다.delimiter
속성으로 구분자(delimiter)를 지정할 수 있으며, 기본 값은 콤마(,
)다.delimiterString
속성을 사용할 수 있다.delimiter
와 delimiterString
은 동시에 사용될 수 없다.@CsvSource
는 인용 문자로 작은 따옴표('
)를 사용한다.''
)는 빈 문자열로, 완전히 빈 값은 null
로 해석된다.null
로 해석된 값의 타입이 원시형인 경우 ArgumentConversionException
예외가 발생한다.emptyValue
속성으로 빈 문자열 특정 문자열로 해석하도록 할 수 있다.nullValue
속성으로 특정 문자열들을 null
로 해석하도록 할 수 있다.nullValue
속성과 관계 없이 항상 null
로 변환된다.Example Input | Resulting Argument List |
---|---|
@CsvSource({ "apple, banana" }) | "apple" , "banana" |
@CsvSource({ "apple, 'lemon, lime'" }) | "apple" , "lemon, lime" |
@CsvSource({ "apple, ''" }) | "apple" , "" |
@CsvSource({ "apple, " }) | "apple" , null |
@CsvSource(value = { "apple, banana, NIL" }, nullValues = "NIL") | "apple" , "banana" , null |
@CsvFileSource
@CsvFileSource
를 사용하면 로컬 파일 시스템 또는 클래스 경로(classpath)의 CSV 파일을 사용할 수 있다.delimiter
속성으로 구분자(delimiter)를 지정할 수 있으며, 기본 값은 콤마(,
)다.delimiterString
속성을 사용할 수 있다.delimiter
와 delimiterString
은 동시에 사용될 수 없다.#
문자로 시작하는 라인은 주석으로 해석되며, 무시된다.@CsvSource
와 달리, 인용 문자로 큰 따옴표("
)를 사용한다.""
)는 빈 문자열로, 완전히 빈 값은 null
로 해석된다.null
로 해석된 값의 타입이 원시형인 경우 ArgumentConversionException
예외가 발생한다.emptyValue
속성으로 빈 문자열 특정 문자열로 해석하도록 할 수 있다.nullValue
속성으로 특정 문자열들을 null
로 해석하도록 할 수 있다.nullValue
속성과 관계 없이 항상 null
로 변환된다.@ParameterizedTest
@CsvFileSource(resources = "/two-column.csv", numLinesToSkip = 1)
void testWithCsvFileSourceFromClasspath(String country, int reference) {
assertNotNull(country);
assertNotEquals(0, reference);
}
@ParameterizedTest
@CsvFileSource(files = "src/test/resources/two-column.csv", numLinesToSkip = 1)
void testWithCsvFileSourceFromFile(String country, int reference) {
assertNotNull(country);
assertNotEquals(0, reference);
}
Country, reference
Sweden, 1
Poland, 2
"United States of America", 3
@ArgumentsSource
@ArgumentsSource
로 재사용 가능한 사용자 지정 ArgumentsProvider
를 지정할 수 있다.ArgumentsProvider
는 반드시 최상위 클래스 또는 정적(static) 내부(nested) 클래스에 정의되어야 한다.@ParameterizedTest
@ArgumentsSource(MyArgumentsProvider.class)
void testWithArgumentsSource(String argument) {
assertNotNull(argument);
}
public class MyArgumentsProvider implements ArgumentsProvider {
@Override
public Stream provideArguments(ExtensionContext context) {
return Stream.of("apple", "banana").map(Arguments::of);
}
}
확장 변환(Widening Conversion)
@ParameterizedTest
에 제공된 인자에 대한 확장 변환을 지원한다.@ValueSource(ints = { 1, 2, 3 })
는 int
타입 뿐만 아니라, long
, float
, double
타입으로 확장 변환 될 수 있다.묵시적 변환(Implicit Conversion)
@CsvSource
와 같은 유스케이스를 지원하기 위해 몇 가지 빌트인 묵시적 타입 변환기를 제공한다.@ParameterizedTest
테스트 메서드의 파라미터 타입이 TimeUnit
이고, 제공된 소스가 String
타입이라면, 해당 문자열은 자동으로 TimeUnit
enum
상수로 변환된다.String
리터럴은 byte
, short
, int
, long
또는 해당 원시형의 박스(boxed) 타입으로 변환 된다.@ParameterizedTest
@ValueSource(strings = "SECONDS")
void testWithImplicitArgumentConversion(ChronoUnit argument) {
assertNotNull(argument.name());
}
Target Type | Example |
---|---|
boolean /Boolean | "true" → true |
byte /Byte | "15" , "0xF" , or "017" → (byte) 15 |
char /Character | "o" → 'o' |
short /Short | "15" , "0xF" , or "017" → (short) 15 |
int /Integer | "15" , "0xF" , or "017" → 15 |
long /Long | "15" , "0xF" , or "017" → 15L |
float /Float | "1.0" → 1.0f |
double /Double | "1.0" → 1.0d |
Enum subclass | "SECONDS" → TimeUnit.SECONDS |
java.io.File | "/path/to/file" → new File("/path/to/file") |
java.lang.Class | "java.lang.Integer" → java.lang.Integer.class (내부 클래스의 경우에는 $ 를 사용한다. ex. "java.lang.Thread$State" ) |
java.lang.Class | "byte" → byte.class (원시 타입 지원) |
java.lang.Class | "char[]" → char[].class (배열 타입 지원) |
java.math.BigDecimal | "123.456e789" → new BigDecimal("123.456e789") |
java.math.BigInteger | "1234567890123456789" → new BigInteger("1234567890123456789") |
java.net.URI | "https://junit.org/" → URI.create("https://junit.org/") |
java.net.URL | "https://junit.org/" → new URL("https://junit.org/") |
java.nio.charset.Charset | "UTF-8" → Charset.forName("UTF-8") |
java.nio.file.Path | "/path/to/file" → Paths.get("/path/to/file") |
java.time.Duration | "PT3S" → Duration.ofSeconds(3) |
java.time.Instant | "1970-01-01T00:00:00Z" → Instant.ofEpochMilli(0) |
java.time.LocalDateTime | "2017-03-14T12:34:56.789" → LocalDateTime.of(2017, 3, 14, 12, 34, 56, 789_000_000) |
java.time.LocalDate | "2017-03-14" → LocalDate.of(2017, 3, 14) |
java.time.LocalTime | "12:34:56.789" → LocalTime.of(12, 34, 56, 789_000_000) |
java.time.MonthDay | "--03-14" → MonthDay.of(3, 14) |
java.time.OffsetDateTime | "2017-03-14T12:34:56.789Z" → OffsetDateTime.of(2017, 3, 14, 12, 34, 56, 789_000_000, ZoneOffset.UTC) |
java.time.OffsetTime | "12:34:56.789Z" → OffsetTime.of(12, 34, 56, 789_000_000, ZoneOffset.UTC) |
java.time.Period | "P2M6D" → Period.of(0, 2, 6) |
java.time.YearMonth | "2017-03" → YearMonth.of(2017, 3) |
java.time.Year | "2017" → Year.of(2017) |
java.time.ZonedDateTime | "2017-03-14T12:34:56.789Z" → ZonedDateTime.of(2017, 3, 14, 12, 34, 56, 789_000_000, ZoneOffset.UTC) |
java.time.ZoneId | "Europe/Berlin" → ZoneId.of("Europe/Berlin") |
java.time.ZoneOffset | "+02:30" → ZoneOffset.ofHoursMinutes(2, 30) |
java.util.Currency | "JPY" → Currency.getInstance("JPY") |
java.util.Locale | "en" → new Locale("en") |
java.util.UUID | "d043e930-7b3b-48e3-bdbe-5a3ccfb833db" → UUID.fromString("d043e930-7b3b-48e3-bdbe-5a3ccfb833db") |
문자열에서 객체 변환 대체(Fallback String-to-Object Conversion)
String
에서 특정 타입으로 자동 변환하기 위한 대체 메커니즘(fallback mechanism)도 제공한다.String
인자를 받아 대상 타입의 인스턴스를 리턴한다.private
이 아닌 static
메서드여야 한다.String
인자를 받아 대상 타입의 인스턴스를 리턴하는 생성자private
이면 안되며, 대상 타입은 반드시 최상위 클래스 또는 정적 내부 클래스에 선언되어야 한다.@ParameterizedTest
@ValueSource(strings = "42 Cats")
void testWithImplicitFallbackArgumentConversion(Book book) {
assertEquals("42 Cats", book.getTitle());
}
public class Book {
private final String title;
private Book(String title) {
this.title = title;
}
public static Book fromTitle(String title) {
return new Book(title);
}
public String getTitle() {
return this.title;
}
}
명시적 변환(Explicit Conversion)
@ConvertWith
애노테이션으로 ArgumentConverter
를 지정할 수도 있다.ArgumentConverter
는 반드시 최상위 클래스 또는 정적 내부 클래스에 선언되어야 한다.TypedArgumentConverter
를 상속해서 타입 체크를 위한 보일러플레이트를 피할 수 있다.JavaTimeArgumentConverter
: @JavaTimeConversionPattern
조합 애노테이션을 통해 사용한다.@ParameterizedTest
@EnumSource(ChronoUnit.class)
void testWithExplicitArgumentConversion(
@ConvertWith(ToStringArgumentConverter.class) String argument) {
assertNotNull(ChronoUnit.valueOf(argument));
}
public class ToStringArgumentConverter extends SimpleArgumentConverter {
@Override
protected Object convert(Object source, Class<?> targetType) {
assertEquals(String.class, targetType, "Can only convert to String");
if (source instanceof Enum<?>) {
return ((Enum<?>) source).name();
}
return String.valueOf(source);
}
}
public class ToLengthArgumentConverter extends TypedArgumentConverter<String, Integer> {
protected ToLengthArgumentConverter() {
super(String.class, Integer.class);
}
@Override
protected Integer convert(String source) {
return source.length();
}
}
@ParameterizedTest
@ValueSource(strings = { "01.01.2017", "31.12.2017" })
void testWithExplicitJavaTimeConverter(
@JavaTimeConversionPattern("dd.MM.yyyy") LocalDate argument) {
assertEquals(2017, argument.getYear());
}
@ParameterizedTest
메서드에 제공된 각 인자는 메서드 파라미터 한 개에 대응한다.ArgumentAccessor
를 사용할 수 있다.ArgumnetAccessor
API를 사용하면, 제공된 인자들을 하나의 인자를 통해 접근할 수 있다. ArgumentAccessor
는 묵시적 타입 변환도 지원한다.@ParameterizedTest
@CsvSource({
"Jane, Doe, F, 1990-05-20",
"John, Doe, M, 1990-10-22"
})
void testWithArgumentsAccessor(ArgumentsAccessor arguments) {
Person person = new Person(arguments.getString(0),
arguments.getString(1),
arguments.get(2, Gender.class),
arguments.get(3, LocalDate.class));
if (person.getFirstName().equals("Jane")) {
assertEquals(Gender.F, person.getGender());
}
else {
assertEquals(Gender.M, person.getGender());
}
assertEquals("Doe", person.getLastName());
assertEquals(1990, person.getDateOfBirth().getYear());
}
ArgumentsAccessor
타입의 파라미터에 ArgumentsAccessor
의 인스턴스가 자동으로 주입된다.사용자 지정 애그리게이터(Cusom Aggregator)
JUnit은 ArgumentsAccessor
를 통한 인자 직접 접근과는 별개로 사용자 지정, 재사용 가능한 애그리게이터(aggregator) 또한 지원한다.
사용자 지정 애그리게이터를 사용하기 위해서는 ArgumnetAggregator
인터페이스를 구현하고, 해당 파라미터에 @AggregateWith
애노테이션으로 애그리게이터를 등록해 주어야 한다.
파라미터화 테스트가 호출되면 애그리게이터의 결과가 해당 인자로 제공된다.
ArgumentsAggregator
는 최상위 클래스 또는 정적 내부 클래스에 선언되어야 한다.
코드 여러 곳에 @AggregateWith
애노테이션이 중복해서 나타날 경우, 이를 조합 애노테이션으로 추출할 수도 있다.
@ParameterizedTest
@CsvSource({
"Jane, Doe, F, 1990-05-20",
"John, Doe, M, 1990-10-22"
})
void testWithArgumentsAggregator(@AggregateWith(PersonAggregator.class) Person person) {
// perform assertions against person
}
public class PersonAggregator implements ArgumentsAggregator {
@Override
public Person aggregateArguments(ArgumentsAccessor arguments, ParameterContext context) {
return new Person(arguments.getString(0),
arguments.getString(1),
arguments.get(2, Gender.class),
arguments.get(3, LocalDate.class));
}
}
@ParameterizedTest
@CsvSource({
"Jane, Doe, F, 1990-05-20",
"John, Doe, M, 1990-10-22"
})
void testWithCustomAggregatorAnnotation(@CsvToPerson Person person) {
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
@AggregateWith(PersonAggregator.class)
public @interface CsvToPerson {
}
String
표현 값을 포함한다.@ParameterizedTest
애노테이션의 name
속성으로 표시 이름을 사용자화할 수 있다.name
속성은 MessageFormat
패턴이므로, 작은 따옴표('
)를 표시하기 위해서는 작은 따옴표 두 개(''
)를 사용해야 한다.@DisplayName("Display name of container")
@ParameterizedTest(name = "{index} ==> the rank of ''{0}'' is {1}")
@CsvSource({ "apple, 1", "banana, 2", "'lemon, lime', 3" })
void testWithCustomDisplayNames(String fruit, int rank) {
}
Display name of container ✔
├─ 1 ==> the rank of 'apple' is 1 ✔
├─ 2 ==> the rank of 'banana' is 2 ✔
└─ 3 ==> the rank of 'lemon, lime' is 3 ✔
Placeholder | Description |
---|---|
DisplayName | 메서드의 표시 이름 |
{index} | 현재 호출 인덱스 (1부터 시작) |
{arguments} | 콤마로 구분된 완전한 인자의 목록 |
{argumentsWithNames} | 콤마로 구분된 완전한 인자의 목록 (파라미터 이름 포함) |
{0} , {1} , … | 각 개별 인자 하나 |
String
표현 값이 설정된 최대 길이(기본값 = 512 문자)를 넘어가면 생략된다.junit.jupiter.params.displayname.argument.maxlength
설정 파라미터를 통해 설정할 수 있다.@Test
메서드와 동일하다.@BeforeEach
메서드는 각 호출 이전에 실행된다.@ParameterizedTest
메서드에서 ParameterResolver
익스텐션도 함께 사용할 수 있지만, 인자 소스로부터 받을 파라미터가 파라미터 목록의 앞에 먼저 와야 한다.@BeforeEach
void beforeEach(TestInfo testInfo) {
}
@ParameterizedTest
@ValueSource(strings = "apple")
void testWithRegularParameterResolver(String argument, TestReporter testReporter) {
testReporter.publishEntry("argument", argument);
}
@AfterEach
void afterEach(TestInfo testInfo) {
}
@TestTemplate
메서드는 일반적인 테스트 케이스가 아니라 테스트 케이스를 위한 템플릿이다.TestTemplateInvocationContextProvider
익스텐션과 함께 사용되어야 한다.@Test
메서드의 실행처럼 동일한 라이프사이클 콜백과 익스텐션을 지원한다.@Test
애노테이션은 컴파일 시간에 완전히 결정된다는 관점에서 정적이다.@Test
메서드의 동작(behavior)은 런타임에는 변경될 수 없다.@TestFactory
가 달린 팩토리 메서드는 런타임에 동적 테스트를 생성한다.@TestFactory
메서드는 @Test
메서드와 대조적으로 테스트 케이스가 아니라 테스트 케이스를 위한 팩토리다.@TestFactory
메서드는 하나의 DynamicNode
, Stream
, Collection
, Iterable
, Iterator
, 또는 DynamicNode
인스턴스의 배열을 리턴해야 한다.DynamicNode
의 생성 가능한 자식 클래스로는 DynamicContainer
와 DynamicTest
가 있다.DynamicContainer
인스턴스는 표시 이름과 동적 자식 노드로 이루어지며, 임의로 동적 노드의 중첩 계층을 만들 수 있다.DynamicTest
인스턴스는 게으르게(lazily) 실행되며, 동적 또는 심지어 비결정적(non-deterministic)인 테스트 케이스의 생성을 할 수 있다.@TestFactory
메서드에 의해 리턴되는 모든 Stream
은 stream.close()
를 호출함으로서 적절히 종료되므로, Files.lines()
와 같은 리소스를 안전하게 사용할 수 있다.@TestFactory
메서드는 @Test
메서드와 마찬가지로 private
이거나 static
일 수 없으며, 원한다면 ParameterResolver
가 제공하는 파라미터를 선언할 수 있다.DynamicTest
는 런타임에 생성되는 테스트 케이스로, 표시 이름과 Executable
로 이루어진다. Executable
은 @FunctionalInterface
이므로, 동적 테스트의 구현은 람다 표현식(lambda expression) 또는 메서드 레퍼런스(method references)로 사용될 수 있다.@Test
의 라이프사이클과 상당히 다르다.@BeforeEach
와 @AfterEach
메서드와, 익스텐션 콜백들은 @TestFactory
메서드에 의해 실행되지, 동적 테스트에 의해 실행되지 않는다.class DynamicTestsDemo {
private final Calculator calculator = new Calculator();
// This will result in a JUnitException!
@TestFactory
List<String> dynamicTestsWithInvalidReturnType() {
return Arrays.asList("Hello");
}
@TestFactory
Collection<DynamicTest> dynamicTestsFromCollection() {
return Arrays.asList(
dynamicTest("1st dynamic test", () -> assertTrue(isPalindrome("madam"))),
dynamicTest("2nd dynamic test", () -> assertEquals(4, calculator.multiply(2, 2)))
);
}
@TestFactory
Iterable<DynamicTest> dynamicTestsFromIterable() {
return Arrays.asList(
dynamicTest("3rd dynamic test", () -> assertTrue(isPalindrome("madam"))),
dynamicTest("4th dynamic test", () -> assertEquals(4, calculator.multiply(2, 2)))
);
}
@TestFactory
Iterator<DynamicTest> dynamicTestsFromIterator() {
return Arrays.asList(
dynamicTest("5th dynamic test", () -> assertTrue(isPalindrome("madam"))),
dynamicTest("6th dynamic test", () -> assertEquals(4, calculator.multiply(2, 2)))
).iterator();
}
@TestFactory
DynamicTest[] dynamicTestsFromArray() {
return new DynamicTest[] {
dynamicTest("7th dynamic test", () -> assertTrue(isPalindrome("madam"))),
dynamicTest("8th dynamic test", () -> assertEquals(4, calculator.multiply(2, 2)))
};
}
@TestFactory
Stream<DynamicTest> dynamicTestsFromStream() {
return Stream.of("racecar", "radar", "mom", "dad")
.map(text -> dynamicTest(text, () -> assertTrue(isPalindrome(text))));
}
@TestFactory
Stream<DynamicTest> dynamicTestsFromIntStream() {
// Generates tests for the first 10 even integers.
return IntStream.iterate(0, n -> n + 2).limit(10)
.mapToObj(n -> dynamicTest("test" + n, () -> assertTrue(n % 2 == 0)));
}
@TestFactory
Stream<DynamicTest> generateRandomNumberOfTestsFromIterator() {
// Generates random positive integers between 0 and 100 until
// a number evenly divisible by 7 is encountered.
Iterator<Integer> inputGenerator = new Iterator<Integer>() {
Random random = new Random();
int current;
@Override
public boolean hasNext() {
current = random.nextInt(100);
return current % 7 != 0;
}
@Override
public Integer next() {
return current;
}
};
// Generates display names like: input:5, input:37, input:85, etc.
Function<Integer, String> displayNameGenerator = (input) -> "input:" + input;
// Executes tests based on the current input value.
ThrowingConsumer<Integer> testExecutor = (input) -> assertTrue(input % 7 != 0);
// Returns a stream of dynamic tests.
return DynamicTest.stream(inputGenerator, displayNameGenerator, testExecutor);
}
@TestFactory
Stream<DynamicTest> dynamicTestsFromStreamFactoryMethod() {
// Stream of palindromes to check
Stream<String> inputStream = Stream.of("racecar", "radar", "mom", "dad");
// Generates display names like: racecar is a palindrome
Function<String, String> displayNameGenerator = text -> text + " is a palindrome";
// Executes tests based on the current input value.
ThrowingConsumer<String> testExecutor = text -> assertTrue(isPalindrome(text));
// Returns a stream of dynamic tests.
return DynamicTest.stream(inputStream, displayNameGenerator, testExecutor);
}
@TestFactory
Stream<DynamicNode> dynamicTestsWithContainers() {
return Stream.of("A", "B", "C")
.map(input -> dynamicContainer("Container " + input, Stream.of(
dynamicTest("not null", () -> assertNotNull(input)),
dynamicContainer("properties", Stream.of(
dynamicTest("length > 0", () -> assertTrue(input.length() > 0)),
dynamicTest("not empty", () -> assertFalse(input.isEmpty()))
))
)));
}
@TestFactory
DynamicNode dynamicNodeSingleTest() {
return dynamicTest("'pop' is a palindrome", () -> assertTrue(isPalindrome("pop")));
}
@TestFactory
DynamicNode dynamicNodeSingleContainer() {
return dynamicContainer("palindromes",
Stream.of("racecar", "radar", "mom", "dad")
.map(text -> dynamicTest(text, () -> assertTrue(isPalindrome(text)))
));
}
}
dynamicTestsWithInvalidReturnType()
와 같이 유효하지 않은 타입을 리턴하는 경우, 이를 컴파일 시간에 알 수 없기 때문에 런타임 예외인 JUnitException
이 발생한다.TestSource
를 제공한다.TestSource
는 각각 DynamicTest.dynamicTest(String, URI, Executable)
나 DynamicContainer.dynamicContainer(String, URI, Stream)
팩토리 메서드로 만들 수 있다.URI
는 다음 TestSource
구현 중 하나로 변환된다:ClasspathResourceSource
: URI
가 classpath
스킴을 포함한 경우 (ex. classpath:/test/foo.xml?line=20,column=2
)DirectorySource
: URI
가 파일 시스템에 존재하는 디렉토리를 나타내는 경우FileSource
: URI
가 파일 시스템에 존재하는 파일을 나타내는 경우MethodSource
: URI
가 method
스킴과 FQMN(Fully Qualified Method Name)을 포함하는 경우method:org.junit.Foo#bar(java.lang.String, java.lang.String[])
UriSource
: 위 네 개의 TestSource
구현을 적용할 수 없는 경우@Timeout
애노테이션은 선언된 테스트, 테스트 팩토리, 테스트 템플릿, 또는 라이프사이클 메서드의 실행 시간이 주어진 시간을 초과하면 실패하도록 한다.class TimeoutDemo {
@BeforeEach
@Timeout(5)
void setUp() {
// fails if execution time exceeds 5 seconds
}
@Test
@Timeout(value = 100, unit = TimeUnit.MILLISECONDS)
void failsIfExecutionTimeExceeds100Milliseconds() {
// fails if execution time exceeds 100 milliseconds
}
}
assertTimeoutPreemptively()
단언과 달리, @Timeout
메서드의 실행은 테스트의 메인 스레드에서 실행된다. 만일 타임아웃이 초과되면, 메인 스레드는 다른 스레드로부터 인터럽트된다. 따라서 ThreadLocal
트랜잭션 관리 등과 함께 상호 운용이 가능하다.@Nested
클래스에 동일한 타임아웃을 설정하기 위해 클래스 레벨에 @Timeout
애노테이션을 선언할 수도 있다.@Timeout
이 선언되어 있더라도, 각 테스트 메서드 또는 @Nested
클래스에서 타임아웃을 오버라이드 할 수 있다.@Timeout
이 선언되어 있더라도, 라이프사이클 메서드에는 타임아웃이 적용되지 않는다.@Timeout
을 @TestFactory
메서드에 선언하면 해당 팩토리 메서드가 주어진 시간 안에 리턴되는지만 검증하고, 리턴된 각 개별 동적 테스트의 실행시간은 검증하지 않는다.assertTimeout()
또는 assertTimeoutPreemptively()
를 사용해야 한다.@Timeout
이 @TestTemplate
메서드(ex. @RepeatedTest
, @ParameterizedTest
)에 선언되어 있다면, 각 호출에 해당 타임아웃이 적용된다.<number> [ns|μs|ms|s|m|h|d]
포맷을 가져야 한다.설정 파라미터 | 대상 |
---|---|
junit.jupiter.execution.timeout.default | 모든 테스트 가능한 요소들 (라이프사이클 메서드 포함) |
junit.jupiter.execution.timeout.testable.method.default | 모든 테스트 가능한 요소들 |
junit.jupiter.execution.timeout.test.method.default | 모든 @Test 메서드 |
junit.jupiter.execution.timeout.testtemplate.method.default | 모든 @TestTemplate 메서드 |
junit.jupiter.execution.timeout.testfactory.method.default | 모든 @TestFactory 메서드 |
junit.jupiter.execution.timeout.lifecycle.method.default | 모든 라이프사이클 메서드 |
junit.jupiter.execution.timeout.beforeall.method.default | 모든 @BeforeAll 메서드 |
junit.jupiter.execution.timeout.beforeeach.method.default | 모든 @BeforeEach 메서드 |
junit.jupiter.execution.timeout.aftereach.method.default | 모든 @AfterEach 메서드 |
junit.jupiter.execution.timeout.afterall.method.default | 모든 @AfterAll 메서드 |
설정 파라미터 값 | 동등한 애노테이션 |
---|---|
42 | @Timeout(42) |
42 ns | @Timeout(value = 42, unit = NANOSECONDS) |
42 μs | @Timeout(value = 42, unit = MICROSECONDS) |
42 ms | @Timeout(value = 42, unit = MILLISECONDS) |
42 s | @Timeout(value = 42, unit = SECONDS) |
42 m | @Timeout(value = 42, unit = MINUTES) |
42 h | @Timeout(value = 42, unit = HOURS) |
42 d | @Timeout(value = 42, unit = DAYS) |
@Timeout
사용하기(Using @Timeout
for Polling Tests)CountDownLatch
혹은 다른 동기화 메커니즘을 사용할 수도 있지만, 불가능한 경우도 있다.@Test
@Timeout(5) // Poll at most 5 seconds
void pollUntil() throws InterruptedException {
while (asynchronousResultNotAvailable()) {
Thread.sleep(250); // custom poll interval
}
// Obtain the asynchronous result and perform assertions
}
@Timeout
비활성화하기(Disable @Timeout
Globally)junit.jupiter.execution.timeout.mode
설정 파라미터를 변경하면 타임아웃 적용 여부를 설정할 수 있다.enabled
, disabled
, disabled_on_debug
가 존재한다.병렬 실행은 실험적(experimental)인 기능이다.
기본적으로 JUnit 테스트는 싱글 스레드에서 순차적으로 실행된다.
원한다면 테스트 실행 속도를 높이기 위해서 병렬 실행을 할 수도 있다.
병렬 실행을 활성화하기 위해서는 junit.jupiter.execution.parallel.enabled
를 true
로 설정하면 된다.
테스트 트리의 노드가 동시에 실행될 것인지 아닌지는 실행 모드에 의해 결정된다.
SAME_THREAD
실행 모드@BeforeAll
또는 @AfterAll
과 동일한 스레드에서 실행된다.CONCURRENT
실행 모드기본적으로 테스트 트리의 노드는 SAME_THREAD
실행 모드를 사용한다.
junit.jupiter.execution.parallel.mode.default
설정 프로퍼티를 이용해 기본 실행 모드를 설정할 수 있다.@Execution
애노테이션으로 선언한 요소와 그 하위 요소의 실행 모드를 지정할 수 있다.기본 실행 모드는 몇 가지 예외적인 경우를 제외하면 테스트 트리의 모든 노드에 적용된다.
Lifecycle.PER_CLASS
모드를 사용하는 경우, 테스트 작성자는 해당 테스트 클래스가 스레드-안전(thread-safe)한 지 확인해야 한다.MethodOrderer
를 사용하는 경우, 동시 실행은 설정한 실행 순서와 충돌할 수 있다.@Execution(CONCURRENT)
애노테이션이 존재하는 경우에만 동시에 실행된다.CONCURRENT
실행 모드로 설정된 테스트 트리의 모든 노드는 선언적 동기화 메커니즘(declarative synchronization mechanism)을 관찰하면서 제공된 구성에 따라 완전히 병렬로 실행된다.
최상위 클래스의 실행 모드의 기본 값은 junit.jupiter.execution.parallel.mode.classes.default
설정 프로퍼티로 설정할 수 있다.
junit.jupiter.execution.parallel.mode.classes.default
설정 값이 명시적으로 설정되지 않은 경우, junit.jupiter.execution.parallel.mode.default
설정 값이 대신 사용된다.junit.jupiter.execution.parallel.enabled=true
junit.jupiter.execution.parallel.mode.default=same_thread
junit.jupiter.execution.parallel.mode.classes.default=concurrent
junit.jupiter.execution.parallel.enabled=true
junit.jupiter.execution.parallel.mode.default=concurrent
junit.jupiter.execution.parallel.mode.classes.default=same_thread
A
와 B
가 각각 두 개의 메서드 test1()
과 test2()
를 가지고 있다고 가정하자.junit.jupiter.execution.parallel.mode.default
설정 값과 junit.jupiter.execution.parallel.mode.classes.default
설정 값의 조합에 따라 4가지 경우가 가능하다.원하는 병렬성(parallelism)의 정도와 최대 풀 사이즈(maximum pool size)와 같은 프로퍼티는 ParallelExecutionConfigurationStrategy
를 통해 설정할 수 있다.
전략을 선택하려면 junit.jupiter.execution.parallel.config.strategy
프로퍼티를 설정하면 된다.
JUnit 플랫폼은 dynamic
과 fixed
두 가지 전략을 기본적으로 제공하며, 원한다면 custom
전략을 구현할 수도 있다.
dynamic
사용할 수 있는 프로세서/코어 수 × ${junit.jupiter.execution.parallel.config.dynamic.factor}
를 계산하여 병렬성의 정도를 결정한다.junit.jupiter.execution.parallel.config.dynamic.factor
의 기본값은 1이다.fixed
junit.jupiter.execution.parallel.config.fixed.parallelism
로 설정된 값을 사용한다.junit.jupiter.execution.parallel.config.fixed.parallelism
는 필수 값이다.custom
ParallelExecutionConfigurationStrategy
를 구현하고, junit.jupiter.execution.parallel.config.custom.class
로 구현된 클래스를 명시하여 원하는 구성을 사용한다.junit.jupiter.execution.parallel.config.strategy
의 기본 값은 dynamic
이다.
병렬성은 동시 스레드의 최대 개수를 의미하지 않는다.
ForkJoinPool
동기화 메커니즘을 사용한다면, 충분한 병렬성을 제공하기 위해 추가적인 스레드가 생성될 수 있다.@Execution
애노테이션을 통한 실행 모드 지정 외에도 또 다른 애노테이션 기반 선언적 동기화 메커니즘을 제공한다.@ResourceLock
애노테이션은 테스트 클래스 또는 메서드에 사용할 수 있다.@ResourceLock
은 특정한 공유 리소스에 대한 동기화된 접근을 보장해야 할 때 사용한다.String
타입의 고유한 이름으로 식별된다.Resources
에 정의되어 있는 SYSTEM_PROPERTIES
, SYSTEM_OUT
, SYSTEM_ERR
, LOCALE
, TIME_ZONE
일 수도 있고, 사용자가 직접 정의한 이름일 수도 있다.@ResourceLock
애노테이션으로 선언되어 있다면 JUnit은 이 정보를 사용하여 테스트 병렬 실행시 충돌이 일어나지 않도록 보증한다.@Isolated
애노테이션을 사용할 수 있다.@Isolated
로 선언된 테스트 클래스 내의 모든 테스트 메서드는 다른 메서드와 동시에 실행되지 않으며, 순차적으로 실행된다.READ
접근을 하는 두 테스트는 동시에 실행될 수 있지만, READ_WRITE
접근을 하는 테스트는 동시에 실행될 수 없다.@Execution(CONCURRENT)
class SharedResourcesDemo {
private Properties backup;
@BeforeEach
void backup() {
backup = new Properties();
backup.putAll(System.getProperties());
}
@AfterEach
void restore() {
System.setProperties(backup);
}
@Test
@ResourceLock(value = SYSTEM_PROPERTIES, mode = READ)
void customPropertyIsNotSetByDefault() {
assertNull(System.getProperty("my.prop"));
}
@Test
@ResourceLock(value = SYSTEM_PROPERTIES, mode = READ_WRITE)
void canSetCustomPropertyToApple() {
System.setProperty("my.prop", "apple");
assertEquals("apple", System.getProperty("my.prop"));
}
@Test
@ResourceLock(value = SYSTEM_PROPERTIES, mode = READ_WRITE)
void canSetCustomPropertyToBanana() {
System.setProperty("my.prop", "banana");
assertEquals("banana", System.getProperty("my.prop"));
}
}
@TempDir
은 실험적(experimental)인 기능이다.
빌트인 TempDirectory
익스텐션은 개별 테스트 또는 테스트 클래스의 모든 테스트를 위한 임시 디렉토리를 생성하고 정리하는데 사용된다.
TempDirectory
익스텐션은 기본적으로 등록되어 있다.
임시 디렉토리 익스텐션을 사용하려면 테스트 클래스의 private
이 아닌 필드 또는 라이프사이클 메서드와 테스트 메서드 파라미터에 @TempDir
애노테이션을 선언한다.
임시 디렉토리 익스텐션은 java.nio.file.Path
와 java.io.File
타입을 지원한다.
@Test
void writeItemsToFile(@TempDir Path tempDir) throws IOException {
Path file = tempDir.resolve("test.txt");
new ListWriter(file).write("a", "b", "c");
assertEquals(singletonList("a,b,c"), Files.readAllLines(file));
}
@TempDir
애노테이션은 생성자 파라미터를 지원하지 않으므로, 필드 주입을 사용해야 한다.static
필드에 저장한다. 따라서 해당 테스트 클래스의 모든 라이프사이클 메서드와 테스트 메서드에서 동일한 공유 디렉토리에 접근이 가능하다.class SharedTempDirectoryDemo {
@TempDir
static Path sharedTempDir;
@Test
void writeItemsToFile() throws IOException {
Path file = sharedTempDir.resolve("test.txt");
new ListWriter(file).write("a", "b", "c");
assertEquals(singletonList("a,b,c"), Files.readAllLines(file));
}
@Test
void anotherTestThatUsesTheSameTempDir() {
// use sharedTempDir
}
}
Launcher
API에 요청을 제공하기 위한 LauncherDiscoveryRequestBuilder
의 configurationParameter()
and configurationParameters()
메서드--config
커맨드 라인 옵션을 사용한다.systemProperty
또는 systemProperties
DSL을 사용한다.configurationParameters
프로퍼티를 사용한다.junit-platform.properties
파일*
: 모든 클래스org.junit.*
: org.junit
기반 패키지 하위의 모든 클래스와 하위 패키지*.MyCustomImpl
: 클래스 이름이 정확히 MyCustomImpl
인 클래스*System*
: FQCN에 System
을 포함한 클래스*System*+, +*Unit*
: FQCN에 System
또는 Unit
을 포함한 클래스org.example.MyCustomImpl
: FQCN이 정확히 org.example.MyCustomImpl
인 클래스org.example.MyCustomImpl, org.example.TheirCustomImpl
: FQCN이 정확히 org.example.MyCustomImpl
또는 org.example.TheirCustomImpl
인 클래스!
, &
, |
연산자와 연산자 우선순위를 조정하기 위한 괄호((
, )
)로 이루어진 부울 표현식이다.any()
와 none()
특수 표현도 조합 가능하다.연산자 | 의미 | 결합 방향 |
---|---|---|
! | not | 오른쪽 |
& | and | 왼쪽 |
` | ` | or |
product
, catalog
, shipping
micro
, integration
, end-to-end
태그 표현식 | 선택되는 대상 |
---|---|
product | product 테스트 |
`catalog | shipping` |
catalog & shipping | catalog 테스트이면서 shippping 테스트 |
product & !end-to-end | product 테스트 중 end-to-end 테스트가 아닌 테스트 |
`(micro | integration) & (product |
System.out
과 System.err
로 출력된 결과를 캡쳐할 수 있는 기능을 지원한다.junit.platform.output.capture.stdout
또는 junit.platform.output.capture.stderr
를 true
로 설정하면 된다.junit.platform.output.capture.maxBuffer
를 설정하여 테스트 실행마다 사용할 최대 버퍼 사이즈를 설정할 수 있다.Runner
, TestRule
, MethodRule
를 대체하는 단 하나의 일관된 개념Extension
그 자체는 단순히 마커 인터페이스다.@ExtendWith
애노테이션을 통해 선언적(declaratively)으로 등록한다.@RegisterExtension
애노테이션을 통해 프로그램적(programmatically)으로 등록한다.ServiceLoader
메커니즘을 통해 자동적(automatically)으로 등록한다.@ExtendWith
애노테이션을 이용해서 하나 이상의 익스텐션을 선언적으로 등록할 수 있다.@ExtendWith
애노테이션은 테스트 인터페이스, 테스트 클래스, 테스트 메서드 또는 조합 애노테이션에 선언 가능하다.@ExtendWith
애노테이션에는 등록하고자 하는 클래스 참조를 제공해야 한다.@ExtendWith
애노테이션으로 등록한 익스텐션은 소스 코드에 명시된 순서대로 등록된다.@ExtendWith(RandomParametersExtension.class)
@Test
void test(@Random int i) {
}
@ExtendWith(RandomParametersExtension.class)
class MyTests {
}
@ExtendWith({ DatabaseExtension.class, WebServerExtension.class })
class MyFirstTests {
}
@ExtendWith(DatabaseExtension.class)
@ExtendWith(WebServerExtension.class)
class MySecondTests {
}
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@ExtendWith({ DatabaseExtension.class, WebServerExtension.class })
public @interface DatabaseAndWebServerExtension {
}
테스트 틀래스의 필드를 @RegisterExtension
애노테이션으로 선언하여 프로그램적으로 익스텐션을 등록할 수 있다.
@ExtendWith
으로 선언적으로 익스텐션을 등록하면 애노테이션을 통해서만 설정이 가능하지만, @RegisterExtension
을 이용하면 프로그램적으로 익스텐션을 설정할 수 있다.
@RegisterExtension
을 통해 프로그램적으로 등록된 익스텐션은 어떤 알고리즘을 통해 결정적으로, 하지만 자명하지 않은 순서로 등록된다.
@Order
를 사용해야 한다.@Order
가 지정되지 않은 @RegisterExtension
은 기본적으로 Integer.MAX_VALUE / 2
의 기본 순서값을 가진다.@RegisterExtension
을 선언한 필드는 private
이거나 평가 시점에 null
이어서는 안된다.
static
여부는 상관 없다.정적 필드(Static Fields)
@RegisterExtension
필드가 static
이라면, @ExtendWith
로 명시된 클래스 레벨 익스텐션들이 먼저 등록된 이후에 등록된다.BeforeAllCallback
, AfterAllCallback
, TestInstancePostProcessor
, 또는 TestInstancePreDestroyCallback
과 같은 인스턴스 레벨 익스텐션 API를 구현할 수도 있고, BeforeEachCallback
과 같은 메서드 레벨 익스텐션을 구현할 수도 있다.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());
}
}
WebServerDemo
클래스의 server
필드는 WebServerExtension
가 지원하는 빌더 패턴으로 초기화된다.WebServerExtension
은 클래스 레벨 익스텐션으로 자동적으로 등록된다.@BeforeAll
, @AfterAll
, @BeforeEach
, @AfterEach
, @Teset
메서드 안에서 server
필드를 통해서 익스텐션의 인스턴스에 접근할 수 있다.인스턴스 필드(Instance Fields)
@RegisterExtension
필드가 정적이 아니라면(non-static), 익스텐션은 테스트 클래스가 초기화되고, 등록된 각 TestInstancePostProcessor
가 테스트 인스턴스를 후처리(post-process)한 이후에 등록된다.BeforeAllCallback
, AfterAllCallback
, TestInstancePostProcessor
와 같은 인스턴스 레벨 API를 구현하고 있다면, 해당 API는 인정되지 않는다.@ExtendWith
등록 이후에 등록된다.@TestInstance(Lifecycle.PER_CLASS)
로 선언되어 있다면 인스턴스 익스텐션은 메서드 레벨 @ExtendWith
등록 이전에 등록된다.class DocumentationDemo {
static Path lookUpDocsDir() {
// return path to docs dir
}
@RegisterExtension
DocumentationExtension docs = DocumentationExtension.forPath(lookUpDocsDir());
@Test
void generateDocumentation() {
// use this.docs ...
}
}
docs
필드는 lookUpDocsDir()
메서드가 호출되고 결과가 DocumentationExtension.forPath()
정적 팩토리 메서드에 제공될 때 초기화된다.DocumentationExtension
는 자동적으로 메서드 레벨 익스텐션으로 등록된다.@BeforeEach
, @AfterEach
, @Teset
메서드 안에서 server
필드를 통해서 익스텐션의 인스턴스에 접근할 수 있다.java.util.ServiceLoader
메커니즘을 통한 전역 익스텐션 등록(global extension registration) 또한 지원한다./META-INF/services
경로 내 org.junit.jupiter.api.extension.Extension
파일에 해당 익스텐션의 FQCN을 제공함으로써 사용자가 원하는 익스텐션을 등록할 수 있다.자동 익스텐션 감지 활성화하기(Enabling Automatic Extension Detection)
junit.jupiter.extensions.autodetection.enabled
설정 파라미터를 true
로 설정하면 된다.Launcher
에 전달되는 LauncherDiscoveryRequest
의 설정 파라미터를 JVM 시스템 프로퍼티로 다음과 같이 제공할 수 있다.-Djunit.jupiter.extensions.autodetection.enabled=true
ServiceLoader
메커니즘에 의해 발견된 익스텐션들은 JUnit 전역 익스텐션(ex. TestInfo
, TestReporter
)이 등록된 이후에 익스텐션 레지스트리에 등록된다.ExecutionCondition
은 프로그램 가능한 조건부 테스트 실행을 위해 Extension
API를 정의한다.ExecutionCondition
은 각 컨테이너(ex. 테스트 클래스)마다 , 각 테스트마다 평가(evaluated)된다.ExtensionContext
에 기반하여 실행되어야 하는지 결정하기 위해서이다.ExtensionContext
익스텐션이 등록되어 있는 경우, 조건 중 하나라도 비활성화되는 즉시 컨테이너 또는 테스트가 비활성화된다.ExtensionContext
의 평가(evaluation)는 부울 OR 연산자의 단락(short-circuiting)처럼 동작한다. (ex. 조건들 중 하나라도 true
로 평가되면 뒤의 조건은 평가하지 않듯이)DiabledCondition
과 @Disabled
예시를 참고하라.@Disabled
된 테스트 메서드가 여전히 깨지는지 확인하기 위해 실행해 보고 싶을 수 있다.junit.jupiter.conditions.deactivate
설정 파라미터로 현재 테스트 실행에서 어떤 조건들을 비활성화 할 것인지(평가하지 않을 것인지) 패턴 값으로 명시할 수 있다.Launcher
에 전달되는 LauncherDiscoveryRequest
의 설정 파라미터를 JVM 시스템 프로퍼티로 제공할 수도 있고, JUnit 플랫폼 설정 파일로 제공할 수도 있다.@Disabled
조건을 비활성화하기 위해서 JVM을 다음의 시스템 프로퍼티와 함께 실행할 수 있다.-Djunit.jupiter.conditions.deactivate=org.junit.*DisabledCondition
TestInstanceFactory
는 테스트 클래스 인스턴스 생성을 위한 API를 정의한다.
일반적인 유스케이스:
TestInstanceFactory
가 등록되어 있지 않다면, 프레임워크는 테스트 클래스를 인스턴스화하기 위해 유일한(sole) 생성자를 호출한다.
ParameterResolver
익스텐션에 의해 결정될 수도 있다.TestInstanceFactory
를 구현한 익스텐션은 테스트 인터페이스, 최상위 테스트 클래스, 또는 @Nested
테스트 클래스에서 등록될 수 있다.
한 클래스에 대해 TestInstanceFactory
를 구현한 익스텐션을 여러 개 등록하면 예외가 발생한다.
TestInstanceFactory
는 상속된다.TestInstanceFactory
가 등록되도록 해야 한다.TestInstancePostProcessor
는 테스트 인스턴스 후처리를 위한 API를 정의한다.MockitoExtension
과 SpringExtension
의 소스 코드를 참고하라.TestInstancePreDestroyCallback
는 테스트 인스턴스가 테스트에 사용된 후 소멸되기 전에 특정 처리를 하기 위한 API를 정의한다.ParameterResolver
는 동적으로 런타임에 파라미터를 결정(resolve)하기 위한 API를 정의한다.ParameterResolver
에 의해 런타임에 반드시 결정되어야 한다.ParameterResolver
는 기본 제공되는 빌트인 ParameterResolver
일 수도 있고 사용자에 의해 등록된 ParameterResolver
일 수도 있다.TypeBasedParameterResolver
를 구현하여 사용하면 편리하다. CustomTypeParameterResolver
, CustomAnnotationParameterResolver
, MapOfListsTypeBasedParameterResolver
의 소스 코드를 참고하라.javac
가 생성하는 바이트 코드의 버그로 인해 java.lang.reflect.Parameter
API로 직접 내부 클래스 생성자(ex. @Nested
테스트 클래스의 생성자) 파라미터의 애노테이션을 찾으면 실패한다.ParameterResolver
구현에 제공되는 ParameterContext
API는 파라미터 애노테이션을 올바르게 찾기 위한 편리한 메서드를 제공한다.boolean isAnnotated(Class<? extends Annotation> annotationType)
Optional<A> findAnnotation(Class<A> annotationType)
List<A> findRepeatableAnnotations(Class<A> annotationType)
java.lang.reflect.Parameter
API 대신 위 메서드를 사용해야 한다.TestWatcher
는 테스트 메서드 실행의 결과를 처리하기 위한 API를 정의한다.TestWatcher
는 다음과 같은 이벤트에 대한 컨텍스트 정보와 함께 호출된다.testDisabled
: 비활성화된 테스트 메서드가 스킵된 뒤 호출된다.testSuccessful
: 테스트 메서드가 성공적으로 완료된 뒤 호출된다.testAborted
: 테스트 메서드가 취소(aborted)된 뒤 호출된다.testFailed
: 테스트 메서드가 실패한 뒤 호출된다.@Test
메서드뿐만 아니라 @TestTemplate
메서드(ex. @RepeatedTest
, @ParameterizedTest
)도 해당된다.@Nested
클래스들을 포함해서 해당 클래스가 포함한 모든 테스트 메서드에 대해 호출된다.ExtensionContext
의 Store
에 저장되어 있는 모든 ExtensionContext.Store.CloseableResource
인스턴스는 이 메서드가 호출되기 전에 종료(closed)된다.Store
를 사용할 수 있다.BeforeAllCallback
BeforeEachCallback
BeforeTestExecutionCallback
AfterTestExecutionCallback
AfterEachCallback
AfterAllCallback
org.junit.jupiter.api.extension
패키지의 JavaDoc을 참조하라.SpringExtension
의 소스 코드를 참고하라.BeforeTestExecutionCallback
과 AfterTestExecutionCallback
는 각각 테스트 메서드 실행 직전과 직후에 동작(behavior)을 추가하기 위한 API를 정의한다.@BeforeEach
또는 @AfterEach
메서드 전후에 콜백을 구현해야 한다면 BeforeEachCallback
과 AfterEachCallback
을 사용하면 된다.public class TimingExtension implements BeforeTestExecutionCallback, AfterTestExecutionCallback {
private static final Logger logger = Logger.getLogger(TimingExtension.class.getName());
private static final String START_TIME = "start time";
@Override
public void beforeTestExecution(ExtensionContext context) throws Exception {
getStore(context).put(START_TIME, System.currentTimeMillis());
}
@Override
public void afterTestExecution(ExtensionContext context) throws Exception {
Method testMethod = context.getRequiredTestMethod();
long startTime = getStore(context).remove(START_TIME, long.class);
long duration = System.currentTimeMillis() - startTime;
logger.info(() ->
String.format("Method [%s] took %s ms.", testMethod.getName(), duration));
}
private Store getStore(ExtensionContext context) {
return context.getStore(Namespace.create(getClass(), context.getRequiredTestMethod()));
}
}
@ExtendWith(TimingExtension.class)
로 등록하면 실행 시간이 출력된다.@ExtendWith(TimingExtension.class)
class TimingExtensionTests {
@Test
void sleep20ms() throws Exception {
Thread.sleep(20);
}
@Test
void sleep50ms() throws Exception {
Thread.sleep(50);
}
}
INFO: Method [sleep20ms] took 24 ms.
INFO: Method [sleep50ms] took 53 ms.
@Test
메서드 안에서 발생한 예외는 TestExecutionExceptionHandler
를 통해 처리된다.@BeforeAll
, @BeforeEach
, @AfterEach
, @AfterAll
) 안에서 발생한 예외는 LifecycleMethodExecutionExceptionHandler
를 통해 처리된다.IOException
인스턴스를 다시 던지지 않고 삼켜버린다(swallow).public class IgnoreIOExceptionExtension implements TestExecutionExceptionHandler {
@Override
public void handleTestExecutionException(ExtensionContext context, Throwable throwable)
throws Throwable {
if (throwable instanceof IOException) {
return;
}
throw throwable;
}
}
@BeforeAll
, @BeforeEach
, @AfterEach
, @AfterAll
등이 실패한 직후에 실행될 것을 보장한다.class RecordStateOnErrorExtension implements LifecycleMethodExecutionExceptionHandler {
@Override
public void handleBeforeAllMethodExecutionException(ExtensionContext context, Throwable ex)
throws Throwable {
memoryDumpForFurtherInvestigation("Failure recorded during class setup");
throw ex;
}
@Override
public void handleBeforeEachMethodExecutionException(ExtensionContext context, Throwable ex)
throws Throwable {
memoryDumpForFurtherInvestigation("Failure recorded during test setup");
throw ex;
}
@Override
public void handleAfterEachMethodExecutionException(ExtensionContext context, Throwable ex)
throws Throwable {
memoryDumpForFurtherInvestigation("Failure recorded during test cleanup");
throw ex;
}
@Override
public void handleAfterAllMethodExecutionException(ExtensionContext context, Throwable ex)
throws Throwable {
memoryDumpForFurtherInvestigation("Failure recorded during class cleanup");
throw ex;
}
}
@BeforeAll
와 @AfterAll
실행 중 발생한 예외를 처리하고자 LifecycleMethodExecutionExceptionHandler
를 구현한 익스텐션은 클래스 레벨에 등록되어야 한다.@BeforeEach
와@AfterEach
를 위한 처리기는 개별 테스트 메서드에 등록될 수도 있다.// Register handlers for @Test, @BeforeEach, @AfterEach as well as @BeforeAll and @AfterAll
@ExtendWith(ThirdExecutedHandler.class)
class MultipleHandlersTestCase {
// Register handlers for @Test, @BeforeEach, @AfterEach only
@ExtendWith(SecondExecutedHandler.class)
@ExtendWith(FirstExecutedHandler.class)
@Test
void testMethod() {
}
}
InvocationInterceptor
는 테스트 코드에 대한 호출을 가로채기 위한 API를 정의한다.public class SwingEdtInterceptor implements InvocationInterceptor {
@Override
public void interceptTestMethod(Invocation<Void> invocation,
ReflectiveInvocationContext<Method> invocationContext,
ExtensionContext extensionContext) throws Throwable {
AtomicReference<Throwable> throwable = new AtomicReference<>();
SwingUtilities.invokeAndWait(() -> {
try {
invocation.proceed();
}
catch (Throwable t) {
throwable.set(t);
}
});
Throwable t = throwable.get();
if (t != null) {
throw t;
}
}
}
@TestTemplate
메서드는 최소한 하나의 TestTemplateInvocationContextProvider
가 등록된 경우에만 실행될 수 있다.TestTemplateInvocationContext
인스턴스의 Stream
을 제공할 책임이 있다.@TestTemplate
메서드의 다음 호출에서 사용될 추가적인 익스텐션의 목록을 명시할 수 있다.TestTemplateInvocationContextProvider
를 구현하고 등록해서 테스트 템플릿에 사용하는 방법을 보여준다.final List<String> fruits = Arrays.asList("apple", "banana", "lemon");
@TestTemplate
@ExtendWith(MyTestTemplateInvocationContextProvider.class)
void testTemplate(String fruit) {
assertTrue(fruits.contains(fruit));
}
public class MyTestTemplateInvocationContextProvider
implements TestTemplateInvocationContextProvider {
@Override
public boolean supportsTestTemplate(ExtensionContext context) {
return true;
}
@Override
public Stream<TestTemplateInvocationContext> provideTestTemplateInvocationContexts(
ExtensionContext context) {
return Stream.of(invocationContext("apple"), invocationContext("banana"));
}
private TestTemplateInvocationContext invocationContext(String parameter) {
return new TestTemplateInvocationContext() {
@Override
public String getDisplayName(int invocationIndex) {
return parameter;
}
@Override
public List<Extension> getAdditionalExtensions() {
return Collections.singletonList(new ParameterResolver() {
@Override
public boolean supportsParameter(ParameterContext parameterContext,
ExtensionContext extensionContext) {
return parameterContext.getParameter().getType().equals(String.class);
}
@Override
public Object resolveParameter(ParameterContext parameterContext,
ExtensionContext extensionContext) {
return parameter;
}
});
}
};
}
}
└─ testTemplate(String) ✔
├─ apple ✔
└─ banana ✔
위 예시에서 테스트 템플릿은 두 번 호출되며, 호출의 표시 이름은 호출 컨텍스트에 명시된 대로 apple
과 banana
이다.
각 호출은 메서드 파라미터를 결정하는데 사용할 사용자 지정 ParameterResolver
를 등록한다.
TestTemplateInvocationContextProvider
익스텐션 API는 다른 컨텍스트에서 테스트와 유사한(test-like) 메서드의 반복적인 호출에 의존하는 다양한 종류의 테스트를 구현하기 위한 것이다.
ExtensionContext
API는 익스텐션의 상태를 유지하기 위해서 Store
를 제공한다.TimingExtension
은 메서드 레벨 스코프로 Store
를 사용하는 예시다.ExtensionContext
에 저장된 값들은 해당 ExtensionContext
를 감싸고 있는 다른 ExtensionContext
에서는 사용될 수 없다.ExtensionContext
는 중첩될 수 있으므로, 안쪽 컨텍스트의 스코프는 제한된다.Store
에 값을 저장하고 가져오는 메서드는 Javadoc을 참조하라.CloseableResource
는 스토어에 추가된 순서의 역순으로 close()
메서드가 호출된다.junit-platform-commons
아티팩트는 org.junit.platform.commons.support
패키지를 노출한다.org.junit.platform.commons.support
패키지는 애노테이션, 클래스, 리플렉션, 클래스 경로 스캔 등을 위한 유틸리티 메서드를 제공한다.TestEngine
또는 Extension
을 개발할 때 이러한 메서드를 사용하는 것이 권장된다.AnnotationSupport
는 애노테이션을 가진 요소(ex. 패키지, 애노테이션, 클래스, 인터페이스, 생성자, 메서드, 필드 등)에 사용할 수 있는 정적 유틸리티 메서드들을 제공한다.AnnotationSupport
의 Javadoc을 참조하라.ClassSupport
는 클래스(java.lang.Class
의 인스턴스)에 사용할 수 있는 정적 유틸리티 메서드를 제공한다.ClassSupport
의 Javadoc을 참조하라.ReflectionSupport
는 JDK 표준 리플렉션과 클래스 로딩 메커니즘과 관련된 정적 유틸리티 메서드를 제공한다.ReflectionSupport
의 Javadoc을 참조하라.ModifierSupport
는 클래스 또는 멤버의 접근제어자와 관련된 정적 유틸리티 메서드를 제공한다.public
, private
, abstract
, static
로 선언되어 있는지 확인한다.ModifierSupport
의 Javadoc을 참조하라.단계 | 타입 | 구분 | FQCN 및 설명 |
---|---|---|---|
1 | 인터페이스 | 익스텐션 | org.junit.jupiter.api.extension.BeforeAllCallback 컨테이너의 모든 테스트가 실행되기 전에 실행되는 코드 |
2 | 애노테이션 | 사용자 | org.junit.jupiter.api.BeforeAll 컨테이너의 모든 테스트가 실행되기 전에 실행되는 코드 |
3 | 인터페이스 | 익스텐션 | org.junit.jupiter.api.extension.LifecycleMethodExecutionExceptionHandler #handleBeforeAllMethodExecutionException @BeforeAll 메서드에서 발생한 예외를 처리하기 위한 코드 |
4 | 인터페이스 | 익스텐션 | org.junit.jupiter.api.extension.BeforeEachCallback 각 테스트가 실행되기 전에 실행되는 코드 |
5 | 애노테이션 | 사용자 | org.junit.jupiter.api.BeforeEach 각 테스트가 실행되기 전에 실행되는 코드 |
6 | 인터페이스 | 익스텐션 | org.junit.jupiter.api.extension.LifecycleMethodExecutionExceptionHandler #handleBeforeEachMethodExecutionException @BeforeEach 메서드에서 발생한 예외를 처리하기 위한 코드 |
7 | 인터페이스 | 익스텐션 | org.junit.jupiter.api.extension.BeforeTestExecutionCallback 테스트 실행 직전에 실행되는 코드 |
8 | 애노테이션 | 사용자 | org.junit.jupiter.api.Test 실제 테스트 메서드의 코드 |
9 | 인터페이스 | 익스텐션 | org.junit.jupiter.api.extension.TestExecutionExceptionHandler 테스트 중에 발생한 예외를 처리하기 위한 코드 |
10 | 인터페이스 | 익스텐션 | org.junit.jupiter.api.extension.AfterTestExecutionCallback 테스트 실행과 대응하는 예외 처리기 직후에 실행되는 코드 |
11 | 애노테이션 | 사용자 | org.junit.jupiter.api.AfterEach 각 테스트가 실행된 이후에 실행되는 코드 |
12 | 인터페이스 | 익스텐션 | org.junit.jupiter.api.extension.LifecycleMethodExecutionExceptionHandler #handleAfterEachMethodExecutionException @AfterEach 메서드에서 발생한 예외를 처리하기 위한 코드 |
13 | 인터페이스 | 익스텐션 | org.junit.jupiter.api.extension.AfterEachCallback 각 테스트가 실행된 이후 실행되는 코드 |
14 | 애노테이션 | 사용자 | org.junit.jupiter.api.AfterAll 컨테이너의 모든 테스트가 실행된 후 실행되는 코드 |
15 | 인터페이스 | 익스텐션 | org.junit.jupiter.api.extension.LifecycleMethodExecutionExceptionHandler #handleAfterAllMethodExecutionException @AfterAll 메서드에서 발생한 예외를 처리하는 코드 |
16 | 인터페이스 | 익스텐션 | org.junit.jupiter.api.extension.AfterAllCallback 컨테이너의 모든 테스트가 실행된 후 실행되는 코드 |
InvocationInterceptor
를 구현하면 추가적으로 사용자 코드 메서드의 모든 호출을 가로챌 수도 있다.BeforeAllCallback
, AfterAllCallback
, BeforeEachCallback
, AfterEachCallback
, BeforeTestExecutionCallback
, AfterTestExecutionCallback
)을 구현한 익스텐션이 여러개 등록된 경우, 항상 래핑 동작(wrapping behavior)을 보장한다.Extension1
이 등록된 후 Extension2
가 등록되었다고 가정하자.Extension1
의 모든 사전(before) 콜백은 Extension2
의 모든 사전 콜백 이전에 실행된다.Extension1
의 모든 사후(after) 콜백은 Extension2
의 모든 사후 콜백 이후에 실행된다.Extenstion1
이 Extension2
를 래핑한다.@BeforeAll
, @BeforeEach
, @AfterEach
, @AfterAll
메서드는 부모 클래스 또는 인터페이스로부터 상속된다.@BeforeAll
메서드는 자식(구현) 클래스의 @BeforeAll
메서드 이전에 실행된다.@BeforeEach
메서드는 자식(구현) 클래스의 @BeforeEach
메서드 이전에 실행된다.@AfterEach
메서드는 자식(구현) 클래스의 @AfterEach
메서드 이후에 실행된다.@AfterAll
메서드는 자식(구현) 클래스의 @AfterAll
메서드 이후에 실행된다.public class Extension1 implements BeforeEachCallback, AfterEachCallback {
@Override
public void beforeEach(ExtensionContext context) {
beforeEachCallback(this);
}
@Override
public void afterEach(ExtensionContext context) {
afterEachCallback(this);
}
}
public class Extension2 implements BeforeEachCallback, AfterEachCallback {
@Override
public void beforeEach(ExtensionContext context) {
beforeEachCallback(this);
}
@Override
public void afterEach(ExtensionContext context) {
afterEachCallback(this);
}
}
abstract class AbstractDatabaseTests {
@BeforeAll
static void createDatabase() {
beforeAllMethod(AbstractDatabaseTests.class.getSimpleName() + ".createDatabase()");
}
@BeforeEach
void connectToDatabase() {
beforeEachMethod(AbstractDatabaseTests.class.getSimpleName() + ".connectToDatabase()");
}
@AfterEach
void disconnectFromDatabase() {
afterEachMethod(AbstractDatabaseTests.class.getSimpleName() + ".disconnectFromDatabase()");
}
@AfterAll
static void destroyDatabase() {
afterAllMethod(AbstractDatabaseTests.class.getSimpleName() + ".destroyDatabase()");
}
}
@ExtendWith({ Extension1.class, Extension2.class })
class DatabaseTestsDemo extends AbstractDatabaseTests {
@BeforeAll
static void beforeAll() {
beforeAllMethod(DatabaseTestsDemo.class.getSimpleName() + ".beforeAll()");
}
@BeforeEach
void insertTestDataIntoDatabase() {
beforeEachMethod(getClass().getSimpleName() + ".insertTestDataIntoDatabase()");
}
@Test
void testDatabaseFunctionality() {
testMethod(getClass().getSimpleName() + ".testDatabaseFunctionality()");
}
@AfterEach
void deleteTestDataFromDatabase() {
afterEachMethod(getClass().getSimpleName() + ".deleteTestDataFromDatabase()");
}
@AfterAll
static void afterAll() {
beforeAllMethod(DatabaseTestsDemo.class.getSimpleName() + ".afterAll()");
}
}
DatabaseTestsDemo
테스트 클래스의 실행결과는 다음과 같다:@BeforeAll AbstractDatabaseTests.createDatabase()
@BeforeAll DatabaseTestsDemo.beforeAll()
Extension1.beforeEach()
Extension2.beforeEach()
@BeforeEach AbstractDatabaseTests.connectToDatabase()
@BeforeEach DatabaseTestsDemo.insertTestDataIntoDatabase()
@Test DatabaseTestsDemo.testDatabaseFunctionality()
@AfterEach DatabaseTestsDemo.deleteTestDataFromDatabase()
@AfterEach AbstractDatabaseTests.disconnectFromDatabase()
Extension2.afterEach()
Extension1.afterEach()
@BeforeAll DatabaseTestsDemo.afterAll()
@AfterAll AbstractDatabaseTests.destroyDatabase()
JUnit은 하나의 테스트 클래스 또는 인터페이스에 선언되어있는 라이프사이클 메서드가 여러개일 경우 실행 순서를 보장하지 않는다.
@Test
메서드의 실행 순서가 결정되는 것과 유사하다.JUnit은 하나의 테스트 클래스 또는 인터페이스에 선언되어있는 라이프사이클 메서드가 여러개일 경우 래핑을 지원하지 않는다.
따라서 라이프사이클 메서드간에 의존성이 없다면, 하나의 테스트 클래스 또는 인터페이스에 대해 최대 한 개의 라이프사이클 메서드만 선언하는 것을 추천한다.
다음 예시는 지역적으로 선언된 라이프사이클 메서드의 실행 순서로 인해 라이프사이클 메서드 구성이 깨진(broken) 경우의 동작을 보여준다.
@ExtendWith({ Extension1.class, Extension2.class })
class BrokenLifecycleMethodConfigDemo {
@BeforeEach
void connectToDatabase() {
beforeEachMethod(getClass().getSimpleName() + ".connectToDatabase()");
}
@BeforeEach
void insertTestDataIntoDatabase() {
beforeEachMethod(getClass().getSimpleName() + ".insertTestDataIntoDatabase()");
}
@Test
void testDatabaseFunctionality() {
testMethod(getClass().getSimpleName() + ".testDatabaseFunctionality()");
}
@AfterEach
void deleteTestDataFromDatabase() {
afterEachMethod(getClass().getSimpleName() + ".deleteTestDataFromDatabase()");
}
@AfterEach
void disconnectFromDatabase() {
afterEachMethod(getClass().getSimpleName() + ".disconnectFromDatabase()");
}
}
BrokenLifecycleMethodConfigDemo
테스트 클래스의 실행결과는 다음과 같다:Extension1.beforeEach()
Extension2.beforeEach()
@BeforeEach BrokenLifecycleMethodConfigDemo.insertTestDataIntoDatabase()
@BeforeEach BrokenLifecycleMethodConfigDemo.connectToDatabase()
@Test BrokenLifecycleMethodConfigDemo.testDatabaseFunctionality()
@AfterEach BrokenLifecycleMethodConfigDemo.disconnectFromDatabase()
@AfterEach BrokenLifecycleMethodConfigDemo.deleteTestDataFromDatabase()
Extension2.afterEach()
Extension1.afterEach()
wow... 잘 읽고갑니다