[번역] JUnit5 공식문서 - Writing Tests

공혁준·2022년 3월 21일
0
post-thumbnail

Ref. JUnit5 User Guide v.5.8.2
📌 이 글에서는 공식문서의 Writing Tests 부분만 다룹니다.

Writing Tests

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

}

Annotations

JUnit Jupiter는 테스트 구성 및 프레임워크 확장을 위해 다음 어노테이션을 지원합니다.

달리 명시되지 않는 한 모든 핵심 어노테이션은 junit-jupiter-api 모듈의 org.junit.jupiter.api 패키지에 있습니다.

AnnotationDescription
@Test메소드가 테스트 메소드임을 나타냅니다. JUnit 4의 @Test 어노테이션과 달리 이 어노테이션은 속성을 선언하지 않습니다. JUnit Jupiter의 테스트 확장은 자체 전용 어노테이션을 기반으로 작동하기 때문입니다. 이러한 메서드는 재정의되지 않는 한 상속됩니다.
@ParameterizedTest메소드가 매개변수화된 테스트임을 나타냅니다. 이러한 메서드는 재정의되지 않는 한 상속됩니다.
@RepeatedTest메소드가 반복 테스트를 위한 테스트 템플릿임을 나타냅니다. 이러한 메서드는 재정의되지 않는 한 상속됩니다.
@TestFactory메소드가 동적 테스트를 위한 테스트 팩토리임을 나타냅니다. 이러한 메서드는 재정의되지 않는 한 상속됩니다.
@TestTemplate메서드는 등록된 공급자가 반환하는 호출 컨텍스트의 수에 따라 여러 번 호출되도록 설계된 테스트 케이스의 템플릿임을 나타냅니다. 이러한 메서드는 재정의되지 않는 한 상속됩니다.
@TestClassOrder어노테이션이 달린 테스트 클래스에서 @Nested 테스트 클래스에 대한 테스트 클래스 실행 순서를 구성하는 데 사용됩니다. 이러한 어노테이션은 상속됩니다.
@TestMethodOrder어노테이션이 달린 테스트 클래스에 대한 테스트 메서드 실행 순서를 구성하는 데 사용됩니다. JUnit 4의 @FixMethodOrder와 유사합니다. 이러한 어노테이션은 상속됩니다.
@TestInstance어노테이션이 달린 테스트 클래스의 테스트 인스턴스 수명 주기를 구성하는 데 사용됩니다. 이러한 어노테이션은 상속됩니다.
@DisplayName테스트 클래스 또는 테스트 메서드에 대한 사용자 지정 표시 이름을 선언합니다. 이러한 어노테이션은 상속되지 않습니다.
@DisplayNameGeneration테스트 클래스에 대한 사용자 지정 표시 이름 생성기를 선언합니다. 이러한 어노테이션은 상속됩니다.
@BeforeEach현재 클래스의 각 @Test, @RepeatedTest, @ParameterizedTest 또는 @TestFactory 메서드보다 먼저 어노테이션이 달린 메서드가 실행되어야 함을 나타냅니다. JUnit 4의 @Before와 유사합니다. 이러한 메서드는 재정의되지 않는 한 상속됩니다.
@AfterEach현재 클래스의 각 @Test, @RepeatedTest, @ParameterizedTest 또는 @TestFactory 메서드 후에 어노테이션이 달린 메서드가 실행되어야 함을 나타냅니다. JUnit 4의 @After와 유사합니다. 이러한 메서드는 재정의되지 않는 한 상속됩니다.
@BeforeAll현재 클래스의 모든 @Test, @RepeatedTest, @ParameterizedTest@TestFactory 메서드보다 먼저 어노테이션이 달린 메서드를 실행해야 함을 나타냅니다. JUnit 4의 @BeforeClass와 유사합니다. 이러한 메서드는 상속되며(숨겨지거나 재정의되지 않는 한) 정적이어야 합니다("클래스별" 테스트 인스턴스 수명 주기가 사용되지 않는 한).
@AfterAll현재 클래스의 모든 @Test, @RepeatedTest, @ParameterizedTest@TestFactory 메서드 후에 어노테이션이 달린 메서드가 실행되어야 함을 나타냅니다. JUnit 4의 @AfterClass와 유사합니다. 이러한 메서드는 상속되며(숨겨지거나 재정의되지 않는 한) 정적이어야 합니다("클래스별" 테스트 인스턴스 수명 주기가 사용되지 않는 한).
@Nested어노테이션이 달린 클래스는 정적이 아닌 중첩 테스트 클래스임을 나타냅니다. @BeforeAll@AfterAll 메서드는 "클래스별" 테스트 인스턴스 수명 주기가 사용되지 않는 한 @Nested 테스트 클래스에서 직접 사용할 수 없습니다. 이러한 어노테이션은 상속되지 않습니다.
@Tag클래스 또는 메서드 수준에서 테스트 필터링을 위한 태그를 선언하는 데 사용됩니다. TestNG의 테스트 그룹이나 JUnit 4의 카테고리와 유사합니다. 이러한 어노테이션은 클래스 수준에서 상속되지만 메서드 수준에서는 상속되지 않습니다.
@Disabled테스트 클래스 또는 테스트 메서드를 비활성화하는 데 사용됩니다. JUnit 4의 @Ignore와 유사합니다. 이러한 어노테이션은 상속되지 않습니다.
@Timeout실행이 지정된 기간을 초과하는 경우 테스트, 테스트 팩토리, 테스트 템플릿 또는 수명 주기 메서드를 실패하는 데 사용됩니다. 이러한 어노테이션은 상속됩니다.
@ExtendWith확장을 선언적으로 등록하는 데 사용됩니다. 이러한 어노테이션은 상속됩니다.
@RegisterExtension필드를 통해 프로그래밍 방식으로 확장을 등록하는 데 사용됩니다. 이러한 필드는 음영 처리되지 않는 한 상속됩니다.
@TempDir라이프 사이클 방법 또는 테스트 방법에서 필드 주입 또는 매개 변수 주입을 통해 임시 디렉토리를 제공하는 데 사용됩니다. org.junit.jupiter.api.io 패키지에 있습니다.

