Junit5 개념

KIMYEONGJUN·2024년 3월 14일
0
post-thumbnail

목적

정보처리기사 필기시험이 끝난후에 오픈소스 코드를 공부하면서 프로젝트에 기여하고싶어서 오픈소스 코드를 찾아보다가 Apache에서 오픈소스 프로젝트를 이슈가 올라와서 보니깐 Junit4에서 Junit5를 마이그레이션을 해야한다고 나와있어서 기여를하고싶어서 참여하게됐다. 참여하면서 Junit개념을 정리하고자 한다.

Junit5

Junit은 차세대 Java Test 프레임워크로 3가지의 서브 패키지로 구성되어 있다.

JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage

JUnit Platform: JVM에서 테스트 프레임워크를 시작하기 위한 기반 역할을 한다. 또한 플랫폼에서 실행되는 테스트 프레임워크를 개발하기 위한 TestEngine API를 정의한다. 또한 플랫폼은 명령줄에서 플랫폼을 시작할 수 있는 Console Launcher와 플랫폼에서 하나 이상의 테스트 엔진을 사용하여 사용자 지정 테스트 스위트를 실행할 수 있는 JUnit Platform Suite Engine을 제공한다. JUnit Platform에 대한 일등 지원은 인기 있는 IDE(IntelliJ IDEA, Eclipse, NetBeans 및 Visual Studio Code 참조) 및 빌드 도구(Gradle, Maven 및 Ant 참조)에도 존재한다.

Junit Jupiter: JUnit 5의 쓰기 테스트와 확장을 위한 프로그래밍 모델과 확장 모델을 결합한 것이다. Jupiter 서브 프로젝트는 플랫폼에서 테스트를 기반으로 Jupiter를 실행하기 위한 TestEngine을 제공한다.

JUnit Vintage: 플랫폼에서 JUnit 3 및 JUnit 4 기반 테스트를 실행하기 위한 Test Engine을 제공한다. 클래스 경로 또는 모듈 경로에 JUnit 4.12 이상이 있어야 한다.

지원되는 Java 버전

JUnit 5는 런타임에 Java 8(또는 그 이상)이 필요하다. 그러나 이전 버전의 JDK로 컴파일된 코드를 테스트할 수 있다.

테스트 코드 작성 예제

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
    void addition() {
        assertEquals(2, calculator.add(1, 1));
    }

}

JUnit 5 주석

JUnit Jupiter는 테스트를 구성하고 프레임워크를 확장하기 위해 다음 주석을 지원한다.
달리 명시되지 않는 한 모든 핵심 주석은 junit-jupiter-api 모듈의 org.junit.jupiter.api 패키지에 있다.

어너테이션설명
@Test테스트 메서드를 표시합니다. JUnit 4의 @Test와 비슷하지만 속성이 없습니다.
@ParameterizedTest매개변수화된 테스트 메서드를 표시합니다.
@RepeatedTest반복 테스트 템플릿 메서드를 표시합니다.
@TestFactory동적 테스트를 위한 테스트 팩토리 메서드를 표시합니다.
@TestTemplate여러 번 호출할 테스트 케이스 템플릿 메서드를 표시합니다.
@TestClassOrder@Nested 테스트 클래스의 실행 순서를 구성합니다.
@TestMethodOrder주어진 테스트 클래스의 테스트 메서드 실행 순서를 구성합니다.
@TestInstance주어진 테스트 클래스의 테스트 인스턴스 생명주기를 구성합니다.
@DisplayName테스트 클래스나 메서드의 사용자 정의 이름을 선언합니다.
@DisplayNameGeneration테스트 클래스의 사용자 정의 이름 생성기를 선언합니다.
@BeforeEach각 테스트 메서드 전에 실행할 메서드를 표시합니다.
@AfterEach각 테스트 메서드 후에 실행할 메서드를 표시합니다.
@BeforeAll모든 테스트 메서드 전에 실행할 메서드를 표시합니다.
@AfterAll모든 테스트 메서드 후에 실행할 메서드를 표시합니다.
@Nestednon-static 중첩 테스트 클래스를 표시합니다.
@Tag테스트 필터링을 위한 태그를 선언합니다.
@Disabled테스트 클래스나 메서드를 비활성화합니다.
@Timeout실행 시간 초과 시 실패하는 테스트를 표시합니다.
@ExtendWith확장을 선언적으로 등록합니다.
@RegisterExtension필드를 통해 확장을 프로그래밍 방식으로 등록합니다.
@TempDir임시 디렉토리를 주입합니다.

메타 주석 및 구성 주석

Junit Jupiter 주석을 메타 주석으로 사용할 수 있다. 즉, 메타 주석의 의미를 자동으로 상속하는 자신의 구성 주석을 정의할 수 있다.

