Junit 5 간단 정리 - Kim-Taesu/study GitHub Wiki
-
Test API
로 사용자 테스트 코드를 작성- junit 5 이전 : junit 4.12 (
junit-vintage
) - junit 5 : junit-jupiter-api (
junit-jupiter
)
- junit 5 이전 : junit 4.12 (
- IDE / build tools 에서 테스트 실행을 위해 Launcher(
junit-platform-launcher
)를 통해 execute 메소드를 실행 - TestEngine(
junit-platform-engine
) API를 사용하여 테스트 발견 & 실행한다.- 테스트 발견 :
TestEngine::discover
- 테스트 실행 :
TestEngine::execute
- 테스트 발견 :
- Junit 5는 3개의 sub-project로 구성되어 있다.
-
Junit 5
=Junit Platform
+Junit Jupiter
+Junit Vintage
Junit Platform
- JVM 상에서 테스트 프레임워크를 시작하기 위한 기반 역할
-
Launcher
: Junit 5에서 테스트를discover
/filter
/execute
할 수 있는 모듈-
Launcher API
는junit-platform-launcher
모듈에 존재한다.
-
- 테스트 프레임워크를 개발하기 위한
TestEngine API
를 정의- Junit은 Junit Junpiter programming model을 사용하여 테스트를 발견하고 실행시키는
TestEngine
을 제공 -
TestEngine
의 실체 구현체는 별도 모듈로 존재- Junit 3,4 :
junit-vintage-engine
- Junit 5 :
junit-jupiter-engine
-
org.junit.platform.engine.TestEngine
인터페이스를org.junit.jupiter.engine.JupiterTestEngine
에서 구현한다. -
@Testable
어노테이션으로 테스트를 발견한다.
-
- Junit 3,4 :
- Junit은 Junit Junpiter programming model을 사용하여 테스트를 발견하고 실행시키는
- Junit 4 기반 플랫폼에서 모든 TestEngine을 실행하기위한 Junit 4 기반 Runner 제공
junit-platform-runner
Junit Jupiter
- Junit 5에서 테스트 및 extension을 작성하기 위한 programming model과 extension model의 조합
-
programming model : 테스트를 작성 (
@Test
,@ParameterizedTest
,@AfterAll
, ...)class SimpleTest { @Test @DisplayName("1 + 1 = 2") void addTest() { assertEquals(2, 1 + 1); } }
-
extention model : Junit Jupieter 확장 모델은 Extention API 이라는 개념을 사용한다.
- Extention 자체는 marker interface 역할
class MyCustomExtension implements BeforeEachCallback, AfterEachCallback { @Override publid void beforeEach(ExtensionContext context) { // setup } @Override public void afterEach(ExtensionContext context) { // teardown } }
-
extension을 등록하는 방법
- 선언적 방식 : class나 메소드에
@ExtendWith
사용 - 프로그래밍 방식 :
@RegisterExtension
사용 - 전역적인 방식 : java
ServiceLoader
사용
- 선언적 방식 : class나 메소드에
-
Extension Points
Step Interface/Annotation Description 1 BeforeAllCallBack
모든 테스트가 실행되기 전에 한 번 실행되는 extension 코드 2 @BeforeAll
모든 테스트가 실행되기 전에 한 번 실행되는 사용자 코드 3 handleBeforeAllMethodExecutionException
@BeforeAll
에서 발생되는 exception을 handling할 코드4 BeforeEachCallback
개별 테스트가 실행되기 전에 실행되는 extension 코드 5 @BeforeEach
개별 테스트가 실행되기 전에 실행되는 사용자 코드 6 handleBeforeEachMethodExecutionException
@BeforeEach
에서 발생되는 exception을 handling할 코드7 BeforeTestExecutionCallback
개별 테스트가 실행되기 직전에 실행되는 extension 코드 8 @Test
사용자 테스트 코드 9 TestExecutionExceptionHandler
@Test
실행 시 발생하는 exception을 handling할 코드10 AfterTestExecutionCallback
개별 테스트가 실행된 후 실행되는 extension 코드 11 @AfterEach
개별 테스트가 실행된 후 실행되는 사용자 코드 12 handleAfterEachMethodExecutionException
@AfterEach
에서 발생되는 exception을 handling할 코드13 AfterEachCallback
개별 테스트가 실행된 후 실행되는 extension 코드 14 @AfterAll
모든 테스트가 실행된 후 실행되는 사용자 코드 15 handleAfterAllMethodExecutionException
@AfterAll
에서 발생되는 exception을 handling할 코드16 AfterAllCallback
모든 테스트가 실행된 후 한 번 실행되는 extension 코드 -
other
-
ExecutionCondition
: 테스트 실행 여부 제어 (profile에 따라 제어 가능) -
TestInstancePostProcessor
: 테스트 인스턴스가 생성 된 후 실행 -
ParameterResolver
: 테스트 클래스 생성자, 메서드에 파라미터가 필요한 경우 사용 -
TestExecutionExceptionHandler
: 특정 유형의 exception이 발생할 때 테스트 동작을 제어 - ...
-
-
programming model + extension model
@ExtendWith(MyCustomExtension.class) class SimpleTest { @RegisterExtension Extension extension = new AnotherExtension(42); @ExtendWith({FooExtension.class, BarExtension.class}) @Test @DisplayName("1 + 1 = 2") void addTest() { assertEquals(2, 1 + 1); } }
-
Junit Vintage
- 플랫폼에서 Junit 3, Junit 4 기반 테스트를 실행하기 위한 TestEngine을 제공
- Junit 5는 런타임에서 Java 8 이상 버전이 필요하다
- 컴파일은 8 이전 버전의 JDK도 가능하다.
- 아래 코드는 Junit Jupiter 테스트의 최소 필요 사항으로만 작성된 테스트다.
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 // Test 선언
void addition() {
assertEquals(2, calculator.add(1, 1));
}
}
Test Class
- 적어도 한개의
@Test
메소드가 있는 Top level class, static member class,@Nested
class - abstract class로 설정할 수 없다.
Test Method
-
@Test
,@RepeatedTest
,@ParameterizedTest
,@TestFactory
,@TestTemplate
어노테이션이 붙은 메소드.
Lifecycle Method
-
@BeforeAll
,@AfterAll
,@BeforeEach
,@AfterEach
어노테이션이 붙은 메소드.
Test Method
, Lifecycle Method
는 테스트할 클래스, 부모 클래스, 인터페이스에서 선언한다.
-
두 Method 모두
abstract
로 설정하면 안된다. -
접근 지정자를
public
으로 설정하지 않아도 된다. 하지만private
로 설정하면 안된다.class StandardTests { @BeforeAll static void initAll() { } @BeforeEach void init() { } @Test void succeedingTest() { } @Test void failingTest() { fail("a failing test"); } @Test @Disabled("for demonstration purposes") void skippedTest() { // not executed } @Test void abortedTest() { assumeTrue("abc".contains("Z")); fail("test should have been aborted"); } @AfterEach void tearDown() { } @AfterAll static void tearDownAll() { } }
- 아래 어노테이션은
junit-jupiter-api
의 어노테이션이다.
@Test
-
test 메소드임을 명시
-
Junit 4와 다른점은 attributes를 갖지 않는다.
class BastTest { private final Calculator calculator = new Calculator(); @Test void addition() { assertEquals(2, calculator.add(1, 1)); } static class Calculator { int add(int a, int b) { return a + b; } } }
@ParameterizedTest
-
테스트에서 여러 파라미터에 대한 테스트를 진행할 때 사용
class BastTest { private final Calculator calculator = new Calculator(); @ParameterizedTest @ValueSource(strings = {"1", "2", "3"}) void parseTest(String numberString) { assertDoesNotThrow(() -> Integer.parseInt(numberString)); } @ParameterizedTest @MethodSource(value = "parseTestParam") void parseTestWithMethodSource(Integer number, String numberString) { Integer parsedNum = assertDoesNotThrow(() -> Integer.parseInt(numberString)); assertEquals(number, parsedNum); } private static Stream<Arguments> parseTestParam() { return Stream.of( Arguments.of(123, "123"), Arguments.of(456, "456"), Arguments.of(789, "789") ); } @ParameterizedTest @CsvSource(value = { "1,2", "3,4", "4,5", "5,6", }) void addition_withParam(int a, int b) { assertEquals(a + b, calculator.add(a, b)); } static class Calculator { int add(int a, int b) { return a + b; } } }
@RepeatedTest
-
반복적인 테스트를 진행할 때 사용
class BastTest { @RepeatedTest(value = 10) void repeatedTest() { assertTrue(Math.random() > 0); } }
@DisplayName
- IDE, test runners에서 테스트 실행 시 test report 화면에 표시되는 이름을 설정할 수 있다.
@DisplayName("A special test case")
class DisplayNameDemo {
@Test
@DisplayName("Custom test name containing spaces")
void testWithDisplayNameContainingSpaces() {
}
@Test
@DisplayName("╯°□°)╯")
void testWithDisplayNameContainingSpecialCharacters() {
}
@Test
@DisplayName("😱")
void testWithDisplayNameContainingEmoji() {
}
}
@TestFactory
- Dynamic 테스트를 위한 테스트 Factory 역할
Dynamic Tests
- 일반적인 테스트(
@Test
)는 complie 시점에서 모두 테스트로 지정되며 runtime 시점에서 테스트를 변경할 수 없다. - Dynamic test는 팩토리 메소드(
@TestFactory
어노테이션을 설정한)에 의해 런타임 시점에서 실행된다. -
@TestFactory
메소드는 테스트 케이스는 아니다, 테스트 케이스의 팩토리 메소드이다.-
@TestFactory
메소드는 반드시 단일DynamicNode
,Stream
,Collection
,Iterable
,Iterator
,Array[DynamicNode]
를 반환해야 한다.
-
- Dynamic Test는 일반적인 테스트와는 달리 lifecycle callback이 없다.
-
@BeforeEach
,@AfterEach
, ... 와 같은 lifecycle callback은 dynamic test와 관련이 없다.
-
언제 Dynamic Test를 사용하는지?
-
테스트 실행을 위한 설정, 파라미터들이 runtime에 셋팅되는 경우
-
runtime에서 테스트가 이뤄져야하는 경우
-
(사실 정확하게 어느 상황에서 써야하는지는 잘 모르겠다..)
-
참조 링크
- https://mincong.io/2021/04/09/junit-5-dynamic-tests/#should-we-use-dynamic-tests
- https://medium.com/@BillyKorando/dynamic-testing-in-junit-5-a-practical-guide-a57e3ceaa240
테스트 코드 예시
Dynamic Tests - DynamicNode
@TestFactory DynamicNode dynamicNodeSingleTest() { return dynamicTest("'pop' is a palindrome", () -> assertTrue(isPalindrome("pop"))); }
Dynamic Tests - Stream
@TestFactory Stream<DynamicTest> dynamicTestsFromStream() { return Stream.of("racecar", "radar", "mom", "dad") .map(text -> dynamicTest(text, () -> assertTrue(isPalindrome(text)))); }
Dynamic Tests - Collection
@TestFactory Collection<DynamicTest> dynamicTestsFromCollection() { return Arrays.asList( dynamicTest("1st dynamic test", () -> assertTrue(isPalindrome("madam"))), dynamicTest("2nd dynamic test", () -> assertEquals(4, calculator.multiply(2, 2))) ); }
Dynamic Tests - Iterable
@TestFactory Iterable<DynamicTest> dynamicTestsFromIterable() { return Arrays.asList( dynamicTest("3rd dynamic test", () -> assertTrue(isPalindrome("madam"))), dynamicTest("4th dynamic test", () -> assertEquals(4, calculator.multiply(2, 2))) ); }
Dynamic Tests - Iterator
@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(); }
@TestTemplate
-
테스트 케이스의 template 역할 (
@TestTemplate
자체로는 테스트 메서드가 아니다.) -
@TestTemplate
에서 확장한 extension(ExtendWith(XXXProvider.class)
)에서 반환하는TestTemplateInvocationContext
의 수에 따라 호출될 횟수를 정한다.-
@TestTemplate
메서드는 하나 이상의TestTemplateInvocationContextProvider
가 등록된 경우에만 실행할 수 있다. - 각 provider는
TestTemplateInvocationContext
인스턴스의 Stream을 제공해야 한다. - 각 context는 사용자가 지정한 displayName과
@TestTamplate
메서드의 다음 호출에만 사용되는 추가 extension 목록을 지정할 수 있다.
@Target({ElementType.ANNOTATION_TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @TestTemplate // TestTemplate임을 명시 @ExtendWith(CustomTemplateInvocationContextProvider.class) // Stream<TestTemplateInvocationContext>을 반환하는 Provider 확장 public @interface CustomTestTemplateTest { }
-
TestTemplateInvocationContextProvider
-
@TestTemplate
메서드의 호출을 위해 하나 또는 여러 context를 제공하려는 extension API를 정의한다. -
TestTemplateInvocationContextProvider
사용하여 다른 context에서@TestTemplate
메서드를 실행할 수 있다.- 다른 매개 변수를 사용하여 테스트 클래스 인스턴스를 다르게 준비
- context를 수정하지 않고 여러 번 테스트를 실행
-
해당 인터페이스에서 구현해야할 메소드는
supportsTestTemplate
와prociveTestTemplateInvocationContexts
이다.-
supportsTestTemplate
: 테스트 프레임워크에 의해 호출되어 특정 extension이 곧 실행될@TestTamplate
메서드에서 작동할지 여부를 결정한다.- 작동(
active
상태)한다면prociveTestTemplateInvocationContexts
메서드가 호출된다.
- 작동(
-
prociveTestTemplateInvocationContexts
:supportsTestTemplate
메서드에서 extension이@TesetTamplate
메서드를 실행해야 하는 경우 실행된다.- 해당 메서드에서 반환된 Stream이 연결되고
@TestTamplate
메서드는 모든 active provider의 context를 사용하여 호출된다.
- 해당 메서드에서 반환된 Stream이 연결되고
public class CustomTemplateInvocationContextProvider implements TestTemplateInvocationContextProvider { private final static List<String> FRUITS = Arrays.asList("apple", "banana", "lemon"); @Override public boolean supportsTestTemplate(ExtensionContext context) { return context.getTestMethod().isPresent(); } @Override public Stream<TestTemplateInvocationContext> provideTestTemplateInvocationContexts(ExtensionContext context) { // 여기서 반환하는 TestTemplateInvocationContext 수 만큼 TestTemplate이 호출된다. // 아래 예시로는 총 2번 실행됨 return Stream.of( makeTestTemplateInvocationContext("apple", "lemon"), makeTestTemplateInvocationContext("banana") ); } private TestTemplateInvocationContext makeTestTemplateInvocationContext(String... parameters) { return new TestTemplateInvocationContext() { // DisplayName 설정 @Override public String getDisplayName(int invocationIndex) { return String.format("Invocation index %d. Parameter: %s",invocationIndex, String.join(",", parameters)); } // 추가 extension 설정 @Override public List<Extension> getAdditionalExtensions() { return Collections.singletonList(new ParameterResolver() { @Override public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) { // Test Method의 파라미터 타입 체크 return parameterContext.getParameter().getType().equals(String[].class); } @Override public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) { return parameters; } }); } }; } }
-
TestTemplate 사용한 테스트 코드 예시
@CustomTestTemplateTest
void testTemplate(String... fruit) {
// given
// when
List<String> fruits = Arrays.asList("apple", "banana", "lemon", "strawberry");
// then
for (String item : fruit) {
assertTrue(fruits.contains(item));
}
}
@TestMethodOrder
- 기본적으로 테스트 실행은 명확한 순서를 지키지 않는다.
- 단위 테스트는 테스트 순서에 의존하면 안되므로 순서를 지키즞 것이 맞다.
-
@TestMethodOrder
를 사용하면 순서를 보장하는 테스트 실행이 가능하다.
테스트 코드 예시
class TestExecutionOrder {
@TestMethodOrder(MethodOrderer.DisplayName.class)
static class testDisplayName{
@Test
@DisplayName("1 test: order 1 test")
void firstTest() {
// perform assertions against null values
}
@Test
@DisplayName("2 test: order 2 test")
void secondTest() {
// perform assertions against empty values
}
@Test
@DisplayName("3 test: order 3 test")
void thirdTest() {
// perform assertions against valid values
}
}
@TestMethodOrder(MethodName.class)
static class testMethodName{
@Test
@DisplayName("cccTest")
void cccTest() {
// perform assertions against null values
}
@Test
@DisplayName("bbbTest")
void bbbTest() {
// perform assertions against empty values
}
@Test
@DisplayName("aaaTest")
void aaaTest() {
// perform assertions against valid values
}
}
@TestMethodOrder(OrderAnnotation.class)
static class testOrderAnnotation{
@Test
@Order(1)
@DisplayName("first test: order 1 test")
void firstTest() {
// perform assertions against null values
}
@Test
@Order(2)
@DisplayName("second test: order 2 test")
void secondTest() {
// perform assertions against empty values
}
@Test
@Order(3)
@DisplayName("third test: order 3 test")
void thirdTest() {
// perform assertions against valid values
}
}
@TestMethodOrder(Random.class)
static class testRandom{
@Test
@Order(1)
@DisplayName("first test: order 1 test")
void firstTest() {
// perform assertions against null values
}
@Test
@Order(2)
@DisplayName("second test: order 2 test")
void secondTest() {
// perform assertions against empty values
}
@Test
@Order(3)
@DisplayName("third test: order 3 test")
void thirdTest() {
// perform assertions against valid values
}
}
}
@TestInstance
- Junit은 각 테스트 실행 시 발생할 수 있는 side effect를 피하기 위해 테스트 메서드 실행 전 각 테스트 클래스의 새 인스턴스를 생성한다.
- Junit 테스트 생명주기의 기본 값은 메서드 단위 생명주기다.
- 만약 동일한 테스트 인스턴스에서 테스트 클래스의 모든 테스트 메서드를 실행시키기 위해서는
@TestInstance(Lifecycle.PER_CLASS)
로 설정해야 한다.- 테스트 클래스 당 1개의 테스트 인스턴스가 생성된다.
-
@TestInstance(Lifecycle.PER_CLASS)
로 설정하게 되면@BeforeAll
은static
를 제거해도 된다. - DB 연결 또는 테스트 실행을 위해 대용량 파일을 로딩할 때 유용하다.
- 참조
@Tag
-
@Tag
어노테이션으로 테스트 클래스 또는 메서드를 태깅할 수 있다. -
Tag할 때는 작성 규칙을 지켜야한다.
- tag 설정 시
null
,blank("")
안됨 - 공백이 없어야 함
- IOS control character가 포함되면 안됨
- 아래 예약 문자를 포함하면 안됨
,
(
)
&
|
!
class TagTest { @Test @Tag("dev") void devTest() { // dev logic } @Test @Tag("real") void realTest() { // real logic } }
- tag 설정 시
Meta-Annotations and Composed Annotation
- Junit의 어노테이션을 사용하여 메타 어노테이션으로 사용할 수 있다.
- 아래 예시는
dev
라는 태그를 가진 테스트를 표시하기 위한 어노테이션 예제이다.
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Tag("dev")
@Test
public @interface DevTest {
}
// --------------------------------------------
@DevTest
void onlyDevTest() {
// some logic
}
- 특정 환경변수를 기준으로 테스트를 실행할 수 있다.
@Test
@EnabledIfEnvironmentVariable(named = "ENV", matches = "dev")
void onlyOnStagingServer() {
// ...
}
@Test
@DisabledIfEnvironmentVariable(named = "ENV", matches = "release")
void notOnDeveloperWorkstation() {
// ...
}
- maven surefire-plugin 을 추가한 뒤 mvn 커맨드 라인에서 환경변수를 설정한다.
mvn test -DENV=dev
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.19</version>
<dependencies>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-surefire-provider</artifactId>
<version>1.0.0</version>
</dependency>
</dependencies>
<configuration>
<includes>**/*.java</includes>
<environmentVariables>
<ENV>${ENV}</ENV>
</environmentVariables>
</configuration>
</plugin>
Junit 4
- junit 4에서는 테스트를 실행시키기 위해
Runner
클래스가 필요했다.@RunWith(XXX.class)
- 테스트 클래스에서 동작 방식을 재정의 하거나 쉽게 추가하기 위해
@Rule
을 사용했다.- 임시 폴더 관리 :
TemporaryFolder
- 예외 관리 :
ExpectedException
- TestSuite의 클래스마다 적용 :
ClassRule
- ...
- 임시 폴더 관리 :
Junit 5
- Junit 5 에서는 Junit 4에서의
Runner
,Rule
을 하나의 개념인Extension
으로 사용한다. -
Extension
그 자체로는 단지 Marker interface이다.
- Extension을 등록하는 방법은 3가지다.
- 선언적 :
@ExtendWith
사용 - 프로그래밍 :
@RegisterExtension
- java의
ServiceLoader
매커니즘 사용
- 선언적 :
Extension 등록 예시
// method
@ExtendWith(RandomParametersExtension.class)
@Test
void test(@Random int i) {
// ...
}
// class
@ExtendWith(RandomParametersExtension.class)
class MyTests {
// ...
}
Spring boot의 extension
-
@SpringBootTest
어노테이션은@ExtendWith(SpringExtension.class)
를 포함한 메타 어노테이션이다. - SpringExtension은 다양한 Extension을 상속한 API를 구현한다.
- BeforeAllCallback : 모든 테스트가 실행되기 전에 테스트 컨테이너에 추가 동작을 실행
- AfterAllCallback : 모든 테스트가 실행된 후 테스트 컨테이너에 추가 동작을 실행
- TestInstancePostProcessor : 테스트 인스턴스의 post-process 동작을 실행
- BeforeEachCallback : 각 테스트 및 사용자 정의 메서드(
@BeforeEach
)가 실행되기 전에 추가 동작을 실행s - AfterEachCallback : 각 테스트 및 사용자 정의 메서드(
@AfterEach
)가 실행된 후 추가 동작을 실행 - BeforeTestExecutionCallback : 각 테스트가 실행되기 전, 사용자 정의 메서드(
@BeforeEach
)가 실행 된 후 추가 동작을 실행 - AfterTestExecutionCallback : 각 테스트가 실행된 후, 사용자 정의 메서드(
@AfterEach
)가 실행되기 전에 추가 동작을 실행 - ParameterResolver : Runtime에서 동적으로 파라미터를 설정