Test Classes and Methods

Test Class: 모든 최상위 클래스, 정적 멤버 클래스 또는 하나 이상의 테스트 메서드를 포함하는 @Nested 클래스.

테스트 클래스는 abstract가 아니어야 하며 단일 생성자가 있어야 합니다.

Test Method: @Test, @RepeatedTest, @ParameterizedTest, @TestFactory 또는 @TestTemplate으로 직접 어노테이션 처리되거나 메타 어노테이션 처리된 모든 인스턴스 메소드.

Lifecycle Method: @BeforeAll, @AfterAll, @BeforeEach 또는 @AfterEach로 직접 어노테이션 처리되거나 메타 어노테이션 처리된 모든 방법.

테스트 메서드 및 수명 주기 메서드는 현재 테스트 클래스 내에서 로컬로 선언되거나 슈퍼클래스에서 상속되거나 인터페이스에서 상속될 수 있습니다(테스트 인터페이스 및 기본 메서드 참조). 또한 테스트 메서드 및 수명 주기 메서드는 abstract가 아니어야 하며 값을 반환하지 않아야 합니다(값을 반환하는 데 필요한 @TestFactory 메서드 제외).

다음 테스트 클래스는 @Test 메서드와 지원되는 모든 수명 주기 메서드의 사용을 보여줍니다.

  • 표준 테스트 클래스
import static org.junit.jupiter.api.Assertions.fail;
import static org.junit.jupiter.api.Assumptions.assumeTrue;

import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;

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() {
    }

}

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() {
    }

}

Assertions

JUnit Jupiter는 JUnit 4에 있는 많은 assertion 메서드와 함께 제공되며 Java 8 람다와 함께 사용하기에 적합한 몇 가지를 추가합니다. 모든 JUnit Jupiter assertions는 org.junit.jupiter.api.Assertions 클래스의 static 메서드입니다.

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.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!";
    }

}

Third-party Assertion Libraries

JUnit Jupiter에서 제공하는 assertion 기능은 많은 테스트 시나리오에 충분하지만 더 많은 성능과 매처와 같은 추가 기능이 필요하거나 필요할 때가 있습니다. 이러한 경우 JUnit 팀은 AssertJ, Hamcrest, Truth 등과 같은 타사 assertion 라이브러리의 사용을 권장합니다. 따라서 개발자는 선택한 assertion 라이브러리를 자유롭게 사용할 수 있습니다.

