JUnit

Junit5 공식문서 정리 및 실습_2(테스트)

nick-p 2024. 11. 18. 09:04

5.3. 테스트 클래스 및 메서드

테스트 메서드와 라이프사이클 메서드는 현재 테스트 클래스 내에서 로컬로 선언되거나, 슈퍼클래스에서 상속되거나, 인터페이스에서 상속될 수 있습니다( 테스트 인터페이스 및 기본 메서드 참조 ). 또한 테스트 메서드와 라이프사이클 메서드는 abstract값을 반환해서는 안 되며 반환해서도 안 됩니다( @TestFactory 값을 반환해야 하는 메서드 제외).

 

클래스 및 메서드 가시성
테스트 클래스, 테스트 메서드, 수명 주기 메서드는 public반드시 그럴 필요는 없지만, 그럴 필요  없습니다private .
일반적으로 테스트 클래스, 테스트 메서드, 라이프사이클 메서드에 대한 수정자를 생략하는 것이 좋습니다 public. 단, 그렇게 하는 데 기술적 이유가 있는 경우는 예외입니다. 예를 들어, 테스트 클래스가 다른 패키지의 테스트 클래스에 의해 확장되는 경우입니다. 클래스와 메서드를 만드는 또 다른 기술적 이유는 publicJava 모듈 시스템을 사용할 때 모듈 경로에서 테스트를 간소화하기 위한 것입니다.
 
필드 및 메서드 상속
테스트 클래스의 필드는 상속됩니다. 예를 들어, @TempDir슈퍼클래스의 필드는 항상 서브클래스에 적용됩니다.
테스트 메서드와 라이프사이클 메서드는 Java 언어의 가시성 규칙에 따라 오버라이드되지 않는 한 상속됩니다. 예를 들어, @Test슈퍼클래스의 메서드는 서브클래스가 명시적으로 메서드를 오버라이드하지 않는 한 항상 서브클래스에 적용됩니다. 마찬가지로, 패키지 비공개 @Test메서드가 서브클래스와 다른 패키지에 있는 슈퍼클래스에 선언된 경우, @Test서브클래스는 다른 패키지의 슈퍼클래스에서 패키지 비공개 메서드를 오버라이드할 수 없으므로 해당 메서드는 항상 서브클래스에 적용됩니다.
 
 

다음 테스트 클래스는 @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() {
    }

}

 

record다음 예제와 같이 Java 클래스를 테스트 클래스로 사용하는 것도 가능합니다 .

Java 레코드로 작성된 테스트 클래스
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

public class MyFirstJUnitJupiterRecordTests {


    @Test
    void addition() {
        assertEquals(2, new Calculator().add(1,1));
    }
}

 

5.4. 표시 이름

테스트 클래스와 테스트 메서드는 @DisplayName 공백, 특수 문자, 심지어 이모티콘을 사용하여 테스트 보고서와 테스트 실행기, IDE에 표시될 사용자 지정 표시 이름을 선언할 수 있습니다.

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

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

    @Test
    @DisplayName("Cunstom test anme containing spaces")
    void testWithDisplayNameContainingSpaces() {

    }

    @Test
    @DisplayName("^________________^")
    void testWithDisplayNameContainingSpecialCharacters() {

    }

    @Test
    @DisplayName("\uD83D\uDE31")
    void testWithDisplayNamecontaingEmoji() {

    }
}

 

 

2.4.1. 표시 이름 생성기

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

생성기는 를 구현하여 생성할 수 있습니다 DisplayNameGenerator. Jupiter에서 사용 가능한 기본 생성기는 다음과 같습니다.

디스플레이 이름 생성기행동

Standard JUnit Jupiter 5.0이 출시된 이후 적용된 표준 표시 이름 생성 동작과 일치합니다.
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;

public class DisplayNameGeneratorDemo {

    @Nested
    @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
    class A_year_is_not_supported {
        @Test
        void if_it_is_zero() {

        }

        @DisplayName("A nagative 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_nagative() {

        }
    }

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

    }

}

 

-> 공식문서에 나오는 형식인데 Intelij에서는 설정이슈가 있어서 아래처럼 나오지 않는다(-> 관련문서)