예를 들어 코드 베이스 전체에 @Tag("Fast")를 복사하여 붙여넣는 대신 다음과 같이 @Fast라는 이름의 사용자 지정 구성 주석을 만들 수 있다. 그러면 @Fast를 @Tag("Fast")의 드롭인 대체물로 사용할 수 있다.

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.junit.jupiter.api.Tag;

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Tag("fast")
public @interface Fast {
}

다음 @Test 방법은 @Fast 주석의 사용법이다.

@Fast
@Test
void myFastTest() {
    // ...
}

@Tag("fast" ) 및 @Test를 대체할 수 있는 사용자 지정 @FastTest 어노테이션을 도입하여 @Tag를 드롭인 방식으로 사용할 수도 있다.

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Tag("fast")
@Test
public @interface FastTest {
}

JUnit은 다음을 "fast"로 태그가 지정된 @Test 메서드로 자동 인식한다.

@FastTest
void myFastTest() {
    // ...
}

정의

플랫폼 개념
컨테이너
다른 컨테이너 또는 테스트를 자식으로 포함하는 테스트 트리의 노드(예: 테스트 클래스)이다.
테스트
실행 시 예상되는 동작을 확인하는 테스트 트리의 노드(예: @Test 메서드).
주피터 개념
라이프사이클 메서드
BeforeAll, @AfterAll, @BeforeEach 또는 @AfterEach로 직접 주석을 달거나 메타 주석이 달린 모든 메서드이다.
테스트 클래스
테스트 메서드를 하나 이상 포함하는 최상위 클래스, 정적 멤버 클래스 또는 @Nested 클래스 (예: 컨테이너)입니다. 테스트 클래스는 추상적이지 않아야 하며 생성자가 하나만 있어야 하다.
테스트 메서드
Test, @RepeatedTest, @ParameterizedTest, @TestFactory 또는 @TestTemplate로 직접 주석을 달거나 메타 주석이 달린 모든 인스턴스 메서드. Test를 제외하고는 테스트 트리에 테스트 또는 잠재적으로 ( @TestFactory의 경우) 다른 컨테이너를 그룹화하는 컨테이너를 생성한다.

테스트 클래스 및 메서드
테스트 메서드와 생명주기 메서드는 현재 테스트 클래스 내에서 로컬로 선언하거나, 수퍼클래스에서 상속하거나, 인터페이스에서 상속할 수 있다( 테스트 인터페이스 및 기본 메서드 참조). 또한 테스트 메서드와 라이프사이클 메서드는 추상적이어서는 안 되며 값을 반환해서는 안된다(값을 반환해야 하는 @TestFactory 메서드 제외).

Display Names

테스트 클래스와 테스트 메서드는 공백, 특수 문자, 이모티콘까지 포함하여 @DisplayName을통해 사용자 지정 표시 이름을 선언할 수 있으며, 이는 테스트 보고서와 테스트 러너 및 IDE에 표시된다.

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

@DisplayName("A special test case")
class DisplayNameDemo {

    @Test
    @DisplayName("Custom test name containing spaces")
    void testWithDisplayNameContainingSpaces() {
    }

    @Test
    @DisplayName("╯°□°)╯")
    void testWithDisplayNameContainingSpecialCharacters() {
    }

    @Test
    @DisplayName("😱")
    void testWithDisplayNameContainingEmoji() {
    }

}

Display Name Generators

JUnit Jupiter는 @DisplayNameGeneration 어노테이션을 통해 구성할 수 있는 사용자 정의 표시 이름 생성기를 지원한다. DisplayName 어노테이션을 통해 제공된 값은 항상 DisplayNameGenerator에서 생성된 표시 이름보다 우선한다.

제너레이터는 DisplayNameGenerator를 구현하여 만들 수 있다. 다음은 Jupiter에서 사용할 수 있는 몇 가지 기본 생성기입니다:

DisplayNameGenerator 동작
StandardJUnit Jupiter 5.0 이후 기본 display name 생성 동작을 따릅니다.
Simple파라미터가 없는 메서드의 경우 괄호를 제거합니다.
ReplaceUnderscores밑줄(_)을 공백으로 대체합니다.
IndicativeSentences테스트와 포함 클래스 이름을 연결하여 전체 문장을 생성합니다.

IndicativeSentences의 경우 다음 예시와 같이 @IndicativeSentencesGeneration을 사용하여 구분 기호 및 기본 생성기를 사용자 지정할 수 있다.

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.DisplayNameGeneration;
import org.junit.jupiter.api.DisplayNameGenerator;
import org.junit.jupiter.api.DisplayNameGenerator.ReplaceUnderscores;
import org.junit.jupiter.api.IndicativeSentencesGeneration;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

class DisplayNameGeneratorDemo {