예를 들어, matchers와 fluent API의 조합을 사용하여 assertion을 보다 설명적이고 읽기 쉽게 만들 수 있습니다. 그러나 JUnit Jupiter의 org.junit.jupiter.api.Assertions 클래스는 Hamcrest Matcher를 허용하는 JUnit 4의 org.junit.Assert 클래스에서 볼 수 있는 것과 같은 assertThat() 메서드를 제공하지 않습니다. 대신 개발자는 타사 assertion 라이브러리에서 제공하는 매처에 대한 기본 제공 지원을 사용하는 것이 좋습니다.

다음 예제는 JUnit Jupiter 테스트에서 Hamcrest의 assertThat() 지원을 사용하는 방법을 보여줍니다. Hamcrest 라이브러리가 클래스 경로에 추가되어 있으면 assertThat(), is()equalTo()와 같은 메서드를 정적으로 가져온 다음 아래의 assertWithHamcrestMatcher() 메서드와 같은 테스트에서 사용할 수 있습니다.

import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.MatcherAssert.assertThat;

import example.util.Calculator;

import org.junit.jupiter.api.Test;

class HamcrestAssertionsDemo {

    private final Calculator calculator = new Calculator();

    @Test
    void assertWithHamcrestMatcher() {
        assertThat(calculator.subtract(4, 1), is(equalTo(3)));
    }

}

당연히 JUnit 4 프로그래밍 모델을 기반으로 하는 레거시 테스트는 org.junit.Assert#assertThat을 계속 사용할 수 있습니다.

Assumptions

JUnit Jupiter는 JUnit 4가 제공하는 assumption 메서드의 하위 집합과 함께 제공되며 Java 8 람다 식 및 메서드 참조와 함께 사용하기에 적합한 몇 가지를 추가합니다. 모든 JUnit Jupiter assumption은 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 Tests

전체 테스트 클래스 또는 개별 테스트 메서드는 @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() {
    }

}

Conditional Test Execution

JUnit Jupiter의 ExecutionCondition extension API를 사용하면 개발자가 컨테이너를 활성화 또는 비활성화하거나 특정 조건을 기반으로 프로그래밍 방식으로 테스트할 수 있습니다. 이러한 조건의 가장 간단한 예는 @Disabled 어노테이션을 지원하는 내장 DisabledCondition입니다. @Disabled 외에도 JUnit Jupiter는 org.junit.jupiter.api.condition 패키지에서 개발자가 선언적으로 컨테이너 및 테스트를 활성화 또는 비활성화할 수 있는 여러 다른 어노테이션 기반 조건을 지원합니다. 여러 ExecutionCondition extensions이 등록되면 조건 중 하나가 비활성화됨을 반환하는 즉시 컨테이너 또는 테스트가 비활성화됩니다. 비활성화될 수 있는 이유에 대한 세부 정보를 제공하려는 경우 이러한 기본 제공 조건과 관련된 모든 어노테이션에는 해당 용도로 사용할 수 있는 disabledReason 속성이 있습니다.

Operating System Conditions

컨테이너 또는 테스트는 @EnabledOnOs@DisabledOnOs 어노테이션을 통해 특정 운영 체제에서 활성화 또는 비활성화할 수 있습니다.

@Test
@EnabledOnOs(MAC)
void onlyOnMacOs() {
    // ...
}

@TestOnMac
void testOnMac() {
    // ...
}

@Test
@EnabledOnOs({ LINUX, MAC })
void onLinuxOrMac() {
    // ...
}

@Test
@DisabledOnOs(WINDOWS)
void notOnWindows() {
    // ...
}

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Test
@EnabledOnOs(MAC)
@interface TestOnMac {
}

Java Runtime Environment Conditions

@EnabledOnJre@DisabledOnJre 어노테이션을 통해 JRE(Java Runtime Environment)의 특정 버전에서 또는 @EnabledForJreRange@DisabledForJreRange 어노테이션을 통해 JRE의 특정 버전 범위에서 컨테이너 또는 테스트를 활성화하거나 비활성화할 수 있습니다. 범위의 기본값은 JRE.JAVA_8이 아래쪽 경계(최소)이고 JRE.OTHER가 위쪽 경계(최대)로, 절반의 열린 범위를 사용할 수 있습니다.

@Test
@EnabledOnJre(JAVA_8)
void onlyOnJava8() {
    // ...
}