+-- 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]

 

->Intelij 에서 나오는 결과

 

-> InteliJ 설정 변경(Run tests using 을 Gradle 에서 Intelij IDEA로 변경)

-> 실행결과

 

5.4.2. 기본 표시 이름 생성기 설정

junit.jupiter.displayname.generator.default 구성 매개변수를 사용하여 DisplayNameGenerator기본적으로 사용하려는 의 정규화된 클래스 이름을 지정할 수 있습니다. @DisplayNameGeneration주석을 통해 구성된 표시 이름 생성기의 경우와 마찬가지로 제공된 클래스는 인터페이스를 구현해야 합니다 . 기본 표시 이름 생성기는 주석이 둘러싼 테스트 클래스 또는 테스트 인터페이스에 DisplayNameGenerator없는 한 모든 테스트에 사용됩니다 . 주석을 통해 제공된 값은 항상 .에서 생성된 표시 이름보다 우선합니다 .@DisplayNameGeneration@DisplayNameDisplayNameGenerator

예를 들어, ReplaceUnderscores기본적으로 표시 이름 생성기를 사용하려면 구성 매개변수를 해당 정규화된 클래스 이름(예: src/test/resources/junit-platform.properties)으로 설정해야 합니다.

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

마찬가지로, .을 구현하는 모든 사용자 정의 클래스의 정규화된 이름을 지정할 수 있습니다 DisplayNameGenerator.

요약하자면, 테스트 클래스 또는 메서드의 표시 이름은 다음 우선순위 규칙에 따라 결정됩니다.

  1. 주석 의 값 @DisplayName(존재하는 경우)
  2. DisplayNameGenerator주석 에 지정된 것이 @DisplayNameGeneration 있으면 호출하여
  3. DisplayNameGenerator구성 매개변수를 통해 구성된 기본값을 호출하여 (존재하는 경우)
  4. 전화로org.junit.jupiter.api.DisplayNameGenerator.Standard

2.5. Assertions

JUnit Jupiter에는 JUnit 4에 있는 많은 어설션 메서드가 포함되어 있으며 Java 8 람다와 함께 사용하기에 적합한 몇 가지가 추가되었습니다. 모든 JUnit Jupiter 어설션은 클래스 static의 메서드 입니다 org.junit.jupiter.api.Assertions.

String어설션 메서드는 선택적으로 세 번째 매개변수로 어설션 메시지를 허용합니다. 어설션 메시지는 a 또는 a 중 하나입니다 Supplier<String>.

(예: 람다 표현식) 을 사용할 때 Supplier<String>메시지는 지연 평가됩니다. 이는 특히 메시지 구성이 복잡하거나 시간이 많이 걸리는 경우 어설션이 실패할 때만 평가되므로 성능 이점을 제공할 수 있습니다.

 

import org.junit.jupiter.api.Test;

import java.util.concurrent.CountDownLatch;
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;

public 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");

            // Lazily evaluates generateFailureMessage('a','b').
            assertTrue('a' < 'b', () -> generateFailureMessage('a','b'));
        }

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

        private static String generateFailureMessage(char a, char b) {
            return "Assertion messages can be lazily evaluated -- "
                    + "to avoid constructing complex messages unnecessarily." + (a < b);
        }
}

 

public class Person {

    private String firstName;
    private String lastName;


    public Person(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    public String getFirstName() {return firstName;}
    public String getLastName() {return lastName;}
}

 

public class Calculator {

    public int add(int a, int b) {
       return a + b;
    }

    public int multiply(int a, int b) {return a * b;}