    @Nested
    @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
    class A_year_is_not_supported {

        @Test
        void if_it_is_zero() {
        }

        @DisplayName("A negative value for year is not supported by the leap year computation.")
        @ParameterizedTest(name = "For example, year {0} is not supported.")
        @ValueSource(ints = { -1, -4 })
        void if_it_is_negative(int year) {
        }

    }

    @Nested
    @IndicativeSentencesGeneration(separator = " -> ", generator = ReplaceUnderscores.class)
    class A_year_is_a_leap_year {

        @Test
        void if_it_is_divisible_by_4_but_not_by_100() {
        }

        @ParameterizedTest(name = "Year {0} is a leap year.")
        @ValueSource(ints = { 2016, 2020, 2048 })
        void if_it_is_one_of_the_following_years(int year) {
        }

    }

}
+-- DisplayNameGeneratorDemo [OK]
  +-- A year is not supported [OK]
  | +-- A negative value for year is not supported by the leap year computation. [OK]
  | | +-- For example, year -1 is not supported. [OK]
  | | '-- For example, year -4 is not supported. [OK]
  | '-- if it is zero() [OK]
  '-- A year is a leap year [OK]
    +-- A year is a leap year -> if it is divisible by 4 but not by 100. [OK]
    '-- A year is a leap year -> if it is one of the following years. [OK]
      +-- Year 2016 is a leap year. [OK]
      +-- Year 2020 is a leap year. [OK]
      '-- Year 2048 is a leap year. [OK]

Setting the Default Display Name Generator

junit.jupiter.displayname.generator.default 구성 매개 변수를 사용하여 기본적으로 사용하려는 DisplayNameGenerator의 정규화된 클래스 이름을 지정할 수 있다. DisplayNameGeneration 어노테이션을 통해 구성된 표시 이름 생성기의 경우와 마찬가지로, 제공된 클래스는 DisplayNameGenerator 인터페이스를 구현해야 한다. 둘러싸는 테스트 클래스 또는 테스트 인터페이스에 @DisplayNameGeneration 어노테이션이 없는 한 기본 표시 이름 생성기는 모든 테스트에 사용된다. DisplayName 어노테이션을 통해 제공된 값은 항상 DisplayNameGenerator에서 생성된 표시 이름보다 우선한다.
예를 들어 ReplaceUnderscores 표시 이름 생성기를 기본적으로 사용하려면 구성 매개변수를 해당 정규화된 클래스 이름으로 설정해야 한다(예: src/test/resources/junit-platform.properties에서):

junit.jupiter.displayname.generator.default = \
    org.junit.jupiter.api.DisplayNameGenerator$ReplaceUnderscores

마찬가지로 DisplayNameGenerator를 구현하는 모든 사용자 정의 클래스의 정규화된 이름을 지정할 수 있다.
요약하면, 테스트 클래스 또는 메서드의 표시 이름은 다음 우선순위 규칙에 따라 결정된다:
DisplayName 어노테이션의 값(있는 경우)
DisplayNameGeneration 어노테이션에 지정된 DisplayNameGenerator를 호출하여(있는 경우).
구성 매개 변수를 통해 구성된 기본 DisplayNameGenerator를 호출하여(있는 경우)
org.junit.jupiter.api.DisplayNameGenerator.Standard를 호출한다.

Assertions

JUnit Jupiter는 JUnit 4에 있는 많은 어설션 메서드와 함께 제공되며, Java 8 람다와 함께 사용하기에 적합한 몇 가지 메서드가 추가되었다. 모든 JUnit Jupiter 어설션은 org.junit.jupiter.api.Assertions 클래스에 있는 정적 메서드이다.

import static java.time.Duration.ofMillis;
import static java.time.Duration.ofMinutes;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTimeout;
import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.concurrent.CountDownLatch;

import example.domain.Person;
import example.util.Calculator;

import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;

class AssertionsDemo {

    private final Calculator calculator = new Calculator();

    private final Person person = new Person("Jane", "Doe");

    @Test
    void standardAssertions() {
        assertEquals(2, calculator.add(1, 1));
        assertEquals(4, calculator.multiply(2, 2),
                "The optional failure message is now the last parameter");
        assertTrue('a' < 'b', () -> "Assertion messages can be lazily evaluated -- "
                + "to avoid constructing complex messages unnecessarily.");
    }

    @Test
    void groupedAssertions() {
        // In a grouped assertion all assertions are executed, and all
        // failures will be reported together.
        assertAll("person",
            () -> assertEquals("Jane", person.getFirstName()),
            () -> assertEquals("Doe", person.getLastName())
        );
    }