@Test
@EnabledOnJre({ JAVA_9, JAVA_10 })
void onJava9Or10() {
    // ...
}

@Test
@EnabledForJreRange(min = JAVA_9, max = JAVA_11)
void fromJava9to11() {
    // ...
}

@Test
@EnabledForJreRange(min = JAVA_9)
void fromJava9toCurrentJavaFeatureNumber() {
    // ...
}

@Test
@EnabledForJreRange(max = JAVA_11)
void fromJava8To11() {
    // ...
}

@Test
@DisabledOnJre(JAVA_9)
void notOnJava9() {
    // ...
}

@Test
@DisabledForJreRange(min = JAVA_9, max = JAVA_11)
void notFromJava9to11() {
    // ...
}

@Test
@DisabledForJreRange(min = JAVA_9)
void notFromJava9toCurrentJavaFeatureNumber() {
    // ...
}

@Test
@DisabledForJreRange(max = JAVA_11)
void notFromJava8to11() {
    // ...
}

Environment Variable Conditions

@EnabledIfEnvironmentVariable@DisabledIfEnvironmentVariable 어노테이션을 통해 기본 운영 체제의 named 환경 변수 값을 기반으로 컨테이너 또는 테스트를 활성화 또는 비활성화할 수 있습니다. matches 속성을 통해 제공된 값은 정규식으로 해석됩니다.

@Test
@EnabledIfEnvironmentVariable(named = "ENV", matches = "staging-server")
void onlyOnStagingServer() {
    // ...
}

@Test
@DisabledIfEnvironmentVariable(named = "ENV", matches = ".*development.*")
void notOnDeveloperWorkstation() {
    // ...
}

Tagging and Filtering

@Tag 어노테이션을 통해 테스트 클래스와 메소드에 태그를 지정할 수 있습니다. 이러한 태그는 나중에 테스트 검색 및 실행을 필터링하는 데 사용할 수 있습니다.

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

@Tag("fast")
@Tag("model")
class TaggingDemo {

    @Test
    @Tag("taxes")
    void testingTaxCalculation() {
    }

}

Test Execution Order

기본적으로 테스트 클래스와 메서드는 결정적이지만 의도적으로 명확하지 않은 알고리즘을 사용하여 정렬됩니다. 이것은 테스트 스위트의 후속 실행이 동일한 순서로 테스트 클래스와 테스트 메소드를 실행하도록 하여 반복 가능한 빌드를 허용합니다.

Method Order

실제 단위 테스트는 일반적으로 실행 순서에 의존하지 않아야 하지만 특정 테스트 메서드 실행 순서를 적용해야 하는 경우가 있습니다. 예를 들어 통합 테스트나 기능 테스트를 작성할 때 테스트 순서가 특히 @TestInstance(Lifecycle.PER_CLASS)와 함께 사용하면 중요합니다.

테스트 메소드가 실행되는 순서를 제어하려면 @TestMethodOrder로 테스트 클래스 또는 테스트 인터페이스에 어노테이션을 달고 원하는 MethodOrderer 구현을 지정하십시오. 사용자 정의 MethodOrderer를 구현하거나 다음 기본 제공 MethodOrderer 구현 중 하나를 사용할 수 있습니다.

  • MethodOrderer.DisplayName: 표시 이름에 따라 테스트 방법을 영숫자순으로 정렬합니다(표시 이름 생성 우선 순위 규칙 참조).
  • MethodOrderer.MethodName: 이름과 형식 매개변수 목록을 기반으로 테스트 방법을 영숫자순으로 정렬합니다.
  • MethodOrderer.OrderAnnotation: @Order 어노테이션을 통해 지정된 값을 기반으로 테스트 메서드를 숫자로 정렬합니다.
  • MethodOrderer.Random: 테스트 방법을 의사 무작위로 주문하고 사용자 지정 시드 구성을 지원합니다.
  • MethodOrderer.Alphanumeric: 이름과 형식 매개변수 목록을 기반으로 테스트 방법을 영숫자순으로 정렬합니다. 6.0에서 제거될 MethodOrderer.MethodName을 위해 더 이상 사용되지 않습니다.

다음 예제는 @Order 어노테이션을 통해 지정된 순서대로 테스트 메서드가 실행되도록 보장하는 방법을 보여줍니다.

import org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;

@TestMethodOrder(OrderAnnotation.class)
class OrderedTestsDemo {

