본문 바로가기
Junit5

Junit5 시작

by doyoungKim 2021. 1. 2.

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

출처: https://medium.com/techwasti/junit5-tutorial-part-1-24471afdd68f

 

기존의 큰 하나의 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,
@Parameterized 또는 현재 클래스의 @TestFactory mehtod 들의 각각 실행주기 보다 전에 실행 되는 테스트를 나타낸다.
Junit4의 @Before 과 비슷함

@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,
@Parameterized 또는 현재 클래스의 @TestFactory mehtod 들의 각각
실행주기 보다 후에 실행 되는 테스트를 나타낸다.
Junit4의 @After 과 비슷함

@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,
@Parameterized 또는 현재 클래스의 @TestFactory mehtod 들의 실행주기 이전에 실행 함을 나타내는 테스트
Junit4의 @BeforeClass 와 유사함.

무조건 static 이어야 하며 per-class 생명 주기의 테스트 인스턴스가 아니여야 한다.

@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,
@Parameterized 또는 현재 클래스의 @TestFactory mehtod 들의 실행주기 이후에 실행 함을 나타내는 테스트
Junit4의 @AfterClass 와 유사함.

무조건 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 클래스는
@BeforeAll 와 @AfterAll methods 들은 테스트 인스턴스 생명주기가 per-class가 아닌 이상 직접 사용할 수 없다.

@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.

테스트 클래스나 테스트 메소드 를 사용하지 않는데 사용된다.
Junit 4의 Ignore 와 비슷하다.

@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 를 이용하여 다음과 같이 코드를 작성한다.

  @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));
}

결과 화면,  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

추후에 더 시간을 써서 공부해 포스팅 하겠다.

 

728x90

'Junit5' 카테고리의 다른 글

Junit5 에서 Mockito 사용하기  (0) 2021.01.02

댓글