    @Test
    void dependentAssertions() {
        // Within a code block, if an assertion fails the
        // subsequent code in the same block will be skipped.
        assertAll("properties",
            () -> {
                String firstName = person.getFirstName();
                assertNotNull(firstName);

                // Executed only if the previous assertion is valid.
                assertAll("first name",
                    () -> assertTrue(firstName.startsWith("J")),
                    () -> assertTrue(firstName.endsWith("e"))
                );
            },
            () -> {
                // Grouped assertion, so processed independently
                // of results of first name assertions.
                String lastName = person.getLastName();
                assertNotNull(lastName);

                // Executed only if the previous assertion is valid.
                assertAll("last name",
                    () -> assertTrue(lastName.startsWith("D")),
                    () -> assertTrue(lastName.endsWith("e"))
                );
            }
        );
    }

    @Test
    void exceptionTesting() {
        Exception exception = assertThrows(ArithmeticException.class, () ->
            calculator.divide(1, 0));
        assertEquals("/ by zero", exception.getMessage());
    }

    @Test
    void timeoutNotExceeded() {
        // The following assertion succeeds.
        assertTimeout(ofMinutes(2), () -> {
            // Perform task that takes less than 2 minutes.
        });
    }

    @Test
    void timeoutNotExceededWithResult() {
        // The following assertion succeeds, and returns the supplied object.
        String actualResult = assertTimeout(ofMinutes(2), () -> {
            return "a result";
        });
        assertEquals("a result", actualResult);
    }

    @Test
    void timeoutNotExceededWithMethod() {
        // The following assertion invokes a method reference and returns an object.
        String actualGreeting = assertTimeout(ofMinutes(2), AssertionsDemo::greeting);
        assertEquals("Hello, World!", actualGreeting);
    }

    @Test
    void timeoutExceeded() {
        // The following assertion fails with an error message similar to:
        // execution exceeded timeout of 10 ms by 91 ms
        assertTimeout(ofMillis(10), () -> {
            // Simulate task that takes more than 10 ms.
            Thread.sleep(100);
        });
    }

    @Test
    void timeoutExceededWithPreemptiveTermination() {
        // The following assertion fails with an error message similar to:
        // execution timed out after 10 ms
        assertTimeoutPreemptively(ofMillis(10), () -> {
            // Simulate task that takes more than 10 ms.
            new CountDownLatch(1).await();
        });
    }

    private static String greeting() {
        return "Hello, World!";
    }

}

Assumptions

JUnit Jupiter는 JUnit 4가 제공하는 가정 메서드의 하위 집합과 함께 제공되며, Java 8 람다 표현식 및 메서드 참조와 함께 사용하기에 적합한 몇 가지 메서드를 추가한다. 모든 JUnit Jupiter 가정은 org.junit.jupiter.api.Assumptions 클래스에 있는 정적 메서드이다.

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
import static org.junit.jupiter.api.Assumptions.assumingThat;

import example.util.Calculator;

import org.junit.jupiter.api.Test;

class AssumptionsDemo {

    private final Calculator calculator = new Calculator();

    @Test
    void testOnlyOnCiServer() {
        assumeTrue("CI".equals(System.getenv("ENV")));
        // remainder of test
    }

    @Test
    void testOnlyOnDeveloperWorkstation() {
        assumeTrue("DEV".equals(System.getenv("ENV")),
            () -> "Aborting test: not on developer workstation");
        // remainder of test
    }

    @Test
    void testInAllEnvironments() {
        assumingThat("CI".equals(System.getenv("ENV")),
            () -> {
                // perform these assertions only on the CI server
                assertEquals(2, calculator.divide(4, 2));
            });

        // perform these assertions in all environments
        assertEquals(42, calculator.multiply(6, 7));
    }

}

Disabling

전체 테스트 클래스 또는 개별 테스트 메서드는 @Disabled 어노테이션, 조건부 테스트 실행에 설명된 어노테이션 중 하나 또는 사용자 지정 ExecutionCondition을 통해 비활성화할 수 있다.
다음은 @Disabled 테스트 클래스이다.

import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;

@Disabled("Disabled until bug #99 has been fixed")
class DisabledClassDemo {

    @Test
    void testWillBeSkipped() {
    }

}

다음은 @Disabled 테스트 메서드가 포함된 테스트 클래스이다.

import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;

class DisabledTestsDemo {

    @Disabled("Disabled until bug #42 has been resolved")
    @Test
    void testWillBeSkipped() {
    }

    @Test
    void testWillBeExecuted() {
    }

}

마무리

오픈소스 코드를 공부하기위해서 Junit5를 개념정리를 하는 시간을 갖을 수 있었지만 아직도 어렵게 느껴지는것같다. 오픈소스를 제대로 기여를 할 수 있을지 모르겠지만 열심히해볼려고한다.

참조

[1] https://junit.org/junit5/docs/current/user-guide/

profile
Junior backend developer

0개의 댓글