What is JUnit 5?
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 코드를 보는게 더 좋을 수 도 있다.
필자는 주로 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 |
---|
댓글