    public int divide(int a, int b) {return a / b;}
}

 

선제적 타임아웃assertTimeoutPreemptively()
assertTimeoutPreemptively()클래스 의 다양한 메서드는 Assertions제공된 executable또는 supplier호출 코드와 다른 스레드에서 실행합니다. 이러한 동작은 executable또는 내에서 실행되는 코드가 저장소 supplier에 의존하는 경우 바람직하지 않은 부작용을 초래할 수 있습니다 java.lang.ThreadLocal.
이에 대한 일반적인 예 중 하나는 Spring Framework의 트랜잭션 테스트 지원입니다. 구체적으로, Spring의 테스트 지원은 ThreadLocal테스트 메서드가 호출되기 전에 트랜잭션 상태를 현재 스레드( 를 통해)에 바인딩합니다. 따라서 에 제공된 executable또는 가 트랜잭션에 참여하는 Spring 관리 구성 요소를 호출하는 경우 해당 구성 요소에서 수행한 모든 작업은 테스트 관리 트랜잭션과 함께 롤백되지 않습니다. 반대로 이러한 작업은 테스트 관리 트랜잭션이 롤백되더라도 영구 저장소(예: 관계형 데이터베이스)에 커밋됩니다.supplierassertTimeoutPreemptively()
저장소 에 의존하는 다른 프레임워크에서도 비슷한 부작용이 발생할 수 있습니다 ThreadLocal.

 

 

2.5.1. Kotlin 어설션 지원

JUnit Jupiter에는 Kotlin 에서 사용하기에 적합한 몇 가지 어설션 메서드도 있습니다 . org.junit.jupiter.api. 모든 JUnit Jupiter Kotlin 어설션은 패키지의 최상위 함수입니다 

import example.domain.Person
import example.util.Calculator
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.assertAll
import org.junit.jupiter.api.assertDoesNotThrow
import org.junit.jupiter.api.assertThrows
import org.junit.jupiter.api.assertTimeout
import org.junit.jupiter.api.assertTimeoutPreemptively
import java.time.Duration

class KotlinAssertionsDemo {
    private val person = Person("Jane", "Doe")
    private val people = setOf(person, Person("John", "Doe"))

    @Test
    fun `exception absence testing`() {
        val calculator = Calculator()
        val result =
            assertDoesNotThrow("Should not throw an exception") {
                calculator.divide(0, 1)
            }
        assertEquals(0, result)
    }

    @Test
    fun `expected exception testing`() {
        val calculator = Calculator()
        val exception =
            assertThrows<ArithmeticException> ("Should throw an exception") {
                calculator.divide(1, 0)
            }
        assertEquals("/ by zero", exception.message)
    }

    @Test
    fun `grouped assertions`() {
        assertAll(
            "Person properties",
            { assertEquals("Jane", person.firstName) },
            { assertEquals("Doe", person.lastName) }
        )
    }

    @Test
    fun `grouped assertions from a stream`() {
        assertAll(
            "People with first name starting with J",
            people
                .stream()
                .map {
                    // This mapping returns Stream<() -> Unit>
                    { assertTrue(it.firstName.startsWith("J")) }
                }
        )
    }

    @Test
    fun `grouped assertions from a collection`() {
        assertAll(
            "People with last name of Doe",
            people.map { { assertEquals("Doe", it.lastName) } }
        )
    }

    @Test
    fun `timeout not exceeded testing`() {
        val fibonacciCalculator = FibonacciCalculator()
        val result =
            assertTimeout(Duration.ofMillis(1000)) {
                fibonacciCalculator.fib(14)
            }
        assertEquals(377, result)
    }

    @Test
    fun `timeout exceeded with preemptive termination`() {
        // The following assertion fails with an error message similar to:
        // execution timed out after 10 ms
        assertTimeoutPreemptively(Duration.ofMillis(10)) {
            // Simulate task that takes more than 10 ms.
            Thread.sleep(100)
        }
    }
}

 

 

5.5.2. 타사 어설션 라이브러리

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

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

다음 예제는 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 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을 사용하여 수행할 수 있습니다 .

 

 

 

.

참조

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

 

JUnit 5 User Guide

Although the JUnit Jupiter programming model and extension model do 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 custo

junit.org

 

2. https://stackoverflow.com/questions/59012129/why-isnt-displayname-working-for-me-in-junit-5

 

Why isn't @DisplayName working for me in JUnit 5?

For some reason, I'm really having a hard time getting display names to actually be respected in JUnit 5 with Kotlin. Here's a test file I created for the purpose of example: import org.assertj.c...

stackoverflow.com