What is JUnit 5?
JUnit 5
Société Générale Use, Contribute and Attract: learn about Société Générale's open source strategy.
junit.org
junit-team/junit5
✅ The 5th major version of the programmer-friendly testing framework for Java and the JVM - junit-team/junit5
github.com
JUnit 5 Architecture: JUnit Platform + JUnit Jupiter + JUnit Vintage
기존의 큰 하나의 jar 덩어리였던 junit4 가 Junit5가 되면서 Viantage(4 번전과의 호환성), Jupiter(5버전 모듈), Platform(Extension, 실행, 관리)로 나뉘어졌다.
Supported Java Versions
Java 8버전 이상을 요구하지만 이전 버전 JDK로 컴파일 된 코드는 테스트 할 수 있다.
Annotations
*공통 Such methods are inherited unless they are overridden. (재정의 하지 않는 이상)
Annotation |
원문 |
해석 |
@Test |
Denotes that a method is a test method. Unlike JUnit 4’s @Test annotation, this annotation does not declare any attributes, since test extensions in JUnit Jupiter operate based on their own dedicated annotations. Such methods are inherited unless they are overridden. |
해당 메소드가 테스트 메소드라는 것을 의미한다. Junit5 test 실행 은 @Test 자체로 작동하기 때문에 재정의 되지 않는 이상Junit4와는 다르게 이 Annotation은 어떠한 속성도 정의하지 않는다. |
@ParameterizedTest |
Denotes that a method is a parameterized test. Such methods are inherited unless they are overridden. |
메서드가 매개변수를 기반으로 된 테스트임을 나타낸다. |
@RepeatedTest |
Denotes that a method is a test template for a repeated test. Such methods are inherited unless they are overridden. |
메소드를 반복해서 테스트 하는 것을나타낸다. |
@TestFactory |
Denotes that a method is a test factory for dynamic tests. Such methods are inherited unless they are overridden. |
동적으로 테스트 하는 메소드를 나타낸다. |
@TestTemplate |
Denotes that a method is a template for test cases designed to be invoked multiple times depending on the number of invocation contexts returned by the registered providers. Such methods are inherited unless they are overridden. |
등록된 제공자로 부터 return 되는 호출 컨텍스트 수에 따라 여러번 호출되도록 설계된 테스트 케이스용 템플릿임을 나타낸다. |
@TestMethodOrder |
Used to configure the test method execution order for the annotated test class; similar to JUnit 4’s @FixMethodOrder. Such annotations are inherited. |
Junit 4의 @FixMethodOrder 와 유사하며, 테스트 method 를 실행 순서를 설정하는데 사용된다. |
@TestInstance |
Used to configure the test instance lifecycle for the annotated test class. Such annotations are inherited. |
@TestIntance 주석이 달린 클래스의 |
@DisplayName |
Declares a custom display name for the test class or test method. Such annotations are not inherited. |
테스트 클래스 또는 테스트 메소드에 대한 display name (보여지는 명칭)을 선언한다. |
@DisplayNameGeneration |
Declares a custom display name generator for the test class. Such annotations are inherited. |
테스트 클래스의 보여지는 명칭을 사용자화 할 수 있다. |
@BeforeEach |
Denotes that the annotated method should be executed before each @Test, @RepeatedTest, @ParameterizedTest, or @TestFactory method in the current class; analogous to JUnit 4’s @Before. Such methods are inherited unless they are overridden. |
@Test, @Repeated, |
@AfterEach |
Denotes that the annotated method should be executed after each @Test, @RepeatedTest, @ParameterizedTest, or @TestFactory method in the current class; analogous to JUnit 4’s @After. Such methods are inherited unless they are overridden. |
@Test, @Repeated, |
@BeforeAll |
Denotes that the annotated method should be executed before all @Test, @RepeatedTest, @ParameterizedTest, and @TestFactory methods in the current class; analogous to JUnit 4’s @BeforeClass. Such methods are inherited (unless they are hidden or overridden) and must be static (unless the "per-class" test instance lifecycle is used). |
모든 @Test, @Repeated, |
@AfterAll |
Denotes that the annotated method should be executed after all @Test, @RepeatedTest, @ParameterizedTest, and @TestFactory methods in the current class; analogous to JUnit 4’s @AfterClass. Such methods are inherited (unless they are hidden or overridden) and must be static (unless the "per-class" test instance lifecycle is used). |
모든 @Test, @Repeated, 무조건 static 이어야 하며 per-class 생명 주기의 테스트 인스턴스가 아니여야 한다. |
@Nested |
Denotes that the annotated class is a non-static nested test class. @BeforeAll and @AfterAll methods cannot be used directly in a @Nested test class unless the "per-class" test instance lifecycle is used. Such annotations are not inherited. |
@Nested 주석이 달린 클래스는 static이지 않은 중첩된 테스트 클래스이다. 이 @Nested 클래스는 |
@Tag |
Used to declare tags for filtering tests, either at the class or method level; analogous to test groups in TestNG or Categories in JUnit 4. Such annotations are inherited at the class level but not at the method level. |
유사한 테스트 그룹 또는 Junit 4의 카테고리 같은 class 또는 method 수준의 테스트들을 필터링하는 목적으로 사용한다. |
@Disabled |
Used to disable a test class or test method; analogous to JUnit 4’s @Ignore. Such annotations are not inherited. |
테스트 클래스나 테스트 메소드 를 사용하지 않는데 사용된다. |
@Timeout |
Used to fail a test, test factory, test template, or lifecycle method if its execution exceeds a given duration. Such annotations are inherited. |
test, test factory, test template 또는 메소드의 생명주기가 주어진 시간안에 수행하도록 하는데 사용한다. |
@ExtendWith |
Used to register extensions declaratively. Such annotations are inherited. |
명시적으로 extensions 를 등록하는데 사용한다. |
@RegisterExtension |
Used to register extensions programmatically via fields. Such fields are inherited unless they are shadowed. |
필드를 통해 프로그래밍 방식으로 확장을 등록할 때 사용한다. |
@TempDir |
Used to supply a temporary directory via field injection or parameter injection in a lifecycle method or test method; located in the org.junit.jupiter.api.io package. |
org.junit.jupiter.api.io package. 의 위치된 메소드 생명주기 또는 테스트 method에 필드 또는 매개변수 주입을 통해 임시 경로를 제공하는데 사용한다. |
Writing Tests
필자가 작성한 코드는 아래의 github에서 볼 수 있다 주로 Junit에서 제공하는 User Guide 코드를 따라 쳐본 것이기 때문에 Junit Document 코드를 보는게 더 좋을 수 도 있다.
doyoung0205/junit5-study
junit5-study repository. Contribute to doyoung0205/junit5-study development by creating an account on GitHub.
github.com
JUnit 5 User Guide
Although the JUnit Jupiter programming model and extension model will not support JUnit 4 features such as Rules and Runners natively, it is not expected that source code maintainers will need to update all of their existing tests, test extensions, and cus
junit.org
필자는 주로 Spring boot 2.4 버전 프로젝트에서 사용해 보았는데 spring-boot-starter-test 에서 기본적으로 Junit5를 다음과 같이 제공한다.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
참고: vintage는 이전 버전과 호환이기 때문에 기본적으로 지원하지 않는다.
@Test
테스트 코드는 Junit에서 test 폴더에서 class 와 method를 만들고 method에 @Test 어노테이션 만 붙여주면 된다.
class 는 굳이 public 이 아니여도 상관없지만 private 이면 안된다.
class StandardTests {
@Test
void succeedingTest() {
}
}
Display Names
위의 테스트 결과 화면을 보면 class 이름이나 method 이름이 그대로 찍히는 것을 볼 수 있다.
여기서 보여지는 이름을 @DisplayName 을 통해서 custom 할 수 있다.
@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
DisplayName 에 대한 설정을 해줄 수 있다. 예를 들어서
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
class A_year_is_not_supported {
@Test
void if_it_is_zero() {
}
}
읽기 편하게 쓰다보면 띄어쓰기 대신 _ 를 사용하기도 하는데, display name 에서 _ 를 공백으로 보이게 할 수 있다.
구분자를 추가할 수 있는 Generation 도 있다.
@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() {
}
}
Assertions
한국말로 해석하면 (사실임을) 주장 이다.
개발자가 생각한 대로 되었는지 테스트할 때 사용된다.
1. assertEquals(같다고 주장), assertNotEquals (같지 않다고 주장): (expect, actual, ?message)
- expect: 기대한 값
- actual: 실제 결과 값
- message: 실패시 메시지
// 예시
assertEquals(2, calculator.add(1, 1)); // true
assertEquals(4, calculator.multiply(2, 2), // true
"The optional failure message is now the last parameter");
assertNotEquals(2, calculator.multiply(1, 3), // true
assertNotEquals(5, calculator.multiply(2, 2), // true
"The optional failure message is now the last parameter");
2. assertTrue, assertFalse: (expect, actual, ?message)
- expect: 기대한 값
- actual: 실제 결과 값
- message: 실패시 메시지
// 예시
assertTrue('a' < 'b', () -> "Assertion messages can be lazily evaluated -- "
+ "to avoid constructing complex messages unnecessarily.");
assertFalse('a' > 'b', () -> "Assertion messages can be lazily evaluated -- "
+ "to avoid constructing complex messages unnecessarily.");
3. assertAll: ( ?heading, executables)
- heading: 제목
- executable: 실행할 assert문들
assert문을 별개로 나열하다 보면, 처음 테스트 문이 실패를 했을 때 이후 테스트문을 확인하지 못하는 단점이 있지만,
assertAll 을 사용하면 전체 결과를 확인 할 수 있다.
@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"))
);
}
);
}
4. assertThrows: (expectType, executable)
- expectType: 예측할 ExceptionType
- executable: 실행할 assert문들
예상한 ExceptionType throw되었는지 테스트한다.
// 예시
Exception exception = assertThrows(ArithmeticException.class, () ->
calculator.divide(1, 0));
5. assertTimeout: (timeout, executable)
- timeout: 주어진 시간
- executable: 실행할 assert문들
주어진 시간안에 작동되는지 테스트한다
// 성공 예시
assertTimeout(ofMinutes(2), () -> {
// Perform task that takes less than 2 minutes.
});
// 실패 예시
assertTimeout(ofMillis(10), () -> {
// Simulate task that takes more than 10 ms.
Thread.sleep(100);
});
위의 코드에서는 한 가지 불편한 점이 있다.
assertTimeout의 내부속으로 들어가보면, 다음과 같이 되어있다.
private static <T> T assertTimeout(Duration timeout,ThrowingSupplier<T> supplier, Object messageOrSupplier) {
long timeoutInMillis = timeout.toMillis();
long start = System.currentTimeMillis();
Object result = null;
try {
result = supplier.get();
} catch (Throwable var10) {
ExceptionUtils.throwAsUncheckedException(var10);
}
long timeElapsed = System.currentTimeMillis() - start;
if (timeElapsed > timeoutInMillis) {
Assertions.fail(AssertionUtils.buildPrefix(AssertionUtils.nullSafeGet(messageOrSupplier)) + "execution exceeded timeout of " + timeoutInMillis + " ms by " + (timeElapsed - timeoutInMillis) + " ms");
}
return result;
}
작동되는 테스트 코드를 실행하기전에 시작시간을 측정하고 테스트 코드가 끝난다음에 종료시간을 측정한다.
예를 들어 주어진 시간이 2초인데, 테스트 코드 시간이 10초가 걸리 더라도 실패결과를 10초 후에 알 수 있는 것이다.
즉, 2초가 지났을 때 바로 실패를 알 수 없다는 이야기이다.
따라서 이를 보안하는 또다른 assert가 있다.
5. assertTimeoutPreemptively: (timeout, executable)
- timeout: 주어진 시간
- executable: 실행할 assert문들
실패를 확인하는데 있어 만약 assertTimeout 이였으면 3초이상이 걸리겠지만,
assertTimeoutPreemptively는 0.01초만에 실패를 확인 할 수있다.
@Test
void timeoutExceeded() {
assertTimeoutPreemptively(ofMillis(10), () -> {
// Simulate task that takes more than 10 ms.
Thread.sleep(3000);
});
}
@Disabled
@Disabled 는 기존에 있던 테스트 코드나 테스트 클래스를 잠시 비활성화 하는데 사용한다.
// 예시
@Disabled("Disabled until bug #99 has been fixed")
class DisabledClassDemo {
@Test
void testWillBeSkipped() {
}
}
class DisabledTestsDemo {
@Disabled("Disabled until bug #42 has been resolved")
@Test
void testWillBeSkipped() {
}
}
@DisabledIf, EnabledIf
Disabled 와 Enabled 에 대해서 상황에 따라서 적용 시킬 수 있다.
@Test
@EnabledIf("customCondition")
void enabled() {
// ...
}
@Test
@DisabledIf("customCondition")
void disabled() {
// ...
}
boolean customCondition() {
return true;
}
@Tag
테스트 코드에 Tag를 붙여놓는다면, 나중에 많은 테스트 코드 중에 원하는 Tag 들에 대해서 실행 할 수 있다.
// 예시
@Test
@Tag("integration")
void testIntegration() {
...
}
}
@Tag("api")
class TagTests {
...
}
필터링(maven pom.xml)
<build>
<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>${maven-surefire-plugin.version}</version>
<configuration>
<!-- Include tags -->
<groups>fast, api</groups>
<!-- Exclude tags -->
<excludedGroups>load, acceptance</excludedGroups>
</configuration>
</plugin>
</plugins>
</build>
필터링(maven cli)
mvn test -Dgroups="load" -DexcludedGroups="api"
@TestMethodOrder, @Order
@Test 메소드가 여러개인 경우 순서가 정해져 있지 않다.
위에서 아래로 차례대로 실행되는 것이 아니다.
class NotOrderedTestsDemo {
@Test
void nullValues() {
// perform assertions against null values
}
@Test
void emptyValues() {
// perform assertions against empty values
}
@Test
void validValues() {
// perform assertions against valid values
}
}
따라서 순서를 정하고 싶을 때는 @TestMethodOrder, @Order 를 이용하여 다음과 같이 코드를 작성한다.
@Nested
기존에 테스트 클래스 안에 중첩으로 테스트 클래스를 만들 때 사용된다.
Stack 에 대해서 처음 인스턴스 초기화를 하고 나서
초기화가 잘되었는지 테스트를 하고
Stack 의 push 테스트를 하면서 중첩해서 묻고 계속 나아갈 수 있다.
@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());
}
}
}
}
@RepeatedTest
테스트 코드를 반복해서 실행한다.
@RepeatedTest(10)
void repeatedTest() {
// ...
}
Dependency Injection for Constructors and Methods
테스트 코드를 실행 할 때, 매개변수에 주입되는 객체들을 주입 시킬 수 있다.
기본적으로 주입되는 객체들은 다음과 같다.
1. TestInfo
: 지금 실행하는 테스트의 정보를 가져옴.
@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() {
}
}
2. TestReporter
: 지금 실행하는 테스트속에 상황을 알려줌
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<String, String> values = new HashMap<>();
values.put("user name", "dk38");
values.put("award year", "1974");
testReporter.publishEntry(values);
}
}
3. RepetitionInfo
: 지금 실행하는 반복되는 테스트 속 에 상황을 알려준다.
private Logger logger = // ...
// end::user_guide[]
Logger.getLogger(RepeatedTestsDemo.class.getName());
// tag::user_guide[]
@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());
}
또한 displayName 도 바로 커스텀 할 수 있다.
@RepeatedTest(value = 1, name = "{displayName} {currentRepetition}/{totalRepetitions}")
@DisplayName("Repeat!")
void customDisplayName(TestInfo testInfo) {
assertEquals("Repeat! 1/1", testInfo.getDisplayName());
}
@BeforeEach
void init() {
System.out.println("init @BeforeEach");
}
@Test
void succeedingTest() {
System.out.println("succeedingTest");
}
@Test
void succeedingTest2() {
System.out.println("succeedingTest2");
}
@AfterEach
void tearDown() {
System.out.println("tearDown @AfterEach");
}
-- 출력결과 --
init @BeforeEach
succeedingTest2
tearDown @AfterEach
init @BeforeEach
succeedingTest
tearDown @AfterEach
@BeforeAll, @AfterAll
@BeforeAll, @AfterAll 은 최초, 최후 라고 생각 하면 편하다.
@BeforeAll
static void initAll() {
System.out.println("initAll @BeforeAll");
}
@BeforeEach
void init() {
System.out.println("init @BeforeEach");
}
@Test
void succeedingTest() {
System.out.println("succeedingTest");
}
@Test
void succeedingTest2() {
System.out.println("succeedingTest2");
}
@AfterEach
void tearDown() {
System.out.println("tearDown @AfterEach");
}
@AfterAll
static void tearDownAll() {
System.out.println("tearDownAll @AfterAll");
}
-- 출력 결과 --
initAll @BeforeAll
init @BeforeEach
succeedingTest2
tearDown @AfterEach
init @BeforeEach
succeedingTest
tearDown @AfterEach
tearDownAll @AfterAll
Test Interface and Default Methods
위의 lifecycle 과 관련된 메소드를 가지고 Logger 를 만들 수 있다.
@TestInstance(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()));
}
}
여기서 주의 할 점은. 꼭 Test Instance 의 lifecycle 이 PER_CLASS여야 한다는 점이다.
Junit5에서는 기본적으로 PER_METHOD인데, 메소드 단위로 lifecycle이 돌면 아래와 같은 오류가 나온다.
org.junit.platform.commons.JUnitException:
@BeforeAll method 'public default void
me.doyoung.junit5study.lifecycle.TestLifecycleLogger.beforeAllTests()'
must be static unless the test class is annotated with
@TestInstance(Lifecycle.PER_CLASS).
따라서 꼭 PER_CLASS 설정을 해줘야 한다.
매번 클래스마다 하기 귀찮다면
src/test/resources 위치에 junit-platform.properties 파일을 만들고 다음과 같은 설정을 해주면 된다.
junit.jupiter.testinstance.lifecycle.default=per_class
@ParameterizedTest
여러개의 매개변수를 사용하여 여러 번 테스트를 실행 할 수 있게 한다.
@ParameterizedTest
@ValueSource(strings = { "racecar", "radar", "able was I ere I saw elba" })
void palindromes(String candidate) {
assertTrue(StringUtils.isPalindrome(candidate));
}
@NullSource, @EmptySource
@NullSource, @EmptySource 를 이용해서 매개변수에 null, 빈값을 주입 할 수 있다.
@ParameterizedTest
@NullSource
@EmptySource
void nullEmptyAndBlankStrings(String text) {
assertTrue(text == null || text.trim().isEmpty());
}
@NullAndEmptySource
줄여서 @NullAndEmptySource 이렇게도 사용할 수 있다.
@ParameterizedTest
@NullAndEmptySource
void nullEmptyAndBlankStrings(String text) {
assertTrue(text == null || text.trim().isEmpty());
}
@EnumSource
@EnumSource 을 통해서 Enum 을 주입할 수 있다.
@ParameterizedTest
@EnumSource(ChronoUnit.class)
void testWithEnumSource(TemporalUnit unit) {
assertNotNull(unit);
}
해당 Enum 의 모든 값들이 매개변수로 들어오는 것을 확인 할 수 있다.
약간 Autowired 처럼 같은 결과지만 다음과 같이 표현 할 수 있다.
@ParameterizedTest
@EnumSource
void testWithEnumSourceWithAutoDetection(ChronoUnit unit) {
assertNotNull(unit);
}
Include
만약, 해당 Enum에 원하는 값들만 주입하고 싶다면, names 속성을 이용하면 된다.
@ParameterizedTest
@EnumSource(names = { "DAYS", "HOURS" })
void testWithEnumSourceInclude(ChronoUnit unit) {
assertTrue(EnumSet.of(ChronoUnit.DAYS, ChronoUnit.HOURS).contains(unit));
}
Exclude
또 다르게 원하는 값들만 제외 하고 싶다면 mode 속성을 EXCLUDE로 변경하고 names 를 주면 된다.
@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
값을 주입하는데 있어서 복잡한 값들을 가져온다면, 메소드의 return 값을 주입 받을 수 도 있다.
1. Local Method
클래스안에 매소드를 이용해서 주입을 받는다.
@ParameterizedTest
@MethodSource("stringProvider")
void testWithExplicitLocalMethodSource(String argument) {
assertNotNull(argument);
}
static Stream<String> stringProvider() {
return Stream.of("apple", "banana");
}
2. External Method
다른 클래스에 있는 메소드를 이용 할 수 도 있다.
@ParameterizedTest
@MethodSource("me.doyoung.junit5study.parameter.StringsProviders#tinyStrings")
void testWithExternalMethodSource(String tinyString) {
// test with tiny string
}
3. Multi arg
여러개의 매개변수의 주입 받을 수 도 있다.
@ParameterizedTest
@MethodSource("stringIntAndListProvider")
void testWithMultiArgMethodSource(String str, int num, List<String> list) {
assertEquals(5, str.length());
assertTrue(num >= 1 && num <= 2);
assertEquals(2, list.size());
}
static Stream<Arguments> stringIntAndListProvider() {
return Stream.of(
arguments("apple", 1, Arrays.asList("a", "b")),
arguments("lemon", 2, Arrays.asList("x", "y"))
);
}
@CsvSource
어노테이션 속성에 값을 정의해서 주입 할 수 있다.
@ParameterizedTest
@CsvSource({
"apple, 1",
"banana, 2",
"'lemon, lime', 0xF1"
})
void testWithCsvSource(String fruit, int rank) {
assertNotNull(fruit);
assertNotEquals(0, rank);
}
@CsvFileSource
다른 곳에 있는 csv파일을 불러와 값을 주입 할 수 있다.
@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);
}
- resources: test/java/resource 가 기본경로 이다.
- files: classpath 가 기본경로 이다.
- numLinesToSkip: 파일 안에 해당 라인으로 스킵한다.
@ArgumentsSource
주입할 인자들을 ArgumentsProvier 를 implements 해 관리할 수도 있다.
@ParameterizedTest
@ArgumentsSource(MyArgumentsProvider.class)
void testWithArgumentsSource(String argument) {
assertNotNull(argument);
}
static
public class MyArgumentsProvider implements ArgumentsProvider {
@Override
public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
return Stream.of("apple", "banana").map(Arguments::of);
}
}
rgumentConverter
예를 들어 @EnumSource 를 사용했다면 매개변수는 Enum타입이여야 하지만
주입되는 과정에서 원하는 값으로 변환 시켜줄 수 있다.
@ParameterizedTest
@EnumSource(ChronoUnit.class)
void testWithExplicitArgumentConversion(
@ConvertWith(ToStringArgumentConverter.class) String argument) {
assertNotNull(ChronoUnit.valueOf(argument));
}
static
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);
}
}
@AggregateWith
매개변수로 주입되는 값들을 ArgumentsAggregator 구현하여 원하는 객체로 변환 할 수 있다.
@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
}
static
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));
}
}
여기서 @AggregateWith(PersonAggregator.class) 를 하나의 어노테이션으로도 만들 수 있다.
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
@AggregateWith(PersonAggregator.class)
public @interface CsvToPerson {
}
@ParameterizedTest
@CsvSource({
"Jane, Doe, F, 1990-05-20",
"John, Doe, M, 1990-10-22"
})
void testWithCustomAggregatorAnnotation(@CsvToPerson Person person) {
// perform assertions against person
}
Junit5 문서를 읽으면서 추가로 공부해야하는 것들이 생겨났다.
- @TestTemplate, @ExtendWith, @TestFactory
- json 파일을 매개변수로 받는 법
- Mockito
- BDD
추후에 더 시간을 써서 공부해 포스팅 하겠다.
'Junit5' 카테고리의 다른 글
Junit5 에서 Mockito 사용하기 (0) | 2021.01.02 |
---|
댓글