    @Test
    @Order(1)
    void nullValues() {
        // perform assertions against null values
    }

    @Test
    @Order(2)
    void emptyValues() {
        // perform assertions against empty values
    }

    @Test
    @Order(3)
    void validValues() {
        // perform assertions against valid values
    }

}

Class Order

테스트 클래스는 일반적으로 실행 순서에 의존하지 않아야 하지만 특정 테스트 클래스 실행 순서를 적용하는 것이 바람직한 경우가 있습니다. 테스트 클래스 간에 우발적인 종속성이 없는지 확인하기 위해 임의의 순서로 테스트 클래스를 실행하거나 다음 시나리오에 설명된 대로 빌드 시간을 최적화하기 위해 테스트 클래스를 주문할 수 있습니다.

  • 이전에 실패한 테스트와 더 빠른 테스트를 먼저 실행: "fail fast" 모드
  • 병렬 실행이 활성화된 상태에서 더 긴 테스트를 먼저 실행: "shortest test plan execution duration" 모드
  • 기타 다양한 사용 사례

전체 테스트 스위트에 대해 테스트 클래스 실행 순서를 전역적으로 구성하려면 junit.jupiter.testclass.order.default 구성 매개변수를 사용하여 사용하려는 ClassOrderer의 정규화된 클래스 이름을 지정합니다. 제공된 클래스는 ClassOrderer 인터페이스를 구현해야 합니다.

사용자 정의 ClassOrderer를 구현하거나 다음 내장 ClassOrderer 구현 중 하나를 사용할 수 있습니다.

  • ClassOrderer.ClassName: 정규화된 클래스 이름을 기반으로 테스트 클래스를 영숫자순으로 정렬합니다.
  • ClassOrderer.DisplayName: 표시 이름에 따라 테스트 클래스를 영숫자순으로 정렬합니다.
  • ClassOrderer.OrderAnnotation: @Order 어노테이션을 통해 지정된 값을 기반으로 테스트 클래스를 숫자로 정렬합니다.
  • ClassOrderer.Random: 테스트 클래스를 의사 무작위로 주문하고 사용자 지정 시드 구성을 지원합니다.

예를 들어 @Order 어노테이션이 테스트 클래스에 적용되려면 해당하는 정규화된 클래스 이름이 있는 구성 매개변수를 사용하여 ClassOrderer.OrderAnnotation 클래스 순서 지정자를 구성해야 합니다(예: src/test/resources/junit-platform.properties에서).

junit.jupiter.testclass.order.default = \
    org.junit.jupiter.api.ClassOrderer$OrderAnnotation

구성된 ClassOrderer는 모든 최상위 테스트 클래스(정적 중첩 테스트 클래스 포함) 및 @Nested 테스트 클래스에 적용됩니다.

@Nested 테스트 클래스에 대해 로컬에서 테스트 클래스 실행 순서를 구성하려면 @Nested 테스트 클래스에 대한 엔클로징 클래스에 @TestClassOrder 어노테이션을 선언하고 직접 사용하려는 ClassOrderer 구현에 대한 클래스 참조를 제공하십시오. 구성된 ClassOrderer@Nested 테스트 클래스와 @Nested 테스트 클래스에 재귀적으로 적용됩니다. 로컬 @TestClassOrder 선언은 항상 상속된 @TestClassOrder 선언 또는 junit.jupiter.testclass.order.default 구성 매개변수를 통해 전역적으로 구성된 ClassOrderer를 재정의합니다.

다음 예제는 @Nested 테스트 클래스가 @Order 어노테이션을 통해 지정된 순서대로 실행되도록 보장하는 방법을 보여줍니다.

import org.junit.jupiter.api.ClassOrderer;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestClassOrder;

@TestClassOrder(ClassOrderer.OrderAnnotation.class)
class OrderedNestedTestClassesDemo {

    @Nested
    @Order(1)
    class PrimaryTests {

        @Test
        void test1() {
        }
    }

    @Nested
    @Order(2)
    class SecondaryTests {

        @Test
        void test2() {
        }
    }
}

Test Instance Lifecycle

개별 테스트 메서드를 격리하여 실행할 수 있도록 하고 변경 가능한 테스트 인스턴스 상태로 인한 예기치 않은 부작용을 피하기 위해 JUnit은 각 테스트 메서드를 실행하기 전에 각 테스트 클래스의 새 인스턴스를 만듭니다. 이 "per-method" 테스트 인스턴스 수명 주기는 JUnit Jupiter의 기본 동작이며 모든 이전 버전의 JUnit과 유사합니다.

JUnit Jupiter가 동일한 테스트 인스턴스에서 모든 테스트 메서드를 실행하도록 하려면 테스트 클래스에 @TestInstance(Lifecycle.PER_CLASS) 어노테이션을 추가하세요. 이 모드를 사용할 때 새 테스트 인스턴스는 테스트 클래스당 한 번 생성됩니다. 따라서 테스트 메서드가 인스턴스 변수에 저장된 상태에 의존하는 경우 @BeforeEach 또는 @AfterEach 메서드에서 해당 상태를 재설정해야 할 수 있습니다.

"per-class" 모드는 기본 "per-method" 모드에 비해 몇 가지 추가 이점이 있습니다. 특히 "per-class" 모드를 사용하면 비정적 메서드와 인터페이스 기본 메서드에서 @BeforeAll@AfterAll을 선언할 수 있습니다. 따라서 "per-class" 모드를 사용하면 @Nested 테스트 클래스에서 @BeforeAll@AfterAll 메서드를 사용할 수도 있습니다.

Kotlin 프로그래밍 언어를 사용하여 테스트를 작성하는 경우 "per-class" 테스트 인스턴스 수명 주기 모드로 전환하여 @BeforeAll@AfterAll 메서드를 더 쉽게 구현할 수도 있습니다.

Nested Tests

@Nested 테스트는 테스트 작성자에게 여러 테스트 그룹 간의 관계를 표현할 수 있는 더 많은 기능을 제공합니다. 이러한 중첩 테스트는 Java의 중첩 클래스를 사용하고 테스트 구조에 대한 계층적 사고를 용이하게 합니다. 다음은 소스 코드와 IDE 내 실행의 스크린샷으로 된 정교한 예입니다.

  • 스택 테스트를 위한 중첩 테스트 모음
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.EmptyStackException;
import java.util.Stack;

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

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

IDE에서 이 예제를 실행할 때 GUI의 테스트 실행 트리는 다음 이미지와 유사하게 보일 것입니다.

Dependency Injection for Constructors and Methods

모든 이전 JUnit 버전에서 테스트 생성자 또는 메소드에는 매개변수가 허용되지 않았습니다(적어도 표준 Runner 구현에서는 허용되지 않음). JUnit Jupiter의 주요 변경 사항 중 하나로 이제 테스트 생성자와 메서드 모두 매개변수를 가질 수 있습니다. 이것은 더 큰 유연성을 허용하고 생성자와 메소드에 대한 종속성 주입을 가능하게 합니다.

ParameterResolver는 런타임에 매개변수를 동적으로 확인하려는 테스트 확장을 위한 API를 정의합니다. 테스트 클래스 생성자, 테스트 메서드 또는 수명 주기 메서드(테스트 클래스 및 메서드 참조)가 매개변수를 수락하는 경우 매개변수는 등록된 ParameterResolver에 의해 런타임에 확인되어야 합니다.

현재 자동으로 등록되는 3개의 내장 리졸버가 있습니다.

  • TestInfoParameterResolver: TestInfoParameterResolver는 현재 컨테이너에 해당하는 TestInfo의 인스턴스를 제공하거나 매개변수의 값으로 테스트합니다. 그런 다음 TestInfo를 사용하여 표시 이름, 테스트 클래스, 테스트 메서드 및 관련 태그와 같은 현재 컨테이너 또는 테스트에 대한 정보를 검색할 수 있습니다. 표시 이름은 테스트 클래스 또는 테스트 메서드의 이름과 같은 기술적인 이름이거나 @DisplayName을 통해 구성된 사용자 지정 이름입니다.

TestInfo는 JUnit 4의 TestName 규칙에 대한 드롭인 대체 역할을 합니다. 다음은 TestInfo를 테스트 생성자, @BeforeEach 메소드 및 @Test 메소드에 주입하는 방법을 보여줍니다.

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.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() {
    }

}
  • RepetitionInfoParameterResolver: @RepeatedTest, @BeforeEach 또는 @AfterEach 메서드의 메서드 매개 변수가 RepetitionInfo 유형인 경우 RepetitionInfoParameterResolverRepetitionInfo의 인스턴스를 제공합니다. 그런 다음 RepetitionInfo를 사용하여 현재 반복 및 해당 @RepeatedTest에 대한 총 반복 횟수에 대한 정보를 검색할 수 있습니다. 그러나 RepetitionInfoParameterResolver@RepeatedTest 컨텍스트 외부에 등록되지 않습니다.
  • TestReporterParameterResolver: 생성자 또는 메소드 매개변수가 TestReporter 유형인 경우 TestReporterParameterResolverTestReporter의 인스턴스를 제공합니다. TestReporter는 현재 테스트 실행에 대한 추가 데이터를 게시하는 데 사용할 수 있습니다. 데이터는 TestExecutionListenerReportingEntryPublished() 메서드를 통해 사용되어 IDE에서 보거나 보고서에 포함될 수 있습니다.

JUnit Jupiter에서는 JUnit 4에서 stdout 또는 stderr로 정보를 인쇄하는 데 사용한 TestReporter를 사용해야 합니다. @RunWith(JUnitPlatform.class)를 사용하면 보고된 모든 항목이 stdout으로 출력됩니다. 또한 일부 IDE는 보고서 항목을 stdout으로 인쇄하거나 테스트 결과를 위해 사용자 인터페이스에 표시합니다.

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

}

Test Interfaces and Default Methods

JUnit Jupiter는 @Test, @RepeatedTest, @ParameterizedTest, @TestFactory, @TestTemplate, @BeforeEach@AfterEach가 인터페이스 기본 메소드에 선언되도록 허용합니다. @BeforeAll@AfterAll은 테스트 인터페이스의 정적 메서드 또는 테스트 인터페이스 또는 테스트 클래스에 @TestInstance(Lifecycle.PER_CLASS) 어노테이션이 달린 경우 인터페이스 기본 메서드에서 선언할 수 있습니다. 여기 예시들이 있습니다.

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

}

Repeated Tests

JUnit Jupiter는 @RepeatedTest로 메서드에 어노테이션을 달고 원하는 총 반복 횟수를 지정하여 지정된 횟수만큼 테스트를 반복할 수 있는 기능을 제공합니다. 반복되는 테스트의 각 호출은 동일한 수명 주기 콜백 및 확장을 완벽하게 지원하는 일반 @Test 메서드의 실행처럼 작동합니다.

다음 예제는 자동으로 10번 반복되는 repeatTest()라는 테스트를 선언하는 방법을 보여줍니다.

@RepeatedTest(10)
void repeatedTest() {
    // ...
}

Parameterized Tests

Parameterized test를 사용하면 다른 인수로 테스트를 여러 번 실행할 수 있습니다. 일반 @Test 메소드처럼 선언되지만 대신 @ParameterizedTest 어노테이션을 사용합니다. 또한 각 호출에 대한 인수를 제공한 다음 테스트 메서드에서 인수를 사용할 소스를 하나 이상 선언해야 합니다.

다음 예제는 @ValueSource 어노테이션을 사용하여 String 배열을 인수의 소스로 지정하는 매개변수화된 테스트를 보여줍니다.

@ParameterizedTest
@ValueSource(strings = { "racecar", "radar", "able was I ere I saw elba" })
void palindromes(String candidate) {
    assertTrue(StringUtils.isPalindrome(candidate));
}

Timeouts

@Timeout 어노테이션을 사용하면 실행 시간이 주어진 기간을 초과하는 경우 테스트, 테스트 팩토리, 테스트 템플릿 또는 수명 주기 메서드가 실패해야 한다고 선언할 수 있습니다. 지속 시간의 시간 단위는 기본적으로 초로 설정되지만 구성할 수 있습니다.

다음 예제는 @Timeout이 수명 주기 및 테스트 메서드에 어떻게 적용되는지 보여줍니다.

class TimeoutDemo {

    @BeforeEach
    @Timeout(5)
    void setUp() {
        // fails if execution time exceeds 5 seconds
    }

    @Test
    @Timeout(value = 100, unit = TimeUnit.MILLISECONDS)
    void failsIfExecutionTimeExceeds100Milliseconds() {
        // fails if execution time exceeds 100 milliseconds
    }

}
profile
몰입을 즐기는 개발자입니다.

0개의 댓글