JUnit5 완벽 가이드

2021-04-11

시작하기전

예제코드는 여기에 있습니다.

JUnit5 문서기반으로 번역과 공부하고, 실졔로 사용하면서 필요한 내용을 조금 보강했습니다.

다른 곳에 공유해도 좋지만, 출처만 남겨주시면 됩니다.

JUnit5

이전 JUnit 버전과 다르게, JUnt5는 세개의 서브 프로젝트로 이루어져 있다.

JUnit5은 JUnit Platform + JUnit Jupiter + JUnit Vintage 이 세개가 합친 것이다.

JUnit Platform

JUnit Platform은 JVM에서 테스트 프레임워크를 실행하는데 기초를 제공한다. 또한 TestEngine API를 제공해 테스트 프레임워크를 개발할 수 있다.

JUnit Jupiter

JUnit Jupiter는 JUnit 5에서 테스트를 작성하고 확장을 하기 위한 새로운 프로그래밍 모델과 확장 모델의 조합이다.

JUnit Vintage

JUnit Vintage는 하위 호환성을 위해 JUnit3과 JUnt4를 기반으로 돌아가는 플랫폼에 테스트 엔진을 제공해준다.

요구사항

JUnit5은 java 8부터 지원하며, 이전 버전으로 작성된 테스트 코드여도 컴파일이 정상적으로 지원된다.

테스트 작성해보기

다음은 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)); 
  }
}

어노테이션

테스트를 구성하고, 프레임워크를 상속하기 위해서 다음과 같은 어노테이션을 지한다.

따로 명시하지 않으면 대부분 junit-jupiter-api 모듈 안의 org.junit.jupiter.api 패키지안에 존재한다.

  • @DisplayName : 테스트 클래스나 테스트 메소드에 이름을 붙여줄 때 사용한다.

    기본적으로 테스트 이름은 메소드이름을 따라간다. 하지만 메소드 이름은 그대로 둔 채 테스트 이름을 바꾸싶을 때 이 어노테이션을 사용한다.

  • @DisplayNameGeneration : 클래스에 해당 애노테이션을 붙이면 @Test 메소드 이름에 언더바(_)로 표시한 모든 부분은 공백으로 처리된다.

    @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
    class SomeTest {
      ...
    }
    
  • @BeforeEach : 각각 테스트 메소드가 실행되기전에 실행되어야 하는 메소드를 명시해준다. @Test , @RepeatedTest , @ParameterizedTest , @TestFactory 가 붙은 테스트 메소드가 실행하기 전에 실행된다. JUnit4의 @Before 와 같은 역할을 한다. 개인적으로 테스트 하기전에 필요한 목업 데이터를 미리 세팅해주기 위해 주로 사용한다.

  • @AfterEach : @Test , @RepeatedTest , @ParameterizedTest , @TestFactory 가 붙은 테스트 메소드가 실행되고 난 후 실행된다. JUnit4의 @After 어노테이션과 같은 역할을 한다.

  • @BeforeAll : @BeforeEach 는 각 테스트 메소드 마다 실행되지만, 이 어노테이션은 테스트가 시작하기 전 딱 한 번만 실행 된다.

  • @AfterAll : 이것도 위와 같다. 테스트가 완전히 끝난 후 딱 한 번만 실행 된다.

  • @Nested : test 클래스안에 Nested 테스트 클래스를 작성할 때 사용되며, static이 아닌 중첩클래스, 즉 Inner 클래스여야만 한다. 테스트 인스턴스 라이플사이클이 per-class 로 설정되어 있지 않다면 @BeforeAll , @AfterAll 가 동작안하니 주의하자. 테스트 인스턴스 라이플사이클은 밑의 내용에서 한번 더 언급 된다.

  • @Tag : 테스트를 필터링할 때 사용한다. 클래스또는 메소드레벨에 사용한다.

  • @Disabled : 테스트 클래스나, 메소드의 테스트를 비활성화 한다. JUnit4의 @Ignore와 같다.

  • @Timeout : 주어진 시간안에 테스트가 끝나지 않으면 실패한다.

  • @ExtendWith : extension을 등록한다. 이 어노테이션은 상속이 된다. 확장팩이라고 생각하면 될 것 같다.

  • @RegisterExtension : 필드를 통해 extension을 등록한다. 이런 필드는 private이 아니라면 상속된다.

  • @TempDir : 필드 주입이나 파라미터 주입을 통해 임시적인 디렉토리를 제공할 때 사용한다.

메타 어노테이션과 컴포즈 어노테이션

JUnit Jupiter 어노테이션은 메타 어노테이션처럼 사용된다. 그 말은 즉슨 자동으로 메타 어노테이션을 상속하는 자기만의 컴포즈 어노테이션을 정의할 수 있다.

예를 들어 코드에다가 @Tag("fast") 를 복사 붙여 넣기 하기보다, 커스텀 컴포즈 어노테이션인 @Fast 를 하나 만든 다음 Tag("fast") 를 대체하여 사용하는 것이다.

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

다음과 같이 사용할 수 있다.

@Fast
@Test
void myFastTest() {}

더 나아가서 @Tag("fast")@Test 를 합쳐 @FastTest 로 만들어도 좋다.

이렇게 자신만의 컴포즈 어노테이션을 만들면, 간편하게 사용할 수 있다.

테스트 클래스와 메소드

테스트 클래스란 최상위 클래스, 스태틱 멤버 클래스, @Nested 클래스에 적어도 한개의 @Test 어노테이션이 달린 테스트 메소드가 포함되있는 걸 말한다. 테스트 클래스는 abstract 이면 안되고, 하나의 생성자가 있어야 한다.

어차피 생성자가 없으면, 컴파일러가 자동으로 생성자를 만들어준다.

테스트 메소드란 @Test ,@RepeatedTest ,@ParamterizedTest,@TestFactory ,@TestTemplate 같은 메타 어노테이션이 메소드에 붙여진 메소드를 말한다.

라이플사이클 메소드란 @BeforeAll , @AfterAll , @BeforeEach , @AfterEach 같은 메타 어노테이션이 메소드에 붙여진 메소드를 말한다.

테스트 메소드와 라이플사이클 메소드는 테스트 할 클래스, 상속한 부모클래스 또는 인터페이스에 선언된다. 추가로 테스트 메소드와 라이프사이클 메소드는 abstract 선언하면 안되고, 어떠한 값도 리턴되선 안된다.

테스트 클래스, 테스트 메소드, 라이플사이클 메소드는 접근제어자를 public 으로 선언을 꼭 안해줘도 된다. 그러나 private 으로 선언하면 안된다.

Display Names

테스트 클래스와 테스트 메소드는 @DisplayName 을 이용해서 테스트 이름을 개발자가 보기 좋게 변경해줄 수 있다. 공백이나 특수문자나, 이모지도 가능하다.

Display Name Generators

JUnit Jupiter는 @DisplayNameGeneration 어노테이션을 통해 테스트이름을 어떻게 보여줄지 결정할 수 있다.

  • Standard : 메소드이름과 그 뒤에 붙는 괄호 그대로 보여준다.
  • Simple : 메소드이름만 보여준다.
  • ReplaceUnderscores : 언더스코어를 제거한다. 게시판_테스트 게시판 테스트 로 보여준다.
  • IndicativeSentences : 테스트 클래스 이름과 테스트 메소드이름 + 괄호를 보여준다.

기본 DisplayName Generator 세팅하기

src/test/resources/junit-platform.properties

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

테스트를 시작할 때 DisplayName 우선순위가 있다.

  1. @DisplayName 어노테이션 안의 value 값

    @DisplayName(value = "디스플레이 네임은 이렇게 바꿉니다.")
    @Test
    void 테스트() {
    }
    
  2. DisplayNameGeneration 어노테이션 안에 있는 DisplayNameGenerator

    @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
    class 테스트클래스 {
    }
    
  3. 설정 파라미터를 통해 설정해준 디폴트 DisplayNameGenerator

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

    예제코드 보러가기

Assertions

JUnit Jupiter는 JUnit4로부터 온 assertion 메소드와 새롭게 자바 8 람다 표현식으로 추가된 메소드들이 있다. 모든 JUnit Jupiter assertion은 정적 메소드이며, org.junit.jupiter.api.Assertions 클래스 안에 있다.

class AssertionsTest {

    private final Calculator calculator = new Calculator();
    private final Person person = new Person("동현", "민");

    @Test
    void standardAssertions() {
        assertEquals(2, calculator.add(1, 1));
        assertEquals(4, calculator.multiply(2, 2),
                "추가적인 실패 메세지는 마지막 파라미터에 넣는다.");
        assertTrue('a' < 'b', () -> "Assertion messages는 지연로딩과 비슷하게 동작한다. -- "
                + "불필요하게 메세지를 만드는 일을 피하기 위해서.");
    }

    @Test
    void groupedAssertions() {
        assertAll("person",
                () -> assertEquals("Jane", person.getFirstName()),
                () -> assertEquals("Doe", person.getLastName()));
    }

    @Test
    void exceptionTesting() {
        Exception exception = assertThrows(ArithmeticException.class, () ->
                calculator.divide(1, 0));

        assertEquals("/ by zero", exception.getMessage());
    }

    @Test
    void timeoutNotExceeded() {
        // 아래의 assertion은 성공.
        assertTimeout(ofMinutes(2), () -> {
            // 2분 미만으로 실행되는 동작 실행
        });
    }

    @Test
    void timeoutNotExceededWithResult() {
        //아래의 assertion은 성공하면서 supplied 객체를 반환한다.
        String actualResult = assertTimeout(ofMinutes(2), () -> {
            return "a result";
        });
        assertEquals("a result", actualResult);
    }

    @Test
    void timeoutExceeded() {
        //이 테스트는 fail이 난다.
        assertTimeout(ofMillis(10), () -> {
            // 10ms 이상 걸리는 작업
            Thread.sleep(100);
        });
    }

    @Test
    void timeoutExceededWithPreemptiveTermination() {
        //이 테스트는 fail이 난다.
        assertTimeoutPreemptively(ofMillis(10), () -> {
            // 10ms 이상 걸리는 작업
            new CountDownLatch(1).await();
        });
    }
}

Preemptive Timeout(선점적인 시간초과) 테스트에서의 assertTimeoutPreemptively()

선언적 타임아웃인 @timeout 어노테이션과 반대로 Assertions에 있는 다양한 assertTimeoutPreemptively() 메소드는 제공된 executable나 supplier를 다른 스레드에서 실행한다. executable 이나 supplier 실행된 코드들이 ThreadLocal에 의존하게 되면 사이드이펙트가 일어날 수 있다.

흔한 예로 스프링 프레임워크에서 transactional 테스트를 진행할 때이다. Spring의 테스트는 트랜잭션 상태를 TreadLocal 을 이용해서 현재 상태를 테스트 메소드가 실행하기 전에 저장해둔다. 결과적으로 assertTimeoutPreemptively()에 제공된 executable이나 supplier가 트랜잭션에 참여하는 스프링 컴포넌트를 호출하게 되면 이 컴포넌트는 테스트가 끝난 후 롤백이 되지 않는다.

써드 파티 Assertion 라이브러리

아쉽게도 JUnit Jupiter가 제공해주는 assertion이 많은 테스트 시나리오에서 부족할 수 있다. 그럴 경우 JUnit 팀은 AssertJ , Hamcrest , Truth 등 써드 파티 라이브러리를 쓰는걸 추천해 한다.

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

public class HamcrestAssertionsTest {

    private final Calculator calculator = new Calculator();

    @Test
    void HamcrestMatcher_이용하기() {
        assertThat(calculator.subtract(4, 1), is(equalTo(3)));
    }
}

Assumptions

JUnit Jupiter는 JUnit4에서 제공해주던 assumption 메소드와, Java 8 람다 표현식과, 메서드 레퍼런스를 이용해 몇 개를 더 추가 했다. 모든 assumptions 메소드는 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;

public class AssumptionTest {
    private final Calculator calculator = new Calculator();

    @Test
    void CI서버에서만_테스트() {
        assumeTrue("CI".equals(System.getenv("ENV")));
    }

    @Test
    void 개발환경에서만_테스트() {
        assumeTrue("DEV".equals(System.getenv("ENV")),
                () -> "Aborting test: not on developer workstation");
    }

    @Test
    void 모든환경_테스트() {
        assumingThat("CI".equals(System.getenv("ENV")), () -> {
            // CI 서버에서만 실행하는 테스트
            assertEquals(2, calculator.divide(4, 2));
        });
        // 모든 환경에서 실행하는 테스트
        assertEquals(42, calculator.multiply(6, 7));
    }
}

특정 테스트 비활성화하기

테스트 클래스나, 테스트 메소드에 @Disabled 어노테이션을 이용해서 비활성화 할 수 있다.

클래스에 선언

@Disabled("이슈 #103 가 해결될 때 까지 비활성화")
public class DisabledClassTest {

    @Test
    void testWillBeSkipped() {
    }
}

메소드에 선언

public class DisabledTest {
    @Disabled("#42 버그가 해결될 때 까지 비활성화")
    @Test
    void testWillBeSkipped() {
    }

    @Test
    void testWillBeExecuted() {
    }
}

@Disabled 를 사용할 때 이유를 명시하지 않고 사용해도 되지만, JUnit 팀에서는 왜 이 테스트가 비활성화 되었는지에 대한 짧은 설명을 추가하는 걸 권장하고 있다. 예를 들어 위에 예제에서는 #42 의 버그가 해결될 때 까지 비활성화 처럼 말이다.

테스트 실행 조건

JUnit Juptier에 있는 ExecutionCondition API는 개발자가 특정 조건에 따라 테스트를 실행할지 말지 결정한다.심플한 예로 @Disabled 어노테이션을 지원하는 내장된 DisabledCondition 이다. JUnit Jupiter는 또한 @Disabled 이외에도 개발자가 선언적으로 테스트를 활성화하거나 비활성화 하기 위해 org.junit.jupiter.api.condition 패키지에 있는 어노테이션 기반 조건을 지원한다. 여러 개의 ExecutionCondition 이 등록되면 여러 개 조건중 하나라도 비활성화 조건에 걸리면, 테스트를 비활성화 한다. 이 테스트가 왜 비활성화 되었는지 알려주고 싶다면, 모든 어노테이션은 disabledReason 속성을 지정할 수 있으므로, 이걸 이용하면 된다.

OS 따라 테스트 실행하기

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

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

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

    @Test
    @DisabledOnOs(value = WINDOWS,disabledReason = "윈도우에서는 테스트하지 않아요.")
    void notOnWindows() {
        // ...
    }
}

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

자바 환경변수에 따라 실행하기

특정 JRE 버전에 따라 테스트를 활성화 비활성화 할 수 있습니다. @EnabledOnJre@DisabledOnJre 어노테이션을 이용해서 활성화할 JRE 버전을 명시하고, @EnabledForJreRange@DisabledForJreRange 어노테이션을 이용하면 여러개의JRE 버전을 테스트 할 수 있다.

public class JreConditionalTest {
    @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() {
        // ...
    }
}

시스템 속성 조건

@EnabledIfSystemProperty@DisabledIfSystemProperty 어노테이션을 사용하면 named에 작성된 JVM 시스템 속성에 따라 테스트를 활성화 또는 비활성화 할 수 있다.

public class SystemPropertyConditionalTest {
    @Test
    @EnabledIfSystemProperty(named = "os.arch", matches = ".*64.*")
    void onlyOn64BitArchitectures() {
        // ...
    }

    @Test @DisabledIfSystemProperty(named = "ci-server", matches = "true")
    void notOnCiServer() {
        // ...
    }
}

Junit Jupiter 5.6부터는 EnabledIfSystemPropertyDisabledIfSystemProperty 는 반복가능한 어노테이션(@RepeatedTest) 으로 변경되었다. 그 결과로, 테스트를 할 때 여러 개를 중첩해서 사용할 수 있다.

반복가능한 어노테이션은 추후에 밑에서 설명한다.

환경 변수 조건

@EnabledIfEnvironmentVariable@DisabledIfEnvironmentVariable 어노테이션을 사용하여 운영체제 시스템의 환경 변수에 따라 테스트를 활성화 또는 비활성화 할 수 있다.

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

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

JUnit Jupiter 5.6부터 두개의 어노테이션은 반복가능한 어노테이션으로 변경되었다. 그 결과로 테스트 할 때 여러개를 중첩해서 사용할 수 있다.

사용자 커스텀 조건

@EnabledIf@DisabledIf 어노테이션을 사용하여 지정해준 메소드가 반환하는 boolean의 값에 따라 테스트를 활성화 또는 비활성화 할 수 있다. 어노테이션 안에 메소드 이름을 작성주면 되고, 만약 테스트 클래스 밖에 있는 메소드라면, 클래스를 포함한 전체 이름을 써줘야 한다. 필요하다면 메소드에 ExtensionContext 타입인 하나의 파라미터를 갖을 수 있다.

package dev.donghyeon.junitstudy.conditional;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledIf;
import org.junit.jupiter.api.condition.EnabledIf;

public class CustomConditionalTest {
    @Test
    @EnabledIf("dev.donghyeon.junitstudy.conditional.ExternalClass#eCustomCondition")
    void enabled() {
        System.out.println("test enabled active");
    }

    @Test
    @DisabledIf("customCondition")
    void disabled() {

    }

    boolean customCondition() {
        return true;
    }
}

class ExternalClass {

    static boolean eCustomCondition() {
        return true;
    }
}

@EnabledIf@DisabledIf 를 클래스 레벨에 사용할 때 컨디션 메서드는 반드시 static 으로 선언되어야 한다. 또한 외부 클래스에 위치한 컨디션 메서드도 static 으로 선언되어야 하며, 사용할 때는 패키지를 포함한 전체 이름을 적어줘야 한다.

태그와 필터링

클래스와 메소드는 @Tag 어노테이션을 통해 태그할 수 있다. 이 태그들은 나중에 테스트를 필터링 할때 사용 된다.

Tags를 사용하기 위한 규칙

  • 태그는 공백이나 null 이 있으면 안됨
  • ISO 제어문자가 있으면 안됨
  • 다음과 같은 문자열이 있으면 안됨
    • ,
    • (
    • )
    • |
    • !
    • &
@Tag("fast")
@Tag("model")
class TaggingTest {

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

테스트 실행 순서 바꾸기

일반적으로 단위테스트는 테스트 순서에 영향을 받지 않지만, 통합 테스트를 작성할 때나, 테스트의 순서가 중요한 함수형 테스트를 할 때 테스트 실행 순서를 바꾸고 싶을 때가 있다.

제대로 작성된 단위 테스트는 일반적으로 실행순서에 상관없지만, 가끔 강제로 특정 순서에 맞게 지정해줄 필요가 생긴다.

테스트 메소드가 실행되는 순서를 바꾸고 싶으면 테스트 클래스나 메소드에 @TestMethodOrder 를 이용하여 MethodOrderer 를 원하는대로 구현하면 된다. MethodOrderer 를 커스텀해서 구현하거나, 이미 내장된 MethodOrderer 구현 중 하나를 사용하면 된다.

  • DisplayName : displayName 기반으로 정렬한다.
  • MethodName : 메소드 이름으로 정렬한다.
  • OrderAnnotation : @Order 어노테이션에 명시된 순서대로 정렬한다.
  • Random : 랜덤으로 정렬한다.

다음의 예제는 @Order 어노테이션을 사용하여 특정 순서대로 테스트를 실행한다.

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

@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class OrderedTest {
    @Test
    @Order(1)
    void nullValues() {
    }

    @Test
    @Order(2)
    void emptyValues() {
    }

    @Test
    @Order(3)
    void validValues() {
    }
}

메소드 실행 순서 디폴트 설정하기

junit.jupiter.testmethod.order.default 설정 파라미터를 이용하여 디폴트로 사용할 MethodOrderer 를 설정해줄 수 있다. 테스트 클래스에 @TestMethodOrder 가 없으면 파라미터에 준 값으로 모든 테스트들에 디폴트 순서를 사용한다.

예를 들어 OrderAnnotation 클래스를 디폴트로 사용하고 싶다면 클래스 이름 전체를 설정 파라미터에 설정하면 된다.

src/test/reousrces/junit-platform.properties

junit.jupiter.testmethod.order.default = \
	org.junit.jupiter.api.MethodOrder$OrderAnnotation

이렇게 사용하면 @TestMethodOrder 어노테이션이 붙지 않은 테스트 클래스에 @Order 어노테이션이 명시된 순서대로 테스트가 실행하게 된다.

⁉️ 테스트 LifeCycle

테스트 인스턴스 상태의 변경가능성 때문에 일어나는 사이드 이펙트를 줄이고, 테스트 메서드를 격리된 환경에서 독립적으로 실행시키기 위해 JUnit은 테스트 메소드를 실행시키기 전에 각각의 테스트 클래스의 새로운 인스턴스를 만든다. 이렇게 메소드마다 테스트 라이프사이클을 갖는 동작은 이전 버전의 JUnit과 똑같은 동작이며, 디폴트 동작이다.

즉 테스트 메소드마다 새로운 테스트 클래스의 인스턴스를 만들어 내며, 기본동작이다.

테스트 메소드에 @Disabled ,@DisabledOnOs 를 붙여 테스트 메소드를 비활성화 해도 해당 테스트에 대해 새로운 인스턴스를 만든다.

만약 같은 인스턴스 안에서 모든 테스트 메소드를 모두 실행하고 싶다면 테스트 클래스에 @TestInstance(Lifecycle.PER_CLASS) 어노테이션을 사용하면 된다. 이 어노테이션을 사용하면 테스트 클래스 단위로 새로운 인스턴스가 생기게 된다. 그러므로 그 안에 있는 인스턴스 변수를 테스트 메소드들이 공유를 하므로 @BeforeEach@AfterEach 를 사용하여 내부 상태를 리셋을 해야만 할 수 있다.

PER-CLASS 는 테스트 인스턴스의 기본 생성 방식인 PER-METHOD 를 사용할 때에 이점은 다음과 같다. 특히 PER-CLASS는 @BeforeAll@AfterAll 를 붙인 메소드에 static을 붙여서 사용하지 않아도 되고 인터페이스의 deafult 메소드에서도 사용하지 않아도 된다. 또한 PER-CLASS@Nested 테스트 클래스에서 @BeforeAll@AfterAll 메소드를 사용할 수 있게 해준다.

@Nested 테스트 클래스는 바로 밑에 설명이 있다.

디폴트 테스트 인스턴스 라이플사이클 변경하기

테스트 클래스에 @TestInstance 어노테이션이 없으면 기본 라이플사이클을 사용한다. 기본 모드는 메소드마다 새로운 인스턴스를 만드는 PER_METHOD 이다. 그러나 전체 테스트에 디폴트 라이프사이클을 변경할 수 있다. 변경하기 위해선 junit.jupiter.testinstance.lifecycle.default 설정 파라미터에 TestInstnace.LifeCycle enum클래스를 써주면 된다. JUnit 설정파일이나, LauncherDiscoveryRequest를 Launcher로 전달한 설정 파라미터를 이용해 JVM 시스템 변수로 제공해줄 수 있다.

예를 들어 라이플사이클 모드를 LifeCycle_PER_CLASS 로 변경하고 싶으면 JVM을 실행할 때 다음과 같이 실행한다.

JVM 시스템 변수 설정

-Djunit.jupiter.testinstance.lifecycle.default=per_class

그러나 이것 보단, JUnit 설정파일을 통해 라이플사이클 모드를 변경하는 것이 프로젝트의 버전 관리를 할 수 있기 때문에 좀 더 권장하는 방법이다.

JUnit 설정 파일을 통해 설정하는 방법은, junit-platform.properties 이름의 파일을 클래스패스 만들고 다음과 같이 작성한다.

JUnit 설정 파일 설정

junit.jupiter.testinstance.lifecycle.default = per_class

디폴트 테스트 인스턴스 라이플사이클 변경이 일관적으로 적용되지 않으면 예상치 못한 결과를 초래 한다. 예를 들어, 빌드 설정엔 “per-class”를 디폴트로 설정했지만, IDE 설정에서는 “per-method”로 설정되어 실행될 수 있다. 이렇게 되면, 빌드 서버에서 오류가 난다. 이런 현상을 해결하기 위해서는 JVM 시스템 변수 대신, JUnit 설정 파일을 사용하는 걸 추천 한다.

Nested Tests

@Nested 테스트는 테스트 그룹 사이의 관계를 표현할 수 있게 해준다.

@DisplayName("A stack")
class TestingStack {

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

위에 코드를 Intellij에서 돌려보면 코드처럼 트리구조로 결과가 나온다.

여기서 WhenNew.createNewStack() 메서드가 새로운 Stack 클래스를 만들어주는 역할을 한다. 이 메서드에 적힌 @BeforeEach 어노테이션은 라이프사이클 메서드로 이 메서드가 포함된 클래스 아래 레벨에 있는 모든 메서드까지 영향을 준다.

오직 non-static인 nested 클래스(예를 들어 inner 클래스)만 @Nested 를 붙일 수 있다. 중첩은 개발자 마음대로 할 수 있으며, 이너 클래스는 한가지만 제외하고 라이프사이클을 가진다. 바로 @BeforeAll@AfterAll 메서드는 기본적으로 작동하지 않는다. 그 이유는 자바는 이너 클래스안에 static 멤버 변수를 두는걸 허락하지 않기 때문이다. 이런 제약을 우회하는 방법으로 테스트 클래스에 @Nested 을 붙이고 @TestInstance(Lifecycle.PER_CLASS)를 사용하여 우회하는 수 밖에 없다.

생생자와 메소드 의존성 주입

이전 JUnit 버전들에서는 테스트 클래스에 생성자나 메소드에 파라미터를 갖지 못하게 했다. JUnit juptier의 주요 변화로 테스트 클래스의 생성자와 메소드가 이제는 파라미터를 갖을 수 있도록 변경되었다. 이런 변화는 코드의 유연성과 생성자와 메소드에 의존성 주입을 가능하게 해준다.

ParameterResolver 는 런타임시 동적으로 파라미터를 결정하는 테스트 익스텐션에 관한 API가 정의되어 있다. 테스트 클래스의 생성자나, 메서드나, 라이플사이클 메서드가 파라미터를 받고 싶다면, 파라미터는 ParameterResolver 를 등록함으로써 런타임시 결정이 된다.

현재 자동적으로 등록되는 3개의 내장된 리졸버들이 있다.

  • TestInfoParameterResolver : 생성자나 메소드 파라미터가 TestInfo의 타입이면 TestInfoParameterResolver가 현재 컨테이너나, 테스트에 일치하는 값을 TestInfo 인스턴스로 제공해준다. 제공받은 TestInfo는 현재 컨테이너 또는 테스트에 관한 displayname, 테스트클래스, 테스트메소드, 관련된 태그들의 테스트 정보를 가져올 때 사용한다.

    다음의 예제는 테스트 생성자와, @BeforeEach 메소드, @Test 메소드에 어떻게 TestInfo 가 주입되는지 보여준다.

    @DisplayName("TestInfo Demo Test")
    class TestInfoTest {
        TestInfoTest(TestInfo testInfo) {
            assertEquals("TestInfo Demo Test", 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 와 관련된 반복의 총 갯수의 정보를 가져올 때 사용한다. 그러나 현재 컨텍스트 외부에 있는 @RepeatedTest 를 찾아내진 못한다.

  • TestReporterParameterResolver : TestReporter 타입의 파라미터를 사용해야할 때 사용한다. TestReporter는 현재 실행중인 테스트에 관한 추가적인 데이터를 발행해야할 때 사용한다. 데이터는 TestExecutionListener안에 있는 reportingEntryPublished() 메소드를 이용해 컨슘되며, IDE나 리포트에서 볼 수 있다.

다른 리졸버를 사용하고 싶으면, @ExtendWith 을 통해서 상속을 하면 된다.

리졸버 커스텀 해보기

@ExtendWith(RandomParametersExtension.class)
class MyRandomParameterTest {

    @Test
    void injectsInteger(@Random int i, @Random int j) {

        assertNotEquals(i, j);
    }

    @Test
    void injectsDouble(@Random double d) {
        assertEquals(0.0, d, 1.0);
    }

}

RandomParametersExtension은 다음과 같이 만들어졌다.

RandomParametersExtension.java

public class RandomParametersExtension implements ParameterResolver {

    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.PARAMETER)
    public @interface Random {
    }

    @Override
    public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
        return parameterContext.isAnnotated(Random.class);
    }

    @Override
    public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) {
        return getRandomValue(parameterContext.getParameter(), extensionContext);
    }

    private Object getRandomValue(Parameter parameter, ExtensionContext extensionContext) {
        Class<?> type = parameter.getType();
        java.util.Random random = extensionContext.getRoot().getStore(ExtensionContext.Namespace.GLOBAL)//
                .getOrComputeIfAbsent(java.util.Random.class);
        if (int.class.equals(type)) {
            return random.nextInt();
        }
        if (double.class.equals(type)) {
            return random.nextDouble();
        }
        throw new ParameterResolutionException("No random generator implemented for " + type);
    }

}

이 리졸버는 파라미터에 랜덤으로 값을 주입해주는 리졸버다.

실제 사용 사례로는 MockitoExtensionSpringExtension을 많이 쓴다.

테스트 인터페이스와 디폴트 메소드

@Test, @RepeatedTest, @ParameterizedTest , @TestFactory, @TestTemplate, @BeforeEach, @AfterEach인터페이스 디폴트 메소드에 선언을 해도 된다. 만약 테스트 인터페이스나 테스트 클래스에 @TestInstnace(Lifecycle.PER_CLASS)로 되어 있다면 @BeforeAll@AfterAllstatic 메서드로 만들거나 디폴트 메서드로 만들어야 한다.

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
interface TestLifecycleLogger {

    static final Logger logger = Logger.getLogger(TestLifecycleLogger.class.getName());

    @BeforeAll
    static 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()));
    }
}
interface TestInterfaceDynamicTestsDemo {
    @TestFactory
    default Stream<DynamicTest> dynamicTestsForPalindromes() {
        return Stream.of("racecar", "radar", "mom", "dad")
                .map(text -> dynamicTest(text, () -> assertTrue(text,equals(text)))); }
}

@ExtendWith@Tag는 테스트 인터페이스에 선언할 수 있어서 이 테스트 인터페이스를 선언한 구현체는 자동으로 tag와 extension을 상속 받는다.

@Tag("timed")
@ExtendWith(TimingExtension.class)
public class TimeExecutingLogger {
}

TimingExtension.java

public class TimingExtension implements BeforeTestExecutionCallback, AfterTestExecutionCallback {

	private static final Logger logger = Logger.getLogger(TimingExtension.class.getName());

	private static final String START_TIME = "start time";

	@Override
	public void beforeTestExecution(ExtensionContext context) throws Exception {
		getStore(context).put(START_TIME, System.currentTimeMillis());
	}

	@Override
	public void afterTestExecution(ExtensionContext context) throws Exception {
		Method testMethod = context.getRequiredTestMethod();
		long startTime = getStore(context).remove(START_TIME, long.class);
		long duration = System.currentTimeMillis() - startTime;

		logger.info(() ->
			String.format("Method [%s] took %s ms.", testMethod.getName(), duration));
	}

	private Store getStore(ExtensionContext context) {
		return context.getStore(Namespace.create(getClass(), context.getRequiredTestMethod()));
	}

}

TimingExtension 클래스는 메서드 이름에서도 알 수 있듯이, 테스트를 실행하기 전과 후에 호출할 콜백 메서드를 구현해놓은 클래스이다.

이렇게 구현 후 테스트 클래스를 만들어 인터페이스들을 상속해보자.

public class TestInterfaceDemo implements TestLifecycleLogger,
        TimeExecutingLogger, TestInterfaceDynamicTestsDemo {

    @Test
    @DisplayName("동일 테스트")
    void isEqualValue() {
        Assertions.assertEquals(1, "a".length(), "항상 같음");
    }
}
2월 25, 2021 10:21:15 오전 dev.donghyeon.junitstudy.testinterface.TestLifecycleLogger beforeAllTests
정보: Before all tests
2월 25, 2021 10:21:15 오전 dev.donghyeon.junitstudy.testinterface.TestLifecycleLogger beforeEachTest
정보: About to execute [동일 테스트]
2월 25, 2021 10:21:15 오전 dev.donghyeon.junitstudy.testinterface.TimingExtension afterTestExecution
정보: Method [isEqualValue] took 7 ms.
2월 25, 2021 10:21:15 오전 dev.donghyeon.junitstudy.testinterface.TestLifecycleLogger afterEachTest
정보: Finished executing [동일 테스트]
2월 25, 2021 10:21:15 오전 dev.donghyeon.junitstudy.testinterface.TestLifecycleLogger afterAllTests
정보: After all tests

이 테스트를 돌려보면 콘솔에 다음과 같이 찍힌 것으로 보아, extension이 정상적으로 상속이 된걸 확인할 수 있다.

TimeExecutingLogger가 @ExtendWith으로 TimingExtension 클래스를 확장했고, TestInterfaceDemo가 TimeExecutimgLogger를 상속해서, extension을 상속받게 됐다.

@Repeated Test

@RepeatedTest 어노테이션에 명시된 숫자로 테스트를 얼마나 반복적으로 실행할지 지정해줄 수 있다. 반복 테스트의 호출은 보통의 @Test 메소드들이랑 똑같이 동작한다.

다음의 예제는 repeatedTest()가 10번 호출된다.

@RepeatedTest(10) 
void repeatedTest() { 
}

또한 반복의 수를 지정해 줄 때, 각각의 반복되는 테스트에 대해서 보여줄 display name도 설정할 수 있다. 게다가, display name은 스트링과 정적 placeholder로 조합해서 패턴을 만들 수 있다. 다음은 지원되는 placeholder 이다.

  • DisplayName : @RepeatedTest 메소드의 보여줄 이름
  • {currentRepetition} : 현재 반복 횟수
  • {totalRepetitions} : 반복할 총 횟수

반복을 돌 때 보여지는 기본 displayname은 다음과 같은 패턴으로 만들어진다.

“repetition {currentRepetition} of {totalRepetitions}”

그래서 위에 있는 repeatedTest()는 “repetition 1 of 10, repetition 2 of 10” 이렇게 보여진다. 메소드 이름을 포함하고 싶으면 사전 정의된, RepeatedTest.LONG_DISPLAY_NAME을 사용하면 된다.

현재 반복 정보와 반복의 수를 구하기 위해서 개발자가 @RepeatedTest ,@BeforeEach ,@AfterEach 메소드에 RepetitionInfo의 인스턴스를 주입시켜야 한다.

반복 테스트 예제

import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.*;

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

@Slf4j
public class RepeatTest2 {

    @BeforeEach
    void beforeEach(TestInfo testInfo, RepetitionInfo repetitionInfo) {
        int currentRepetition = repetitionInfo.getCurrentRepetition();
        int totalRepetitions = repetitionInfo.getTotalRepetitions();
        String methodName = testInfo.getTestMethod().get().getName();
        log.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());
    }

    @RepeatedTest(value = 1, name = "{displayName} {currentRepetition}/{totalRepetitions}")
    @DisplayName("Repeat!")
    void customDisplayName (TestInfo testInfo) {
        assertEquals("Repeat! 1/1", testInfo.getDisplayName());
    }

    @RepeatedTest(value = 1, name = RepeatedTest.LONG_DISPLAY_NAME)
    @DisplayName("Details...")
    void customDisplayNameWithLongPattern(TestInfo testInfo) {
        assertEquals("Details... :: repetition 1 of 1", testInfo.getDisplayName());
    }

    @RepeatedTest(value = 5, name = "Wiederholung {currentRepetition} von {totalRepetitions}")
    void repeatedTestInGerman() { // ...

    }
}

Console 결과

INFO: About to execute repetition 1 of 10 for repeatedTest
INFO: About to execute repetition 2 of 10 for repeatedTest
INFO: About to execute repetition 3 of 10 for repeatedTest
INFO: About to execute repetition 4 of 10 for repeatedTest
INFO: About to execute repetition 5 of 10 for repeatedTest
INFO: About to execute repetition 6 of 10 for repeatedTest
INFO: About to execute repetition 7 of 10 for repeatedTest 
INFO: About to execute repetition 8 of 10 for repeatedTest
INFO: About to execute repetition 9 of 10 for repeatedTest 
INFO: About to execute repetition 10 of 10 for repeatedTest
INFO: About to execute repetition 1 of 5 for repeatedTestWithRepetitionInfo
INFO: About to execute repetition 2 of 5 for repeatedTestWithRepetitionInfo
INFO: About to execute repetition 3 of 5 for repeatedTestWithRepetitionInfo
INFO: About to execute repetition 4 of 5 for repeatedTestWithRepetitionInfo
INFO: About to execute repetition 5 of 5 for repeatedTestWithRepetitionInfo 
INFO: About to execute repetition 1 of 1 for customDisplayName 
INFO: About to execute repetition 1 of 1 for customDisplayNameWithLongPattern 
INFO: About to execute repetition 1 of 5 for repeatedTestInGerman 
INFO: About to execute repetition 2 of 5 for repeatedTestInGerman 
INFO: About to execute repetition 3 of 5 for repeatedTestInGerman 
INFO: About to execute repetition 4 of 5 for repeatedTestInGerman 
INFO: About to execute repetition 5 of 5 for repeatedTestInGerman

테스트 이름 결과

├─ RepeatedTestsDemo ✔
│ ├─ repeatedTest() ✔
│ │ ├─ repetition 1 of 10 ✔
│ │ ├─ repetition 2 of 10 ✔
│ │ ├─ repetition 3 of 10 ✔
│ │ ├─ repetition 4 of 10 ✔
│ │ ├─ repetition 5 of 10 ✔
│ │ ├─ repetition 6 of 10 ✔
│ │ ├─ repetition 7 of 10 ✔
│ │ ├─ repetition 8 of 10 ✔ 
│ │ ├─ repetition 9 of 10 ✔ 
│ │ └─ repetition 10 of 10 ✔
│ ├─ repeatedTestWithRepetitionInfo(RepetitionInfo) ✔
│ │ ├─ repetition 1 of 5 ✔
│ │ ├─ repetition 2 of 5 ✔
│ │ ├─ repetition 3 of 5 ✔
│ │ ├─ repetition 4 of 5 ✔
│ │ └─ repetition 5 of 5 ✔
│ ├─ Repeat! ✔
│ │ └─ Repeat! 1/1 ✔
│ ├─ Details... ✔
│ │ └─ Details... :: repetition 1 of 1 ✔
│ └─ repeatedTestInGerman() ✔
│ ├─ Wiederholung 1 von 5 ✔
│ ├─ Wiederholung 2 von 5 ✔
│ ├─ Wiederholung 3 von 5 ✔
│ ├─ Wiederholung 4 von 5 ✔
│ └─ Wiederholung 5 von 5 ✔

파라미터화 테스트

파라미터화 테스트는 각각 다른 인자로 여러 번 테스트를 돌린다. @Test 대신 @ParameterizedTest 어노테이션을 사용하면 된다. 추가적으로 호출 시 사용될 인자를 적어도 하나는 적어줘야 한다.

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

이 테스트코드를 실행하면 콘솔에는 다음과 같이 찍힌다.

palindromes(String) ✔
├─ [1] candidate=racecar ✔
├─ [2] candidate=radar ✔
└─ [3] candidate=able was I ere I saw elba ✔

파라미터화 테스트는 현재 실험단계다.

필수 셋업

파라미터화 테스트를 사용하려면 junit-jupiter-params 를 추가해야한다. (보통 JUnit5 추가하면 자동으로 들어가있긴 하다.)

인자 사용하기

파라미터화 테스트 메소드는 일반적으로 미리 정해놓은 파라미터 소스메서드 파라미터 인덱스1:1 매칭이 된다. 그러나 파라미터화 테스트 메서드가 소스로부터 하나의 객체만 메서드로 전달해 줄 수 있다. 그리고 인자는 ParameterResolver 에 의해 제공받을 수 있다.(TestInfo나 TestReporter 같은 것들) 특히 파라미터화 테스트 메서드는 반드시 다음의 규칙을 따른 형태로 선언되야 한다.

  • 인덱스화된 인자들은 반드시 처음에 선언되어야 한다.
  • aggregator는 그 다음에 선언되어야 한다.
  • ParameterResolver에 의해 제공되는 인자는 반드시 마지막에 선언되어야 한다.

인자 제공하기

JUnit Jupiter는 인자를 제공하기 위해 몇 개의 source 어노테이션을 제공한다.

@ValueSource

@ValueSource는 가장 심플한 소스다. 간단하게 하나의 배열로 값을 정의하며, 하나의 인자만 받는 파라미터화 테스트에만 적용할 수 있다.

다음은 @ValueSource가 지원하는 타입의 목록이다.

  • Short
  • byte
  • int
  • long
  • float
  • double
  • char
  • boolean
  • java.lang.String

예를 들어 다음의 @ParameterizedTest 메소드는 3번 호출 된다.

@ParameterizedTest 
@ValueSource(ints = { 1, 2, 3 }) 
void testWithValueSource(int argument) {
	assertTrue(argument > 0 && argument < 4); 
}

Null and Empty Sources

잘못된 입력이 들어올 수 있는 경우를 확인하고 적절한 행동을 검증하기 위해서 파라미터화 테스트에 null 또는 빈 값을 제공해 줄 수 있다. 다음의 어노테이션은 null 과 빈 값을 제공해준다.

  • @NullSource : @ParameterizedTest 메소드에 null을 제공한다.
  • @EmptySource : 다음과 같은 타입 String ,List ,Set, Map, primitive 배열 예) int[] ,char[] …, 객체 배열 예) String[] ,Intger[] … 같은 인자에 빈값을 제공한다.
    • 지원되는 타입중에 서브타입들은 지원하지 않는다.
  • @NullAndEmptySource : @NullSource 와 @EmptySource 기능들을 합친 어노테이션이다.

다음과 같이 만약 파라미터화 테스트로 다양한 빈값의 스트링을 제공해주고 싶다면 다음과 같이 사용하면 된다.

@ValueSource(Strings = {" ", "    ", "\t", "\n"})

또한 null , 빈 값 , 빈 입력을 테스트 하고 싶으면, @NullSource, @EmptySoucre, @ValueSouce를 혼합해서 사용하면 된다.

@ParameterizedTest 
@NullSource 
@EmptySource 
@ValueSource(strings = { " ", " ", "\t", "\n" }) 
void nullEmptyAndBlankStrings(String text) {
  assertTrue(text == null || text.trim().isEmpty()); 
}

이걸 더 요약하면 다음과 같이 작성하면 된다.

@ParameterizedTest 
@NullAndEmptySource
@ValueSource(strings = { " ", " ", "\t", "\n" }) 
void nullEmptyAndBlankStrings(String text) {
  assertTrue(text == null || text.trim().isEmpty()); 
}

위에 있는 nullEmptyAndBlankString(String) 파라미터화 테스트는 6번의 호출이 이루어진다. null과 빈 스트링, 그리고 @ValueSoucre에 적힌 4개의 String들이다.

@EnumSource

@EnumSource는 Enum을 편리하게 사용하게 해 준다.

@ParameterizedTest
@EnumSource(ChronoUnit.class)
void testWithEnumSource(TemporalUnit unit) {
  assertNotNull(unit);
}

value 속성은 선택이다. 생략하면 메소드에 선언된 첫번 째 타입 파라미터가 사용된다. enum 타입을 참조하지 않으면 테스트는 실패 한다. 위에 예제는 메소드 파라미터가 TemporalUnit 으로 선언되어 있기 때문에 value 속성이 필요하다(하지만 생략하면 첫번 째 파라미터가 사용됨 즉 “Nanos” , “Micros” , “Millis”).

ChronoUnit.java

public enum ChronoUnit implements TemporalUnit {
    NANOS("Nanos", Duration.ofNanos(1)),
    MICROS("Micros", Duration.ofNanos(1000)),
    MILLIS("Millis", Duration.ofNanos(1000_000)),
  //이하 생략
}

아래 예제는 Enum타입이 아닌 ChronoUnit을 바로 인자로 사용했는데, 이렇게 사용하면 @EnumSource의 타입설정을 생략해줄 수 있다.

@EnumSource의 enum Type 생략

@ParameterizedTest 
@EnumSource 
void testWithEnumSourceWithAutoDetection(ChronoUnit unit) {
	assertNotNull(unit); 
}

이 @EnumSource 속성으로 어떤 상수를 사용할지 지정해주는 추가적인 name 속성을 사용할 수 있다. 생략하면 모든 상수가 사용된다.

ChronoUnit 상수 중 DAYS와 HOURS만 테스트하기

@ParameterizedTest
@EnumSource(names = { "DAYS", "HOURS" })
void testWithEnumSourceInclude(ChronoUnit unit) {
  assertTrue(EnumSet.of(ChronoUnit.DAYS, ChronoUnit.HOURS).contains(unit)); 
}

@EnumSource 어노테이션은 mode 라는 추가적인 속성을 제공하는데, 이 속성은 어떤 상수를 테스트 메소드에 넘길지, 섬세하게 조절할 수 있는 기능이 있다. 예를 들어, enum 상수 풀 이나 특정 정규식에서 다음과 같이 특정 name만을 제외할 수 있다.

ChronoUnit 상수 중 EARS와 FOREVER 제외하고 테스트하기

@ParameterizedTest 
@EnumSource(mode = EXCLUDE, names = { "ERAS", "FOREVER" })
void testWithEnumSourceExclude(ChronoUnit unit) { 
  assertFalse(EnumSet.of(ChronoUnit.ERAS, ChronoUnit.FOREVER).contains(unit)); 
}

ChronoUnit 상수 중 DAYS로 끝나는 상수만 테스트하기

@ParameterizedTest 
@EnumSource(mode = MATCH_ALL, names = "^.*DAYS$")
void testWithEnumSourceRegex(ChronoUnit unit) {
  assertTrue(unit.name().endsWith("DAYS")); 
}

@MethodSource

@MethodSource는 하나 이상의 테스트 클래스 또는 외부 클래스 팩토리 메서드를 참조할 수 있다.

테스트 클래스 안에 있는 팩토리 메서드는 @TestInstance(Lifecycle.PER_CLASS) 어노테이션을 테스트 클래스에 붙이지 않았으면, 반드시 static을 붙여줘야 하며 외부 클래스에 있는 팩토리 메서드는 반드시 static을 붙여줘야 한다. 게다가 이런 팩토리 메서드는 어떠한 인자도 있으면 안된다.

각각의 팩토리 메서드는 반드시 인자의 stream을 만들어야 하며, stream 안에 있는 각각의 인자들의 집합은 @ParameterizedTest 메서드의 각각의 호출마다 물리적 인자로 제공된다.

만약 하나의 파라미터만 필요하다면 다음과 같이 파라미터 타입의 인스턴스의 Stream을 리턴한다.

@ParameterizedTest 
@MethodSource("stringProvider") 
void testWithExplicitLocalMethodSource(String argument) {
  assertNotNull(argument); 
}

static Stream<String> stringProvider() {
  return Stream.of("apple", "banana"); 
}

@MethodSource를 통해서 팩토리 메소드 이름을 명시적으로 제공해주지 않으면, @ParamterizedTest 메소드가 붙은 현재 테스트 메소드 이름을 기준으로 팩토리 메서드를 찾는다.

@MethodSource에서 메서드 이름 생략

@ParameterizedTest 
@MethodSource 
void testWithDefaultLocalMethodSource(String argument) { 	
  assertNotNull(argument); 
}

//팩터리 메서드 이름이 @ParameterizedTest 메서드 이름과 같음.
static Stream<String> testWithDefaultLocalMethodSource() { 
  return Stream.of("apple", "banana"); 
}

다음과 같이 DoubleStream, IntStream, LongStream의 primitive 타입의 Stream도 지원한다

primitive 타입 사용 예제

@ParameterizedTest 
@MethodSource("range") void testWithRangeMethodSource(int argument) {
  assertNotEquals(9, argument); 
}

static IntStream range() {
  return IntStream.range(0, 20).skip(10); 
}

파라미터화 테스트 메서드가 여러 개의 파라미터를 갖고 있으면 collection , stream , 인자 인스턴스의 배열, 객체 배열을 리턴해야 한다.

arguments(Obejct)는 Arguments 인터페이스에 정의된 정적 팩토리 메서드 이다 arguments(Object)의 대안으로 Arguments.of(Object)를 사용할 수도 있다.

테스트 메서드 파라미터가 여러 개 일 때

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

외부에 있는 정적 팩토리 메소드를 사용하려면 메소드 이름을 구별할 수 있게 다음과 같이 전체를 적어줘야한다.

import java.util.stream.Stream; 
import org.junit.jupiter.params.ParameterizedTest; 
import org.junit.jupiter.params.provider.MethodSource;
class ExternalMethodSourceDemo {

  @ParameterizedTest 
  @MethodSource("example.StringsProviders#tinyStrings") 
  void testWithExternalMethodSource(String tinyString) { 
    // test with tiny string 
  }
}

class StringsProviders {
  
  static Stream<String> tinyStrings() {
    return Stream.of(".", "oo", "OOO"); 
  } 
}

@CsvSource

@CsvSource는 리스트를 콤마(,)로 구분 해 준다.

@ParameterizedTest
@CsvSource({
  "apple, 1",
  "banana, 2",
  "'lemon, lime', 0xF1" 
})
void testWithCsvSource(String fruit, int rank) {
  assertNotNull(fruit);
  assertNotEquals(0, rank); 
}

기본 구분자는 콤마를 사용하지만, delimiter 속성을 이용해서 다른 문자를 기본 구분자로 사용할 수도 있다. 또는 대안적으로 delimiterString 속성을 쓰면 문자 대신 문자열로 구분자를 사용할 수 있다. 그러나 delimiter 속성과 delimiterString을 동시에 사용하면 안된다.

'' 의 결과는 emptyValue 속성이 설정되어 있지 않으면 빈 String을 반환하고, 아예 빈 값이면 null 값을 반환한다. 만약 null이 리턴하는 대상 타입이 primitive 타입일 경우 ArgumentConversionException 예외를 발생시키니 조심하자.

예제 인풋 Resulting Argument List
@CsvSource({ “apple, banana” }) “apple”, “banana”
@CsvSource({ “apple, ‘lemon, lime’” }) “apple”, “lemon, lime”
@CsvSource({ “apple, ‘’” }) “apple”, “”
@CsvSource({ “apple, “ }) “apple”, null
@CsvSource(value = { “apple, banana, NIL” }, nullValues = “NIL”) “apple”, “banana”, null

@CsvFileSource

@CsvFileSource는 클래스패스나 로컬 파일 시스템에 있는 CSV 파일을 사용한다. CSV 파일에 있는 각각의 라인마다 파라미터화 테스트가 호출 된다.

기본 구분자는 콤마(,)지만, delimiter 속성을 설정해서 다른 문자를 사용할 수 있다. 또는 대안적으로 delimiterString 속성을 쓰면 문자 대신 문자열로 구분자를 사용할 수 있다. 그러나 delimiter 속성과 delimiterString을 동시에 사용하면 안된다.

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

two-column.csv

Country, reference
Sweden, 1 
Poland, 2
"United States of America", 3

@ArgumentsSource

@ArgumentsSource는 커스텀 또는 재사용가능한 ArgumentsProvider를 지정해 줄 때 사용한다. ArgumentsProvider의 구현은 반드시 클래스 탑 레벨에 선언하거나, 정적 중첩 클래스에 선언해야 한다.

@ParameterizedTest 
@ArgumentsSource(MyArgumentsProvider.class) 
void testWithArgumentsSource(String argument) {
  assertNotNull(argument); 
}
public class MyArgumentsProvider implements ArgumentsProvider {

  @Override 
  public Stream<? extends Arguments> provideArguments(ExtensionContext context) { 		
    return Stream.of("apple", "banana").map(Arguments::of); 
  }
}

인자 변환

확대 변환 (Widening 변환)

@ParaterizedTest에 제공된 인자들은 확대 Primitive 변환을 지원한다. 예를 들어 @ValueSoue(ints = {1, 2, 3})이 적힌 파라미터화 테스트에서 int 타입 뿐만 아니라, long, float, double 타입들로 받을 수 있다.

묵시적 변환

@CsvSource 같은 경우를 지원하기 위해 JUnit Jupiter는 내장된 많은 묵시적 변환기를 지원한다. 변환 프로세스는 각각의 메소드 파라미터에 선언된 타입에 달려있다.

예를 들어 만약 @ParameterizedTest가 TimeUnit 파라미터가 선언되어 있고, 외부에서 String 값으로 파라미터를 공급해준다고 했을 때, 이 공급되는 String은 자동적으로 일치하는 TimeUnit enum 상수로 변환된다.

@ParameterizedTest 
@ValueSource(strings = "SECONDS") 
void testWithImplicitArgumentConversion(ChronoUnit argument) { 						   			             assertNotNull(argument.name()); 
}   

String 인스턴스는 묵시적으로 ChronoUnit인 타켓 타입으로 묵시적으로 변환된다.

10진법, 16진법, 8진법 String 문자들은,byte,short,int,long 등 대응하는 타입으로 변환 된다.

타겟 타입 예제
boolean/ Boolean “true” → true
byte/Byte “15”, “0xF”, or “017” → (byte) 15
char/Character “o” → ‘o’
short/Sh ort “15”, “0xF”, or “017” → (short) 15
int/Inte ger “15”, “0xF”, or “017” → 15
long/Lon g “15”, “0xF”, or “017” → 15L
float/Fl oat “1.0” → 1.0f
double/D ouble “1.0” → 1.0d
Enumsubclass “SECONDS” → TimeUnit.SECONDS
java.io. File “/path/to/file” → new File(“/path/to/file”)
java.lan g.Class “java.lang.Integer” → java.lang.Integer.class (use $ for nested classes, e.g. “java.lang.Thread$State”)
java.lan g.Class “byte” → byte.class (primitive types are supported)
java.lan g.Class “char[]” → char[].class (array types are supported)
java.mat h.BigDec imal “123.456e789” → new BigDecimal(“123.456e789”)
java.mat h.BigInt eger “1234567890123456789” → new BigInteger(“1234567890123456789”)
java.net .URI “https://junit.org/” → URI.create(“https://junit.org/”)
java.net .URL “https://junit.org/” → new URL(“https://junit.org/”)
java.nio .charset .Charset “UTF-8” → Charset.forName(“UTF-8”)
java.nio .file.Pa th “/path/to/file” → Paths.get(“/path/to/file”)
java.tim e.Durati on “PT3S” → Duration.ofSeconds(3)
java.tim e.Instan t “1970-01-01T00:00:00Z” → Instant.ofEpochMilli(0)
java.tim e.LocalD ateTime “2017-03-14T12:34:56.789” → LocalDateTime.of(2017, 3, 14, 12, 34, 56, 789_000_000)
java.tim e.LocalD ate “2017-03-14” → LocalDate.of(2017, 3, 14)
java.tim e.LocalT ime “12:34:56.789” → LocalTime.of(12, 34, 56, 789_000_000)
java.tim e.MonthD ay ”–03-14” → MonthDay.of(3, 14)
java.tim e.Offset DateTime “2017-03-14T12:34:56.789Z” → OffsetDateTime.of(2017, 3, 14, 12, 34, 56, 789_000_000, ZoneOffset.UTC)
java.tim e.Offset Time “12:34:56.789Z” → OffsetTime.of(12, 34, 56, 789_000_000, ZoneOffset.UTC)
java.tim e.Period “P2M6D” → Period.of(0, 2, 6)
java.tim e.YearMo nth “2017-03” → YearMonth.of(2017, 3)
java.tim e.Year “2017” → Year.of(2017)
java.tim e.ZonedD ateTime “2017-03-14T12:34:56.789Z” → ZonedDateTime.of(2017, 3, 14, 12, 34, 56, 789_000_000, ZoneOffset.UTC)
java.tim e.ZoneId “Europe/Berlin” → ZoneId.of(“Europe/Berlin”)
java.tim e.ZoneOf fset “+02:30” → ZoneOffset.ofHoursMinutes(2, 30)
java.uti l.Curren cy “JPY” → Currency.getInstance(“JPY”)
java.uti l.Locale “en” → new Locale(“en”)
java.uti l.UUID “d043e930-7b3b-48e3-bdbe-5a3ccfb833db” → UUID.fromString(“d043e930-7b3b-48e3-bdbe- 5a3ccfb833db”)

Fallback String to-Object 변환

위에 있는 테이블에 있는 것 처럼 String 타입을 변환하려는 대상 타입으로 묵시적으로 변환할 수 있지만, JUnit Jupiter는 만약 대상 타입이 아래에 적힌 것 처럼 정확히 하나의 팩토리 메소드 또는 팩토리 생성자에 알맞은 경우를 위해 String 타입을 자동적으로 변환해주는 fallback 메카니즘을 제공한다.

  • factory method : 접근자가 private가 아니여야 하며, 생성하고자 하는 클래스 내에 String 인자를 받으며 해당 타입을 리턴하는 static 메서드가 존재해야 한다. 네이밍컨벤션을 딱히 따르지 않아도 되지만. 정적 팩터리 컨벤션을 따르면 협업하기에 좋다.

    public class Book {
      
      private final String title;
        
      public static Book fromTitle(String title) {
        return new Book(title); 
      }
      
    }
    
  • factory constructor : 대상 클래스에 private 아닌 생성자로 하나의 String을 받는 생성자가 있어야 한다.

    public class Book {
      
      private final String title;
        
      public Book(String title) {
        return new Book(title); 
      }
      
    }
    

만약 여러개의 팩토리 메서드가 있으면 무시된다. 팩토리 메서드와 팩터리 생성자 두개가 있으면 생성자 대신 팩터리 메서드를 사용한다.

아래 예제에서, @ParameterizedTest 메소드에서, Book 인자는 Book.fromTitle(String) 팩토리 메서드가 호출될 때 생성되며, title의 값으로 “42 Cats” 전달 된다.

@ParameterizedTest 
@ValueSource(strings = "42 Cats") 
void testWithImplicitFallbackArgumentConversion(Book book) { 
	assertEquals("42 Cats", book.getTitle()); 
}

Book.java

public class Book {

  private final String title;

  private Book(String title) { 
    this.title = title; 
  } 
  
  public static Book fromTitle(String title) {
    return new Book(title); 
  }

  public String getTitle() {
    return this.title; 
  }
}

명시적 변환

묵시적 인자 변환에 의존하는 대신, 아래의 예제와 같이 특정 파라미터에 @ConvertWith 어노테이션을 사용하여 ArgumentConverter를 명시적으로 지정해주면 된다. ArgumentConverter의 구현은 반드시 클래스 최상위 레벨에 선언하거나, 정적 중첩 클래스로 선언되야한다.

@ParameterizedTest 
@EnumSource(ChronoUnit.class) 
void testWithExplicitArgumentConversion( 
  @ConvertWith(ToStringArgumentConverter.class) String argument) {
  assertNotNull(ChronoUnit.valueOf(argument));
}

ToStringArgumentConverter.java

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

컨버터가 오직 타입을 다른 타입으로 변경하는거라면, 보일러플레이트 코드 타입 체크를 줄이기 위해서 TypedArgumentConverter를 상속하면 된다.

ToLengthArgumentConverter.java

public class ToLengthArgumentConverter extends TypedArgumentConverter<String, Integer> 
{

  protected ToLengthArgumentConverter() {
    super(String.class, Integer.class); 
  }

	@Override 
  protected Integer convert(String source) {
    return source.length(); 
  }

}

명시적 인자 컨버터는 원래 테스트가 구현하거나, 익스텐션 저자가 구현을 하기 때문에 junit-jupiter-params 는 오직 하나의 래퍼런스 할 수 있는 명시적 인자 컨버터만 제공한다 그것이 JavaTimeArgumentConverter 이다. 이 어노테이션은 JavaTimeConversionPattern 을 사용해서 컴포즈 어노테이션으로 제공한다.

@ParameterizedTest 
@ValueSource(strings = { "01.01.2017", "31.12.2017" }) 
void testWithExplicitJavaTimeConverter( 
  @JavaTimeConversionPattern("dd.MM.yyyy") LocalDate argument) {
  assertEquals(2017, argument.getYear());
}

인자 수집

기본적으로 @ParameterizedTest 메소드에 제공되는 각각의 인자들은 하나의 메소드 파라미터와 일치한다. 만약에 테스트 메서드가 많은 인자들을 요구 할 경우 인자 수 만큼 파라미터의 개수가 늘어나게 된다.

이런 경우때문에 ArgumentsAccessor여러 개의 파라미터를 대신하여 사용한다. 이 API를 사용하기 위해 테스트 메소드에 제공된 하나의 인자를 통해서 접근할 수 있다. 게다가 위에서 묵시적 변환에서 얘기한 것 처럼 타입 변환이 지원 된다.

@ParameterizedTest 
@CsvSource({
  "Jane, Doe, F, 1990-05-20",
  "John, Doe, M, 1990-10-22" 
}) 
void testWithArgumentsAccessor(ArgumentsAccessor arguments) {
	Person person = new Person(arguments.getString(0),
                             arguments.getString(1),
                             arguments.get(2, Gender.class),
                             arguments.get(3, LocalDate.class));

  if (person.getFirstName().equals("Jane")) {
    assertEquals(Gender.F, person.getGender()); 
  } else {
    assertEquals(Gender.M, person.getGender()); 
  }
  assertEquals("Doe", person.getLastName()); 
  assertEquals(1990, 	person.getDateOfBirth().getYear());
}

ArgumentsAccessor 인스턴스는 자동으로 ArgumentsAccessor 타입의 파라미터에 주입된다.

커스텀 수집

ArgumentsAccessor를 @ParameterizedTest 인자로 직접적으로 접근하는걸 제외하고, 사용자 정의의 재사용 가능한 수집기를 사용할 수 있다.

커스텀 수집기를 사용하기 위해서 ArgumentsAggregator 인터페이스를 구현해서, @AggregateWith 어노테이션을 통해서 등록해야 한다.

@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 
}
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 어노테이션이 파라미터화 테스트 메소드 여러 개에서 사용하고 있다면, 다음과 같이 커스텀 어노테이션을 만들어서 사용해 볼 수도 있다.

@ParameterizedTest 
@CsvSource({
	"Jane, Doe, F, 1990-05-20",
	"John, Doe, M, 1990-10-22" 
}) 
void testWithCustomAggregatorAnnotation(@CsvToPerson Person person) {
// perform assertions against person 
}
@Retention(RetentionPolicy.RUNTIME) 
@Target(ElementType.PARAMETER) 
@AggregateWith(PersonAggregator.class)
public @interface CsvToPerson { }

DispalyName 커스텀 하기

기본적으로 파라미터화 테스트 호출의 display name은 호출된 index와 특정 호출에 관련한 모든 인자를 나타내는 String으로 이루어져 있다. 각각 앞에 파라미터 변수 이름이 붙는다.

그러나 다음과 같이 name 속성을 이용해서 display name을 커스텀할 수 있다.

@DisplayName("Display name of container") 
@ParameterizedTest(name = "{index} ==> the rank of ''{0}'' is {1}") 
@CsvSource({ "apple, 1", "banana, 2", "'lemon, lime', 3" }) 
void testWithCustomDisplayNames(String fruit, int rank) { 
}

이 테스트를 실행해보면 콘솔에 다음과 같이 찍힌다.

Display name of container ✔ 
├─ 1 ==> the rank of 'apple' is 1 ✔ 
├─ 2 ==> the rank of 'banana' is 2 ✔ 
└─ 3 ==> the rank of 'lemon, lime' is 3 ✔

name 속성은 MessageFormat 패턴을 사용한다. 그래서 작은따옴표(‘)를 표현하기 위해 작은따옴표를 두번 썼다.

다음은 display name을 커스텀 하기 위한 지원되는 placeholder 들이다.

Placeholder Description
DisplayName 메소드의 이름을 나타낸다.
{index} 현재 호출된 인덱스를 나타낸다.
{arguments} the complete, comma-separated arguments list
{argumentsWithNames} the complete, comma-separated arguments list with parameter names
{0}, {1}, … 0번째 인자 1번째 인자..

인자를 displayname에 표현할 때, 최대 표현 글자 수가 넘어가면 생략된다. 이 설정은 기본값은 512이며, junit.jupiter.params.displayname.argument.maxlength 에서 변경가능하다.

생명주기와 상호작용(Interoperability)

@BeforeEach 메소드는 호출 전에 실행 되듯이, 파라미터화 테스트는 호출마다 @Test 메소드와 동일한 생명주기를 갖는다. 다이나믹 테스트와 비슷하게, 호출마다 테스트에 트리가 하나씩 IDE에 보여진다. 일반적인 @Test 메소드와 @ParamterizedTest 메소드를 같은 테스트 클래스안에 혼합해서 사용해도 된다.

다이나믹 테스트는 밑에서 설명한다.

그러나 @ParamterizedTest를 사용할 때는 항상 인자를 받는 파라미터가 먼저 와야한다.

잘못된 파라미터 순서를 가진 @ParamterizedTest 예제

@ParameterizedTest
@ValueSource(strings = "apple")
void testWithRegularParameterResolver(TestReporter testReporter,String argument) {
    testReporter.publishEntry("argument", argument);
}

올바른 예

@ParameterizedTest
@ValueSource(strings = "apple")
void testWithRegularParameterResolver(String argument,TestReporter testReporter) {
    testReporter.publishEntry("argument", argument);
}

테스트 템플릿

@TestTemplate 메소드는 일반적인 테스트 케이스보단 테스트 케이스에 대한 템플릿 이다. 등록된 프로바이더가 리턴하는 컨텍스트 호출 수에 따라 여러 번 호출 되도록 디자인 되었다. 그래서 반드시 등록된 TestTemplateInvocationContextProvider와 함께 사용해야 한다. 테스트 템플릿 메소드의 각각의 호출은 일반적인 @Test 메소드처럼 실행된다.

Repeated Test와 Parameterized Test는 내장된 테스트 템플릿 중 하나이다.

Dynamic Test (동적 테스트)

JUnit Jupiter의 @Test 어노테이션은 Junit 4의 @Test 어노테이션과 굉장히 유사하다. 둘다 테스트 케이스에 대한 내용이 메소드 안에 있다. 이런 테스트 케이스는 그 상태로 정적이며, 컴파일 시간에 정해지며, 런타임 시에도 아무 변화가 없다.

Junit Jupiter는 이 기본 테스트들에 대해 완전히 새로운 테스트 프로그래밍 모델을 소개 했다. 이 모델은 @TestFactory 어노테이션이 붙은 팩토리 메소드에 의해 런타임시 만들어지는 동적 테스트이다.

@Test 어노테이션을 사용하는 메소드와 반대로, @TestFactory 메소드는 그 자체로 테스트는 아니며, 팩토리 메소드가 테스트 케이스다. 그래서, 동적 테스트는 팩토리의 산물이다. 기술적으로 말하자면, @TestFactory 메소드는 반드시 하나의 DynamicNode ,Stream, Collection, Interable, Interator, DynamicNode 인스턴스의 배열을 리턴해야 한다. DynamicContainer 인스턴스는 dispalyName 과 랜덤으로 dynamic node의 중첩 계층을 만들 수 있는 동적 자식 노드의 리스트로 이루어져 있다. DynamicTest 인스턴스는 lazy 실행되어 테스트 케이스의 동적 생성과 비 결정적(non-deterministic) 생성이 가능하다.

@TestFactory가 리턴하는 Stream은 stream.close()을 호출해서 닫아줘야 Files.lines() 같은 리소스를 안전하게 사용할 수 있다.

@Test 메소드를 같이 쓰려면, @TestFactory를 반드시 private나 static으로 선언하면 안되며, 선택적으로 ParameterResolver에서 파라미터를 제공해주는 걸 사용할 수 있다.

그래서 DynamicTest는 런타임시 만들어지는 테스트케이스를 말한다. DynamicTest는 display name과 다이나믹 테스트에서 람다 표현식이나 메서드추론 방식으로 제공될 수 있는 함수형인터페이스의 조합이라고 볼 수 있다.

Dynamic 테스트 생명주기

Dynamic 테스트의 생명주기는 @Test 어노테이션과는 좀 다르다. 특히 콜백 라이플 사이클이 존재하지 않는데, @BeforeEach 와 @AfterEach 메소드는 @TestFactory 메소드에서는 실행하는데, 각각의 Dynamic 테스트에 대해서는 실행하지 않는다. 다른 말로, Dynamic 테스트 관한 람다 표현식안의 테스트 인스턴스의 필드에 접근하기위해서 해당 필드는 초기화 되지 않는 다는 말이다.

동적 테스트 예제

다음의 DynamicTestsDemo 클래스는 동적 테스트의 예제를 보여준다.

첫번 째 메소드는 유효하지 않은 타입을 반환한다. 유효하지 하지 않는 타입을 리턴하는건 컴파일 시에 알수 없고 런타임시에 알 수 있기 때문에, 발견되면 JUnitException을 던진다.

DynamicNode ,Stream, Collection, Interable, Interator, DynamicNode 중 하나를 리턴하지 않아서 실패한다.

@TestFactory
List<String> dynamicTestsWithInvalidReturnType() {
  return Arrays.asList("Hello");
}

다음 5개의 메소드는 DynamicTest 인스턴스의 Collection, Iterable, Iterator, Stream을 만드는 간단한 예제이다. 대부분의 예제는 동적 동작을 제대로 보여주진 않고, 그냥 규칙에 맞게 지원되는 리턴 타입을 반환하기만 한다. 그러나 dynamicTestFromStream()dynamicTestsFromIntStream()은 주어진 string의 set과 인풋 숫자의 범위로 동적 테스트를 얼마나 쉽게 만드는지 보여준다.

@TestFactory
Collection<DynamicTest> dynamicTestsFromCollection() {
  return Arrays.asList(
    dynamicTest("1st dynamic test", () -> assertTrue(isPalindrome("madam"))),
    dynamicTest("2nd dynamic test", () -> assertEquals(4, calculator.multiply(2, 2)))
  );
}

@TestFactory
Iterable<DynamicTest> dynamicTestsFromIterable() {
  return Arrays.asList(
    dynamicTest("3rd dynamic test", () -> assertTrue(isPalindrome("madam"))),
    dynamicTest("4th dynamic test", () -> assertEquals(4, calculator.multiply(2, 2)))
  );
}

@TestFactory
Iterator<DynamicTest> dynamicTestsFromIterator() {
  return Arrays.asList(
    dynamicTest("5th dynamic test", () -> assertTrue(isPalindrome("madam"))),
    dynamicTest("6th dynamic test", () -> assertEquals(4, calculator.multiply(2, 2)))
  ).iterator();
}

@TestFactory
DynamicTest[] dynamicTestsFromArray() {
  return new DynamicTest[]{
    dynamicTest("7th dynamic test", () -> assertTrue(isPalindrome("madam"))),
    dynamicTest("8th dynamic test", () -> assertEquals(4, calculator.multiply(2, 2)))
  };
}

@TestFactory
Stream<DynamicTest> dynamicTestsFromStream() {

  return Stream.of("racecar", "radar", "mom", "dad")
    .map(text -> dynamicTest(text, () -> assertTrue(isPalindrome(text))));
}

@TestFactory
Stream<DynamicTest> dynamicTestsFromIntStream() {

  // Generates tests for the first 10 even integers.
  return IntStream.iterate(0, n -> n + 2).limit(10).mapToObj(n -> dynamicTest("test" + n, () -> assertTrue(n % 2 == 0)));

}

그 다음 메소드는 정말로 다이나믹하다. generateRandomNumberOfTests()는 랜덤 숫자를 만드는 Iterator, display name generator, test executor를 만들어, 이 세개를 DynamicTest.stream()에게 제공한다. generateRandomNumberOfTests()의 비 결정적(non-deterministic) 동작 때문에 반복되는 테스트와 충돌은 날수 있기 때문에 사용에 주의 해야 한다. 동적 테스트는 뛰어난 표현력을 갖고 있다.

@TestFactory
Stream<DynamicTest> generateRandomNumberOfTestsFromIterator() {

  // 0~100 부터 랜덤한 int 값을 만들어 7로 나누어지지 않는 수를 만든다.
  Iterator<Integer> inputGenerator = new Iterator<Integer>() {
    Random random = new Random();
    int current;

    @Override
    public boolean hasNext() {
      current = random.nextInt(100);
      return current % 7 != 0;
    }

    @Override
    public Integer next() {
      return current;
    }

  };
  // 다음과 같이 표시할 display name을 만든다 : input:5, input:37, input:85, etc.
  Function<Integer, String> displayNameGenerator = (input) -> "input:" + input;
  // 현재 input 값에 대하여 테스트를 실행한다.
  ThrowingConsumer<Integer> testExecutor = (input) -> assertTrue(input % 7 != 0);

  // dynamic tests의 스트림을 반환한다.
  return DynamicTest.stream(inputGenerator, displayNameGenerator, testExecutor);
}

그 다음 메소드는 유연셩 측면에서 generateRandomNumberOfTests()와 비슷하다. 그러나 dynamicTestsFromStreamFactoryMethod()DynamicTest.stream 팩토리 메소드를 통해서 다이나믹 테스트의 stream을 만들어낸다.

@TestFactory
Stream<DynamicTest> dynamicTestsFromStreamFactoryMethod() {

  // 검사할 회문의 stream을 만든다.
  Stream<String> inputStream = Stream.of("racecar", "radar", "mom", "dad");

  // 보여줄 displayname을 만든다 : racecar는 회문이다.
  Function<String, String> displayNameGenerator = text -> text + " 는 회문이다.";

  // 현재 input에 대해 테스트를 실행한다. 
  ThrowingConsumer<String> testExecutor = text -> assertTrue(isPalindrome(text));

  // dynamic tests의 stream을 리턴
  return DynamicTest.stream(inputStream, displayNameGenerator, testExecutor);

}

dynamicNodeSingleTest() 메소드는 하나의 테스트 케이스에 대해서 DynamicTest를 만들기 때문에 Stream으로 리턴하는 대신 바로 DynamicNode를 리턴한다.

@TestFactory
Stream<DynamicNode> dynamicTestsWithContainers() {

  return Stream.of("A", "B", "C")
    .map(input -> dynamicContainer("Container " + input, Stream.of(
      dynamicTest("not null", () -> assertNotNull(input)),
      dynamicContainer("properties", Stream.of(
        dynamicTest("length > 0", () -> assertTrue(input.length() > 0)),
        dynamicTest("not empty", () -> assertFalse(input.isEmpty()))
      ))
    )));
}

@TestFactory
DynamicNode dynamicNodeSingleTest() {
  return dynamicTest("'pop' is a palindrome", () -> assertTrue(isPalindrome("pop")));
}

@TestFactory
DynamicNode dynamicNodeSingleContainer() {
  return dynamicContainer("palindromes",
                          Stream.of("racecar", "radar", "mom", "dad")
                          .map(text -> dynamicTest(text, () -> assertTrue(isPalindrome(text)))
                              ));
}

다이나믹 URI 테스트

IDE나 빌드 툴에서 테스트의 source나 컨테이너들의 위치를 표현하기 위한 TestSource 를 제공한다.

다이나믹 테스트 또는 다이다믹 컨테이너를 위한 TestSourcejava.net.URI 으로부터 만들 수 있으며, 각각 DynamicTest.dynamicTest(String, URI, Executable) 이나 DynamicContainer.dynamicContainer(String, URI, Stream) 팩토리 메서드로 만들 수 있다. URI는 다음의 TestSource 구현체에 따라 형변환이 가능하다.

ClasspathResourceSource

URI이 classpath를 포함하고 있다.

예를 들어 classpath:/test/foo.xml?line20,column=2

DirectorySource

URI이 파일 시스템의 디렉토리를 나타내고 있다.

FileSource

URI이 파일 시스템안에 파일을 나타내고 있다.

MethodSource

URI이 전체의 method 이름을 포함하고 있다.

예를 들어 method:org.junit.Foo#bar(java.lang.String, java.lang.String[]) 처럼 사용된걸 말한다.

UriSource

위에 있는 조건에 아무것도 포함되어 있지 않을 때 사용한다.

Timeouts

@Timeout 어노테이션은 테스트, 테스트 팩토리, 테스트 템플릿 , 라이프사이클 메소드에 선언하며, 주어진 시간 동안 실행시간이 초과하면 실패하게 된다. 기본 단위는 seconds(초)이지만, 따로 설정도 가능하다.

다음 예제는 @Timeout이 라이프사이클 메서드와, 테스트 메서드에 사용된 모습이다.

class TimeoutDemo {

    @BeforeEach
    @Timeout(5)
    void setUp() {
      	//실행 시간이 5초 이상을 넘어가면 실패한다.
    }

    @Test
    @Timeout(value = 100, unit = TimeUnit.MILLISECONDS)
    void failsIfExecutionTimeExceeds100Milliseconds() {
        // 실행 시간이 100 milliseconds 를 넘어가면 실패한다.
    }

}

assertTimeoutPreemptively() 검증 메서드와 대조적으로 @Timeout 어노테이션이 붙은 메소드는 테스트의 메인스레드 안에서 진행된다. 만약 시간초과가 되면, 다른 스레드에 의해 메인스레드는 인터럽트 된다. 이는 현재 실행중인 스레드에 민감한 메커니즘을 사용하는 Spring과 같은 프레임 워크와의 상호 운용성을 보장하기 위해 수행된다. ThreadLocal의 트랜잭션 관리가 예로 있다.

테스트 클래스안에 모든 테스트 메소드 또는 모든 @Nested 클래스 안에 같은 timeout을 적용하고 싶으면 클래스 레벨에 @Timeout 어노테이션을 선언하면 된다. 이렇게하면 특정 메소드나 @Nested 클래스 안에 @Timeout 어노테이션을 오버라이드만 하지 않으면, 모든 테스트, 테스트팩토리테스트템플릿 메소드에 일괄 적용된다.

@Timeout 어노테이션을 @TestFactory에서 사용하면, 각각의 DynamicTest 마다 타임아웃을 체크하는 것이 아니라, 전체의 DynamicTest에 대해서 타임아웃을 체크한다.

@Timeout이 @RepeatedTest 나 @ParameterizedTest 같은 @TestTemplate 메서드에 선언되어 있다면, 각각의 호출마다 timeout이 적용 된다.

다음의 설정 파라미터는 특정 카테고리에 있는 모든 메서드의 전체 타임아웃을 지정하는 방법이다. 이 방법은 @Timeout이 붙지 않은 테스트 클래스에 일괄 적용 된다.

junit.jupiter.execution.timeout.default

테스트 가능한 모든 메서드와 lifecylce 메서드에 기본적으로 일괄 적용된다.

junit.jupiter.execution.timeout.testable.method.default

테스트 가능한 모든 메서드에 기본적으로 일괄 적용된다.

junit.jupiter.execution.timeout.test.method.default

@Test 어노테이션이 붙은 메서드에 일괄 적용된다.

junit.jupiter.execution.timeout.testtemplate.method.default

@TestTemplate 메서드에 일괄 적용 된다.

junit.jupiter.execution.timeout.testfactory.method.default

@TestFactory 메서드에 일괄 적용 된다.

junit.jupiter.execution.timeout.lifecycle.method.default

모든 lifecylce 메서드에 일괄 적용 된다.

junit.jupiter.execution.timeout.beforeall.method.default

@BeforeAll 메서드에 일괄 적용 된다.

junit.jupiter.execution.timeout.beforeeach.method.default\

@BeforeEach 메서드에 일괄 적용 된다.

junit.jupiter.execution.timeout.aftereach.method.default

@AfterEachl 메서드에 일괄 적용 된다.

junit.jupiter.execution.timeout.afterall.method.default

@AfterAll 메서드에 일괄 적용 된다.

더 상세한 조건이 덜 상세한 조건을 오버라이드 한다.

예를 들어 junit.jupiter.execution.timeout.test.method.default 설정은 junit.jupiter.execution.timeout.testable.method.default 를 오버라이드 한다.

위에 있는 설정의 값은 대소문자 상관없이 다음의 포맷을 가진다. <number> [ns|μs|ms|s|m|h|d].

timeout 설정 파라미터 값에 대한 예제

파라미터 값 어노테이션을 사용할 시 동일한 값
42 @Timeout(42)
42 ns @Timeout(value = 42, unit = NANOSECONDS)
42 μs @Timeout(value = 42, unit = MICROSECONDS)
42 ms @Timeout(value = 42, unit = MILLISECONDS)
42 s @Timeout(value = 42, unit = SECONDS)
42 m @Timeout(value = 42, unit = MINUTES)
42 h @Timeout(value = 42, unit = HOURS)
42 d @Timeout(value = 42, unit = DAYS)

Polling Test에 @Timeout 사용하기

비동기 코드를 다뤄야 할 때 검증을 하기 전에, 어떤 일이 생기기를 기다리는 poll 테스트를 작성하는 것이 일반적이다. 어떤 경우에는 CountDownLatch나 다른 동기화 메카니즘으로 로직을 재작성 해야 한다. 그러나 이런 방법은 때때로 불가능할 수도 있다. 예를 들어, 테스트에 있는 대상이 외부에 있는 메세지 브로커로 채널에 메세지를 전송하면, 메세지가 성공적으로 채널로 전달될 때 까지 검증을 수행하지 못한다. 이와 같은 비동기 테스트에는 비동기 메시지가 성공적으로 전달되지 않는 경우와 같이 무기한 실행하여 테스트들이 중단되지 않도록 특정 형태의 시간 제한이 필요하다.

폴링하는 비동기 테스트에 대한 제한 시간을 구성하여 테스트가 무기한 실행되지 않도록 할 수 있다. 다음의 예제는 @Timeout 어노테이션을 이용한 예제다.

@Test
@Timeout(5)  // 최대 5초동안 실행
void pollUntil() throws InterruptedException {
  while (asynchronousResultNotAvailable()) {
    Thread.sleep(250); // 폴링 시간 설정
  }
  // 비동기 결과를 얻어서 검증을 실행
}

폴링 간격을 컨트롤하거나, 비동기 테스트를 유연하게 하고 싶으면 Awaitility 를 사용해보는 걸 추천한다.

전체적으로 @Timout 비활성화하기

디버그모드로 테스트를 돌릴 때, 고정된 타임아웃 제한은 테스트결과에 영향이 있을 수 있다. 예를 들어 모든 검증이 성공했음에도 테스트가 실패할 수 있다.

Junit jupiter는 junit.jupiter.execution.timeout.mode 설정 파라미터를 지원해서 enabled, disabled, disabled_on_debug 세가지 모드로 변경할 수 있다. 기본 모드는 enabled 이다. VM 런타임이 인풋 파라미터가 -agentlib:jdwp로 시작하는 파라미터가 있으면 디버그 모드로 실행된다.

병렬적 실행

병렬적 실행은 experimental 기능이다.

기본적으로 테스트들은 싱글스레드안에서 순차적으로 실행된다. 테스트를 병렬적으로 실행하면, 테스트 실행 속도가 빨라진다. 이 기능은 5.3부터 지원한다. 병렬적 실행을 활성화 하려면 junit.jupiter.execution.parallel.enabled 설정 파라미터를 true로 설정하면 된다.

위에서 파라미터를 true로 설정한 것은 첫번 째 단계에 불과하며, true로 설정해도 테스트는 기본적으로 순차적으로 실행 될 것이다. 이는 테스트 트리의 노드가 동시에 실행되는지 여부에 따라 실행 모드가 제어 된다. 다음의 두 가지 모드가 사용 가능 하다.

SAME_THREAD

부모가 사용하던 스레드를 사용한다. 예를 들어, 테스트 메소드를 사용할 때, 테스트 클래스에 있는 @BeforeAll이나 @AfterAll 메서드가 사용하던 스레드를 같이 사용 한다.

CONCURRENT

리소스 잠금이 동일한 스레드에서 강제로 실행하지 않는 한 동시에 실행한다.

기본적으로 테스트 트리에 있는 노드는 SAME_THREAD를 사용한다. junit.jupiter.execution.parallel.mode.default 설정 파라미터를 사용하면 기본값을 바꿀 수 있다. 아니면, @Execution 어노테이션을 사용해서 독립적으로 해당 테스트 또는 클래스만 실행모드를 변경할 수 있다.

모든 테스트를 병렬적으로 실행하는 파라미터 설정이다.

junit.jupiter.execution.parallel.enabled = true junit.jupiter.execution.parallel.mode.default = concurrent

기본 실행 모드는 Lifecycle.PER_CLASS 모드 또는 MethodOrderer 둘 중 하나가 테스트 트리의 모든 노드에 적용 된다. Lifecycle.PER_CLASS을 사용할 경우 테스트 작성자는 테스트 클래스가 스레드로부터 안전한지 확인해야 한다. MethodOrderer 경우, 동시 실행이 구성된 실행순서와 충돌할 수 있다. 따라서 두 경우 모두 `@Execution(CONCURRENT)`주석이 테스트 클래스 또는 메서드에 있는 경우에만 병렬적으로 실행된다.

CONCURRENT 실행 모드로 구성된 테스트 트리의 모든 노드는 선언적 동기화 메커니즘을 관찰하면서 제공된 구성에 따라 완전히 병렬로 실행된다.

추가적으로 junit.jupiter.execution.parallel.mode.classes.default 설정 파라미터를 클래스 설정할 수 있다. 이 설정을 위에 설정해준거랑 같이 사용하면 병렬적으로 실행되지만, 메소드들은 같은 스레드에서 실행된다.

테스트 클래스를 병렬적으로 실행하지만 클래스 마다 메서드를 같은 스레드안에서 실행하는 설정 파라미터

junit.jupiter.execution.parallel.enabled = true junit.jupiter.execution.parallel.mode.default = same_thread junit.jupiter.execution.parallel.mode.classes.default = concurrent

이와 반대적인 설정은, 하나의 클래스의 모든 메소드는 병렬적으로 실행되지만, 클래스 마다 순차적으로 실행된다.

테스트 클래스를 순차적으로 실행하지만 그 안에 있는 메서드는 병렬적으로 실행한다.

junit.jupiter.execution.parallel.enabled = true junit.jupiter.execution.parallel.mode.default = concurrent junit.jupiter.execution.parallel.mode.classes.default = same_thread

즉 junit.jupiter.execution.parallel.mode.default 은 테스트 메서드들을 병렬적으로 실행할건지에 대한 설정이고, junit.jupiter.execution.parallel.mode.classes.default 은 테스트 클래스들을 병렬적으로 실행할건지에 대한 설정이다.

파라미터에 따른 실행 예시

junit.jupiter.execution.parallel.mode.default 가 설정되지 않았으면, junit.jupiter.execution.parallel.mode.classes.default 의 값으로 대신 사용된다.

설정

ParallelExecutionConfigurationStrategy 을 이용하여 원하는 병렬과, 최대 풀 사이즈의 프로퍼티들을 설정할 수 있다. dynamic과 fixed 두개의 구현을 제공하며 커스텀해서 사용해도 된다.

사용하고 싶은 구현을 선택하려면 junit.jupiter.execution.parallel.config.strategy 설정 파라미터를 사용해야 한다.

dynamic

junit.jupiter.execution.paralle.config.dynamic.factor 설정 파라미터에 설정된 값과 사용가능한 프로세스와 코어 수를 곱하여 원하는 병렬 처리를 계산한다. (기본 값은 1)

fixed

필수적인 junit.jupiter.execution.parallel.config.fixed.parallelism 설정 파라미터를 원하는 병렬 처리로 사용 한다.

custom

필수적인 junit.jupiter.execution.parallel.config.custom.class 설정 파라미터를 통해 사용자 지정 ParallelExecutionConfigurationStrategy 구현을 지정하여 원하는 설정을 결정한다.

만약 어떠한 전략도 설정되지 않았으면, factor 1을 가진 dynamic 설정을 이용한다. 그렇게 되면, 병렬 구성은 프로세서/코어의 사용 가능한 수로 사용된다.

병렬처리는 최대 동시 스레드 수를 의미 하지 않는다.

Junit은 동시에 실행되는 테스트의 수가 설정된 병렬 처리를 초과하지 않을 것이라고 보장하지 않는다. 예를 들어 다음 세션에서 살펴 볼 동기화 메카니즘 중 하나인 ForkJoinPool을 사용할 때 그 뒤에는 충분한 병렬 처리로 실행이 계속 되도록 추가 스레드를 생성 한다. 따라서 테스트 클래스에서 이러한 보장이 필요한 경우 동시성을 제어하는 자체 수단을 사용해야 한다.

동기화(Synchronization)

실행 모드를 컨트롤 하기 위해서 @Execution 어노테이션을 이용한다. Junit은 또 다른 어노테이션 기반 선언적 동기화 메카니즘을 제공한다. @ResourceLock 어노테이션은 테스트 클래스나 메서드에 선언할 수 있으며, 안정적인 테스트 실행 보장하기 위해 동기화된 접근이 필요한 특정 공유 자원에 사용한다.

이런 공유 자원은 String 타입으로 유일한 이름을 갖도록하여 식별한다. 이름은 사용자가 정의하거나, Resources 상수 안에 미리 선언된 SYSTEM_PROPERTIES, SYSTEM_OUT, SYSTEMERR, LOCALE, TIME_ZONE을 사용할 수 있다.

만약 아래의 테스트가 @ResourceLock 어노테이션 없이 병렬하게 실행된다면 테스트가 이상해진다. 가끔은 테스트가 패스되고, 가끔은 race condition 때문에 실패하기도 한다.

@ResourceLock 어노테이션이 붙은 공유 자원에 접근하려고 할 때 Junit은 병렬적으로 실행되는 테스트에 충돌이 없게 보장한다.

격리된 테스트 실행

대부분의 테스트가 병렬적으로 실행되는데, 어떠한 동기화도 없이 실행되는 클래스라면,@Isolated 어노테이션을 이용하여 격리된 상태로 테스트를 실행할 수 있다. 이런 테스트 클래스는 다른 테스트와 동시에 실행되지 않고, 순차적으로 실행 된다.

공유 자원을 고유하게 식별하는 String 타입외에도 접근 모드를 지정해줄 수 있다. 공유 자원에 대한 READ 접근이 필요한 두 테스트는 서로 병렬로 실행 될 수 있지만, 공유 자원에 대한 READ_WRITE 접근이 필요한 다른 테스트가 실행되는 동안에는 실행되지 않는다. 즉 READ_WRTIE의 테스트가 전부 끝날 때 까지 대기한다.

@Execution(CONCURRENT)
class SharedResourcesDemo {

    private Properties backup;

    @BeforeEach
    void backup() {
        backup = new Properties();
        backup.putAll(System.getProperties());
    }

    @AfterEach
    void restore() {
        System.setProperties(backup);
    }

    @Test
    @ResourceLock(value = SYSTEM_PROPERTIES, mode = READ)
    void customPropertyIsNotSetByDefault() {
        assertNull(System.getProperty("my.prop"));
    }

    @Test
    @ResourceLock(value = SYSTEM_PROPERTIES, mode = READ_WRITE)
    void canSetCustomPropertyToApple() {
        System.setProperty("my.prop", "apple");
        assertEquals("apple", System.getProperty("my.prop"));
    }

    @Test
    @ResourceLock(value = SYSTEM_PROPERTIES, mode = READ_WRITE)
    void canSetCustomPropertyToBanana() {
        System.setProperty("my.prop", "banana");
        assertEquals("banana", System.getProperty("my.prop"));
    }

}

Built-in Extensions

Junit 팀은 재사용 가능한 extension이 분리된 라이브러리로 패키징되고 유지되도록 권장하지만, Junit Jupiter API 아티팩트는 유용하고 사용자가 다른 의존성을 추가할 필요 없는 몇 가지의 extension이 포함되어 있다.

TempDirectory Extension

@TempDir는 experimental 기능이다.

TempDirectory extension은 테스트클래스 안에 있는 독립적인 테스트 또는 모든 테스트에 대해 임시 디렉토리를 생성하고 정리를 할 때 사용한다. 이 기능을 사용하려면 접근 제어자가 private이 아닌 java.nio.file.Pathjava.io.File 필드에 @TempDir 어노테이션을 붙이거나, 파라미터에 붙여준다.

@Test 
void writeItemsToFile(@TempDir Path tempDir) throws IOException {
  Path file = tempDir.resolve("test.txt");
  new ListWriter(file).write("a", "b", "c");
  assertEquals(singletonList("a,b,c"), Files.readAllLines(file)); 
}

@TempDir는 생성자 파라미터에는 지원하지 않는다. 만약 라이프 사이클 메서드와 현재 테스트 메서드를 넘어서 임시 디렉토리를 유지하고 싶으면 접근제어자가 private이 아닌 인스턴스 필드에 @TempDir 어노테이션을 이용해 필드 인젝션을 이용해야 한다.

다음의 예제는 static field에 있는 공유 임시 디렉토리에 저장한다. 이렇게 작성하면 모든 라이프사이클 메서드와 테스트 메서드가 같은 sharedTempDir을 사용하게 된다.

모든 테스트가 임시 디렉토리를 공유하는 테스트 클래스

class SharedTempDirectoryDemo {

    @TempDir
    static Path sharedTempDir;

    @Test
    void writeItemsToFile() throws IOException {

        Path file = sharedTempDir.resolve("test.txt");
        new ListWriter(file).write("a", "b", "c");
        assertEquals(singletonList("a,b,c"), Files.readAllLines(file));
    }

    @Test
    void anotherTestThatUsesTheSameTempDir() {
        // use sharedTempDir 
    }
}

Junit4에서 마이그레이션 하기

JUnit Juptier 프로그래밍 모델과 extension 모델은 Junit4 특징인 Rules과 Runners에 네이티브하게 지원되지는 않지만, 반드시 JUnit5로 버전업을 해야되는건 아니다.

대신 JUnit은 Junit 플랫폼 인프라를 이용해 JUnit3와 와 JUnit4 기반의 테스트를 실행시켜주는 Junit Vintage 테스트 엔진을 통해 마이그레이션을 지원해준다. 모든 클래스와 어노테이션들은 새로운 패키지인 org.junit.jupiter 베이스 안에 존재하기 때문에 JUnit4와 JUnit Jupiter는 클래스패스에서 충돌날 일이 없다. 게다가 JUnit 팀은 Junit4에 대하여 지속적인 유지보수와 버그수정 릴리즈 진행하고 있다.

JUnit Platform에서 JUnit4 테스트 실행하기

먼저junit-vintage-engine 이 테스트 런타임 패스에 있는지 확인하자.

Categories 지원

@Category 어노테이션이 붙은 테스트 클래스나 메서드를 실행하기 위해, JUnit Vintage 테스트 엔진에 해당 테스트 식별자의 태그로 카테고리의 패키지를 포함한 클래스 이름을 적어야 한다. 예를 들어 @Category(Example.class) 어노테이션이 붙은 테스트 메서드가 있으면 JUnit 4에서는 Categories 러너에 com.acme.Example 태그와 같다.

마이그레이션 팁

다음의 주제들은 JUnit4 에서 JUnit Jupiter로 마이그레이션할 때 조심해야할 것들이다.

  • 어노테이션은 org.junit.jupiter.api 패키지에 있다.
  • Assertion은 org.junit.jupiter.api.Assertions 에 있다.
    • org.junit.Asssert 에 있는 assertion 메서드나 다른 assertion 라이브러리인 AssertJ, Hamcrest, Truth 등을 사용해도 된다.
  • Assumption은 org.junit.jupiter.api.Assumptions 에 있다.
  • @Before 와 @After는 더 이상 없다. 대신 @BeforeEach와 @AfterEach를 사용해야 한다.
  • @BeforeClass와 @AFterClass는 더 이상 없다. 대신 @BeforeAll와 @AfterAll를 사용해야 한다.
  • @Ignore는 더이상 없다. 대신 @Disabled나 내장된 조건실행(execution condition)을 사용하면 된다.
  • @Category는 더 이상 없다. 대신 @Tag를 사용하면 된다.
  • @RunWith는 더 이상 없다. 대신 @ExtendWith을 사용하면 된다.
  • @Rule과 @ClassRule은 더 이상 없다. 대신 @ExtendWith과 @RegisterExtenstion을 사용하면 된다.

제한된 JUnit 4 Rule 지원

JUnit4 Rule은 테스트 케이스를 실행하기 전후에 추가 코드를 실행할 수 있도록 도와준다. @Before와 @After로 선언된 메서드에서도 실행 전후처리로 코드를 넣을 수 있지만, JUnit4 Rule로 작성하면 재사용하거나 더 확장 가능한 기능으로 개발할 수 있는 장점이 있다..

위에 명시한 것 처럼 JUnit Jupiter에서 JUnit4의 rule은 지금도 지원하지 않고, 앞으로도 더 이상 네이티브하게 지원하지 않는다. 그러나 이미 많은 대규모 조직에서 JUnit 4 기반의 커스텀 rule을 사용하고 있다. 이러한 조직들을 위해서 점진적인 마이그레이션이 가능하도록 JUnit 팀은 JUnit4 rule을 JUnit Jupiter에서 그대로 지원하기로 결정했다.

JUnit jupiter에 있는 junit-jupiter-migrationsupport 이 현재 다음의 3개 타입의 Rule을 지원한다.

  • org.junit.rules.ExternalResource(org.junit.rules.TemporaryFolder 포함)
  • org.junit.rules.Verifier (org.junit.rules.ErrorCollector 포함)
  • org.junit.rules.ExpectedException

JUnit4와 마찬가지고 Rule 어노테이션을 필드와 메서드에 지원한다. 테스트 클래스에서 이러한 extension을 클래스 레벨에 사용하여 확장하면 JUnit4의 Rule 클래스 import 문을 포함하여 레거시 코드에 Rule 구현체를 변경하지 않고 유지할 수 있다.

이런 Rule 지원의 제약조건들은 클래스 레벨에 @EnableRuleMigrationSupport 어노테이션을 사용함으로써 완화할 수 있다. 이 어노테이션은 rule 마이그레이션 지원 extension인 VerifierSupport, ExternalResourceSupport, ExpectedExceptionSupport을 활성화 해주는 어노테이션으로 이루어져 있다. 대안적으로 마이그레이션을 지원하는 rules과 JUnit4의 @Ignore 어노테이션을 등록해주는 @EnableRuleMigrationSupport 을 활용할 수 있다.

그러나 JUnit5용의 새로운 extension을 개발할 계획이 있다면 JUnit4의 rule 기반 모델 대신 JUnit Jupiter 모델을 사용하도록 하자.

JUnit 4 @Ignore 지원

Jupiter의 @Disabled 어노테이션과 비슷하게 JUnit4의 @Ignore 어노테이션은 junit-jupiter-migrationsupport 모듈에서 지원한다.

JUnit Jupiter 기반의 테스트에서 @Ignore을 사용하려면, 빌드안에 junit-jupiter-migrationsupport 테스트 의존성을 설정한 다음 테스트 클래스에 @ExtendWith(IgnoreCondition.class)@EnableJunit4MigrationSupport를 붙이면 된다. @EnableJunit4MigrationSuppor는 자동으로 제한된 JUnit Rule 지원과 함께 IgnoreCondition을 등록한다. IgnoreCondition은 테스트 클래스나 메서드에 @Ignore 어노테이션이 붙은 것들을 비활성화 하는 ExecutionCondition이다.

import org.junit.Ignore;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.migrationsupport.EnableJUnit4MigrationSupport;

// @ExtendWith(IgnoreCondition.class) 
@EnableJUnit4MigrationSupport 
class IgnoredTestsDemo {

	@Ignore
  @Test 
  void testWillBeIgnored() { }

	@Test 
  void testWillBeExecuted() { }

}

테스트 실행하기

IDE 지원

Intellij IDEA

Intellij는 2016.2 버전부터 JUnit 플랫폼에서 테스트 실행을 지원한다. 자세한 내용은 Intellij IDEA blog 에 있다. 그러나 2017.3 또는 최신버전을 사용하는걸 추천하는데, IDEA가 프로젝트에 사용하고 있는 API 버전(junit-platform-launcher, junit-jupiter-engine, junit-vintage-engine)을 자동적으로 다운로드 하기 때문이다.

예를 들어 5.7.0 같은 JUnit 5의 다른 버전을 사용하고 싶으면 다음과 같이 사용하면 된다.

Gradle

// Only needed to run tests in a version of IntelliJ IDEA that bundles older versions
testRuntimeOnly("org.junit.platform:junit-platform-launcher:1.7.0") 
testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.7.0") 
testRuntimeOnly("org.junit.vintage:junit-vintage-engine:5.7.0")

maven

<!-- Only needed to run tests in a version of IntelliJ IDEA that bundles older versions -->
<dependency>
	<groupId>org.junit.platform</groupId>
	<artifactId>junit-platform-launcher</artifactId>
	<version>1.7.0</version>
	<scope>test</scope>
</dependency>rd>
	<artifactId>junit-jupiter-engine</artifactId>
	<version>5.7.0</version>
	<scope>test</scope>
</dependency>
<dependency>
	<groupId>org.junit.vintage</groupId>
	<artifactId>junit-vintage-engine</artifactId>
	<version>5.7.0</version>
	<scope>test</scope>
</dependency>

이클립스,넷빈즈,VS 코드는 생략

빌드 지원

Gradle

JUnit 플랫폼 Gradle 플러그인은 현재 중단되었다.

JUnit 팀이 만든 junit-platform-gradle-plugin 은 JUnit Platform 1.2에서 deprecated 되었으며, 1.3에서 중단이 되었다. Gradle의 스탠다드 test task로 변경해야 한다.

4.6버전부터 Gradle은 JUnit 플랫폼에서 테스트를 실행할 수 있도록 네이티브로 지원한다. 활성화 하려면 다음과 같이 build.gradle 안에 test task안에 userJunitPlatform()을 지정해줘야 한다.

test { 
  useJUnitPlatform() 
}

태그나 엔진으로 필터링 하는것도 지원한다.

test {
		useJUnitPlatform { 
      includeTags 'fast', 'smoke & feature-a'
      // excludeTags 'slow', 'ci' 
      includeEngines 'junit-jupiter' 
      // excludeEngines 'junit-vintage' }
}

Gradle 공식문서 에 들어가면 좀 더 종합적인 옵션들을 볼 수 있다.

설정 파라미터

표준 Gradle test task는 JUnit 플랫폼의 설정 파라미터를 설정하기 위한 DSL(도메인 특화 언어)를 현재 제공하지 않는다. 그러나 시스템 프로퍼티나 junit-platform.priperties 파일을 통해서 빌드 스크립트 안에 설정 파일을 제공해줄 수 있다.

우리가 계속적으로 살펴봤던 설정 파라미터를 빌드 할 때 다음과 같이 작성해 줄 수 있는 것이다.

test {
  // ...
  systemProperty 'junit.jupiter.conditions.deactivate', '*'
  systemProperties = [
    'junit.jupiter.extensions.autodetection.enabled':'true',
    'junit.jupiter.testinstance.lifecycle.default':'per_class'
    ] 
  // ...
}

테스트 엔진 설정하기

테스트를 실행하기 위해 클래스패스에 TestEngine 구현체가 있어야 한다.

JUnit jupiter 기반의 테스트 지원을 설정하기 위해 다음과 같이 JUnit Jupiter API에 대한 testImplementation 의존성 및 JUnit Jupiter TestEngine 구현에 대한 testRuntimeOnly 의존성을 구성해야 한다.

dependencies { 
  testImplementation("org.junit.jupiter:junit-jupiter-api:5.7.1") 
  testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.7.1") 
}

다음과 같이 JUnit 플랫폼은 testImplementation 의존성을 JUnit4 용과 Junit Vintage TestEngine 구현체를 다음과 같이 설정하면 JUnit4 기반의 테스트를 이용할 수 있다.

dependencies { 
  testImplementation("junit:junit:4.13")
  testRuntimeOnly("org.junit.vintage:junit-vintage-engine:5.7.1") 
}

로깅 추가하기 (선택)

JUnit은 warning 로깅과 디버그 정보를 찍기 위해 일명 JUL이라는 java.util.logging 패키지안에 있는 자바 로깅 API를 사용한다. 자세한 내용은 LogManager 설정 옵션을 참고하면 된다.

아니면 대안적으로 Log4J나 Logback같은 다른 로깅 프레임워크를 이용해 로그 메세지를 이용하는 것도 가능하다. LogManger의 커스텀 구현으로 제공되는 로깅 프레임워크를 사용하고 싶다면, 사용할 할 LogManager 구현체의 풀네임으로 java.util.logging.manager 시스템 변수에 설정한다. 다음의 예제는 Log4j 2.x 버전을 설정하는 예제다.

test { 
  systemProperty 'java.util.logging.manager', 'org.apache.logging.log4j.jul.LogManager'
}

Maven

JUnit Platfrom Maven Surefire Provider는 중단되었다.

junit-platform-surefire-provider 는 원래 JUnit 팀이 개발했었는데, JUnit Platform 1.3에서 deprecated 되었으며, 1.4에서는 중단되었다. 대신 Maven Surefire’s native 를 사용하자.

version 2.22.0 부터, Maven Surefire와 Maven Failsafe는 JUnit Platform에서 실행되는 테스트에 대해 native 지원 을 한다. Junit5-jupiter-starter-maven안의 pom.xml 파일은 Maven Surefire 플러그인을 어떻게 사용하는지 보여주며, 메이븐 빌드를 위한 설정을 시작할 수 있다.

Test Engines 설정하기

Maven Surefire와 Maven Failsafe가 어디서든지 테스트를 실행하기 위해서 테스트클래스패스에 적어도 한개의 TestEngine 구현체가 필요하다.

JUnit jupiter 기반 테스트를 설정하려면 JUnit Jupiter API와 JUnit Jupiter TestEngine 구현의 의존성을 test 범위로 설정해준다.

<build>
	<plugins>
		<plugin>
			<artifactId>maven-surefire-plugin</artifactId>
			<version>2.22.2</version>
		</plugin>
		<plugin>
			<artifactId>maven-failsafe-plugin</artifactId>
			<version>2.22.2</version>
		</plugin>
	</plugins>
</build>
<!-- ... -->
<dependencies>
	<!-- ... -->
	<dependency>
		<groupId>org.junit.jupiter</groupId>
		<artifactId>junit-jupiter-api</artifactId>
		<version>5.7.1</version>
		<scope>test</scope>
	</dependency>
	<dependency>
		<groupId>org.junit.jupiter</groupId>
		<artifactId>junit-jupiter-engine</artifactId>
		<version>5.7.1</version>
		<scope>test</scope>
	</dependency>
	<!-- ... -->
</dependencies>
<!-- ... -->

Maven Surefire과 Maven Failsafe는 다음과 같이 test 범위로 JUnit4와 JUnit Vintage TestEngine 구현체를 설정하면 JUnit4 기반의 테스트를 돌릴 수 도 있다.

<build>
	<plugins>
		<plugin>
			<artifactId>maven-surefire-plugin</artifactId>
			<version>2.22.2</version>
		</plugin>
		<plugin>
			<artifactId>maven-failsafe-plugin</artifactId>
			<version>2.22.2</version>
		</plugin>
	</plugins>
</build>
<!-- ... -->
<dependencies>
	<!-- ... -->
	<dependency>
		<groupId>org.junit.jupiter</groupId>
		<artifactId>junit-jupiter-api</artifactId>
		<version>5.7.1</version>
		<scope>test</scope>
	</dependency>
	<dependency>
		<groupId>org.junit.jupiter</groupId>
		<artifactId>junit-jupiter-engine</artifactId>
		<version>5.7.1</version>
		<scope>test</scope>
	</dependency>
	<!-- ... -->
</dependencies>
<!-- ... -->
<build>
	<plugins>
		<plugin>
			<artifactId>maven-surefire-plugin</artifactId>
			<version>2.22.2</version>
		</plugin>
		<plugin>
			<artifactId>maven-failsafe-plugin</artifactId>
			<version>2.22.2</version>
		</plugin>
	</plugins>
</build>
<!-- ... -->
<dependencies>
	<!-- ... -->
	<dependency>
		<groupId>junit</groupId>
		<artifactId>junit</artifactId>
		<version>4.13</version>
		<scope>test</scope>
	</dependency>
	<dependency>
		<groupId>org.junit.vintage</groupId>
		<artifactId>junit-vintage-engine</artifactId>
		<version>5.7.1</version>
		<scope>test</scope>
	</dependency>
	<!-- ... -->
</dependencies>

테스트 클래스 이름으로 필터링 하기

Maven Surefire 플러그인은 다음의 패턴에 전부 일치하는 테스트 클래스를 스캔할 수 있다.

  • **/Test*.java
  • **/*Test.java
  • **/*Tests.java
  • **/*TestCase.java

추가적으로 static 멤버 클래스를 포함해서 모든 중첩 클래스는 기본적으로 제외된다.

그러나 pom.xml 파일안에 설정하면 기본동작을 변경할 수 있다. 예를 들어 static 멤버 클래스만 제외하고 싶으면 다음과 같이 규칙을 오버라이드 하면 된다.

<!-- ... -->
<build>
	<plugins>
		<plugin>
			<artifactId>maven-surefire-plugin</artifactId>
			<version>2.22.2</version>
			<configuration>
				<excludes>
					<exclude/>
				</excludes>
			</configuration>
		</plugin>
	</plugins>
</build>
<!-- ... -->

좀 더 자세한 내용은 여기 를 참고하면 된다.

태그로 필터링 하기

테스트를 다음과 같이 속성을 설정하면 tag과 tag expression으로 필터링할 수 있다.

  • tag나 tag expression을 포함하고 싶으면 groups 를 사용하자.
  • tag나 tag expression을 제외하고 싶으면 excludedGroups 를 사용하자.
<!-- ... -->
<build>
	<plugins>
		<plugin>
			<artifactId>maven-surefire-plugin</artifactId>
			<version>2.22.2</version>
			<configuration>
				<groups>acceptance | !feature-a</groups>
				<excludedGroups>integration, regression</excludedGroups>
			</configuration>
		</plugin>
	</plugins>
</build>
<!-- ... -->

설정 파라미터

<!-- ... -->
<build>
	<plugins>
		<plugin>
			<artifactId>maven-surefire-plugin</artifactId>
			<version>2.22.2</version>
			<configuration>
				<properties>
					<configurationParameters> junit.jupiter.conditions.deactivate = * junit.jupiter.extensions.autodetection.enabled = true junit.jupiter.testinstance.lifecycle.default = per_class </configurationParameters>
				</properties>
			</configuration>
		</plugin>
	</plugins>
</build>
<!-- ... -->

콘솔 런처

콘솔 런처는 콘솔에서 JUnit 플랫폼을 실행할 수 있게 해주는 커맨드라인 자바 애플리케이션이다. JUnit Vintage와 JUnit Jupiter 테스트를 실행한 후 테스트의 결과를 콘솔로 출력해준다.

모든 의존성을 포함한 junit-platform-console-standalone-1.7.0.jar 는 Maven Central 레파지토리 안에 junit-platform-console-standalone 디렉토리 안에 있다.

아래와 같이 독립적인 콘솔런처를 실행할 수 있다.

java -jar junit-platform-console-standalone-1.7.0.jar

├─ JUnit Vintage
│ └─ example.JUnit4Tests
│ └─ standardJUnit4Test ✔
└─ JUnit Jupiter
	├─ StandardTests
  │ ├─ succeedingTest() ✔
  │ └─ skippedTest() ↷ for demonstration purposes
  └─ A special test case
		├─ Custom test name containing spaces ✔
		├─ ╯°□°)╯ ✔
		└─ 挐 ✔

Test run finished after 64 ms 
[ 5 containers found ]
[ 0 containers skipped ]
[ 5 containers started ]
[ 0 containers aborted ]
[ 5 containers successful ]
[ 0 containers failed ]
[ 6 tests found ] 
[ 1 tests skipped ]
[ 5 tests started ]
[ 0 tests aborted ]
[ 5 tests successful ]
[ 0 tests failed ]

Exit Code

테스트가 실패하면 status 코드가 1 ,테스트할 메서드가 존재하지 않고, 실행시 --fail-if-no-tests 실행옵션을 줬으면 status 코드가 2, 그 외에는 0이다.

다시 돌아와서 Extension에 대해서 알아보자.

Extension Model

JUnit4을 위한 extension인 Runner , TestRule ,MethodRule 과 대조적으로 JUnit Jupiter의 extension 모델은 단일적이고 일관성이 있다.

Extension 등록하기

Extension은 @ExtendWith 를 이용해서 선언적으로 Extension을 등록할 수 있다. 혹은 @RegisterExtension 이나 Java의 ServiceLoader 메카니즘을 이용해 자동적으로 등록할 수도 있다.

선언적 Extension 등록

테스트 인터페이스, 테스트 클래스, 테스트 메서드, @ExtendWith(..) 이 포함된 커스텀 어노테이션에 어노테이션을 이용해 선언적으로 선언하여 등록할 extension을 제공해줌으로써 extension을 등록할 수 있다.

예를 들어 특정 테스트 메서드에 커스텀 RandomParametersExtension 을 등록하고 싶다면 다음과 같이 사용하면 된다.

@ExtendWith(RandomParametersExtension.class)
@Test
void test(@Random int i) {
  // ...
}

특정 클래스와 서브클래스에 등록하고 싶다면 다음과 같이 사용한다.

@ExtendWith(RandomParametersExtension.class)
class MyTests {
  
}

여러 개의 extension을 등록하려면 다음과 같이 사용한다.

@ExtendWith({ DatabaseExtension.class, WebServerExtension.class})
class MyFirstTests {
  
}

아니면 다음과 같이 분리해서 사용해도 된다.

@ExtendWith(DatabaseExtension.class)
@ExtendWith(WebServerExtension.class)
class MySecondTests {
  
}

Extension 등록 순서

Extension의 등록 순서에 따라 코드가 실행된다. 위에 코드로 예를 들면 DatabaseExtension이 먼저 실행 된 후 WebServerExtension 이 실행 된다.

재 사용 가능한 여러개의 extension을 조합하고 싶다면 다음과 같이 커스텀으로 조합된 어노테이션을 사용하면 된다.

@Target({ ElementType.TYPE, ElementType.METHOD }) 
@Retention(RetentionPolicy.RUNTIME)
@ExtendWith({ DatabaseExtension.class, WebServerExtension.class })
public @interface DatabaseAndWebServerExtension { }

코드로 extension 등록하기

테스트 클래스 안에서 @RegisterExtension 어노테이션을 필드에 선언해서 extension을 등록할 수 있다.

@ExtendWith 을 통해서 선언적으로 extension이 등록 될 때 어노테이션으로 통해서만 설정이 가능하다. 이와 반대로 @RegisterExtension 을 통해서 extensio n을 등록하면 extension의 생성자나 정적 팩터리 메서드나 빌더 API에게 인자를 넘겨주는 일도 가능하다.

Extension 등록 순서

기본적으로 @RegisterExtension 으로 등록된 extension은 내부적으로 이미 결정된 순서에 따라 실행된다. 그러므로 명시적으로 순서를 정해주고 싶다면 @RegisterExtension 필드에 @Order 어노테이션으로 순서를 정해주자.

@Order 어노테이션이 붙지 않은 @RegisterExtension 필드는 Integer.MAX_VALUE 를 2로 나눈 값을 가진 디폴트 순서를 가진다. 이를 통해 @Order 어노테이션이 추가 된 필드를 어노테이션이 없는 필드 앞이나 뒤에 명시적으로 정렬할 수 있다. 기본 순서 값이 명시된 순서 값 보다 낮은 extension은 어노테이션이 붙지 않은 extension보다 먼저 등록된다.

@RegisterExtension 필드는 반드시 private 또는 null이 아니여야 하며 static이나 non-static 이여도 된다.

Static Fields

@RegisterExtension 어노테이션이 적용된 필드가 static 이라면 extension은 클래스 레벨에 선언된 @ExtendWith 어노테이션의 extension들이 먼저 등록된 후 등록된다. 그러므로 static 필드를 통해서 등록된 extension은 클래스레벨에 구현하거나 BeforeAllCallback , AfterAllCallback , TestInstancePostProcessor , TestInstancePreDestroyCallback처럼 인스턴스 레벨에 구현하거나 BeforeEachCallback 처럼 메소드 레벨에 구현해야 한다.

다음의 예제에서는 테스트 클래스에 있는 server 필드가 WebServerExtension 이 지원해주는 빌더 패턴으로 초기화 된다. 예를 들어 모든 테스트가 시작하기 전에 서버를 시작하고 모든 테스트가 끝난 후 서버를 종료하는 WebServerExtension 은 클래스 레벨에 extension 으로 자동으로 등록된다. 추가적으로 @BeforeAll @AfterAll static 라이프사이클 메서드 과 @BeforeEach , @AfterEach @Teset 메서드는 필요하면 server 필드를 통해서 extension의 인스턴스에 접근할 수 있다.

static 필드를 통해서 등록된 extension 예제

class WebServerDemo {

    @RegisterExtension
    static WebServerExtension server = WebServerExtension.builder()
            .enableSecurity(false)
            .build();

    @Test
    void getProductList() {
      WebClient webClient = new WebClient();
      String serverUrl = server.getServerUrl(); 
      assertEquals(200, webClient.get(serverUrl + "/products").getResponseStatus());
    }

}

Instacne Fields

만약 @RegisterExtension 어노테이션이 붙은 필드가 static이 아니라면(인스턴스 변수라면), 이 extension은 테스트 클래스가 초기화되고 각각의 등록된 TestInstancePostProcessor 가 테스트 인스턴스를 후 처리된 후 등록 된다. 그러므로 이런 extension 들은 BeforeAllCallback , AfterAllCallback , TestInstancePostProcessor 를 이용해서 구현해야 한다. 기본적으로 인스턴스 extension은 @ExtendWtih 어노테이션을 통해 메서드 레벨에 등록된 extension 다음에 등록된다. 그러나 만약 테스트 클래스가 @TestInstance(Lifecycle.PER_CLASS) 가 설정이 되어있다면, 인스턴스 extension은 @ExtendWtih 어노테이션을 통해 메서드 레벨에 등록된 extension 전에 등록된다.

다음의 예제는 테스트 클래스에 있는 docs 필드는 lookUpDocsDir() 메서드가 호출되고, 결과가 forPath에 전달 될 때 초기화 된다. 설정된 DocumentationExtension은 자동적으로 메소드 레벨로 extension이 등록된다. 게다가 @BeforeEach, @AfterEach , @Test 메서드는 필요하면 docs 필드를 통해 extension에 접근할 수 있다.

인스턴스 필드를 통해서 extension 등록하기

class DocumentationDemo {

    static Path lookUpDocsDir() {
        // 문서 경로를 적는다.
        return null;
    }

    @RegisterExtension
    DocumentationExtension docs = DocumentationExtension.forPath(lookUpDocsDir());

    @Test
    void generateDocumentation() {
        // use this.docs ...
    }
}

class DocumentationExtension implements AfterEachCallback {

    private final Path path;

    private DocumentationExtension(Path path) {
        this.path = path;
    }

    static DocumentationExtension forPath(Path path) {
        return new DocumentationExtension(path);
    }

    @Override
    public void afterEach(ExtensionContext context) {
        /* no-op for demo */
    }
}

자동으로 Extension 등록하기

선언적 Extension 등록과 코드로 Extension 등록하는 방법은 어노테이션을 이용하지만, 자바의 java.util.ServiceLoader 메카니즘을 이용하여 전역 extension을 등록할 수 있다.이 메카니즘은 서드파티 extension을 자동 감지해서 자동으로 등록할 수 있게 해준다.

특별하게도, 커스텀 정의된 extension은 /META-INF/services 폴더 안에 org.junit.jupiter.api.extension.Extension으로 된 파일에 해당 extension 클래스 이름을 전체를 제공함으로써 등록할 수 있다.

자동 Extension 감지 활성화

자동감지는 고급기능이라서 기본적으로 활성화 되어있지 않다. 활성화 하려면 junit.jupiter.extensions.autodetection.enabled 설정 파라미터를 true로 변경해야 한다. LauncherDiscoveryRequestLauncher 를 이용해 설정 파라미터로 JVM 시스템 프로퍼티로 전달 해줄 수도 있거나, JUnit Platform 설정 파일을 통해서 설정해줄 수 있다.

예를 들어 자동 감지를 활성화 하려면 JVM을 시작할 때 다음과 같은 시스템 변수와 함께 시작시킨다.

-Djunit.jupiter.extensions.autodetection.enabled=true

자동 감지가 활성화 되면, JUnit Jupiter의 글로벌 extension이 등록된 후 ServiceLoader 메카니즘을 통해서 extension이 등록 된다.

Extension 상속

등록된 extension은 테스트 클래스 계층에서 상속될 수 있다. 이와 비슷하게 탑레벨에 등록된 extension는 메소드레벨에 상속 된다. 특정 extension 구현체는 주어진 extension context나 부모 context에서 한번만 오직 등록될 수 있다. 결론은 중복된 extension 구현체를 등록하려고 하면 무시된다.

조건부 테스트 실행

ExecutionCondition 은 조건부 테스트 실행의 관한 Extension API를 정의해 놓았다.

ExecutionCondition은 제공된 ExtensionContext 를 바탕으로 이 테스트를 실행할지 말지 각각의 테스트 컨테이너를 확인한다.

여러 개의 ExecutionCondition이 등록이 되면, 그 중에 하나라도 비활성화가 되면 테스트를 비활성화 한다. 그러므로 이미 앞에서 어떤 조건때문에 비활성화 되었는지 모르므로 컨디션이 제대로 동작하는지 보장을 할 수가 없게 된다.

조건부 비활성화

가끔 특정 조건부를 비활성화하고 테스트를 돌리고 싶을 때가 있다. 예를 들어 @Disable로 선언된 테스트가 아직도 정상작동 안하는지 확인하고 싶을 때가 있다. 이런 경우 테스트를 실행하려면 junit.jupiter.conditions.deactive 설정 파라미터로 어떤 조건을 비활성화 할건지 패턴식으로 제공해주면 된다.

예를 들어 @Disable 조건을 비활성화 하고 싶으면 JVM 실행할 때 다음의 시스템 변수를 포함해야 한다.

-Djunit.jupiter.conditions.deactivate=org.junit.*DisabledCondition

테스트 인스턴스 팩토리

TestInstanceFactory 는 테스트 클래스 인스턴스 생성에 관련한 Extension API를 정의해 놓았다.

일반적인 사용 사례는 DI 프레임워크에서 테스트 인스턴스를 가져오거나, 테스트 클래스 인스턴스를 만들기 위해 정적 팩터리 메서드를 호출하는 것이다.

만약 TestInstanceFactory가 등록되지 않았으면, 프레임워크는 테스트 클래스를 초기화 하기 위해 하나의 생성자를 호출한다.

TestInstnaceFactory를 구현한 Extension은 테스트 인터페이스, 테스트 클래스 탑레벨, @Nested 테스트 클래스에 등록될 수 있다.

TestInstanceFactory를 구현한 구현체를 여러개 등록하면 테스트에 예외가 발생한다. 그리고, 부모 클래스에 정의된 구현체를 하위 구현체가 상속 받으니, 실수로 중복된 구현체를 등록하지 않도록 주의하자.

테스트 인스턴스 후처리기

TestInstancePostProcessor는 테스트 인스턴스의 후처리기를 위한 API를 정의해 놓았다.

일반적인 사용 사례는 테스트인스턴스에 의존성을 주입하거나, 테스터 인스턴스에 있는 커스텀된 초기화 에서드를 호출할 때다.

이걸 제대로 사용한 사례는 MockitoExtension과 SpringExtension이다.

테스트 인스턴스 Pre-destory 콜백

TestInstancePreDestoryCallback 은 테스트 인스턴스가 사용 된 후 destory 하기 전에 처리할 작업들을 위한 Extension이다.

일반적인 사용 사례에는 테스트 인스턴스에 삽입 된 의존성 정리, 테스트 인스턴스에서 사용자 지정 초기화 해제 메서드 호출 등이 포함된다.

Parameter Resolution

ParameterResolver 는 런타임시 동적으로 파라미터를 선택하기 위한 Extension이다.

테스트 클래스 생성자, 테스트 메서드, 라이플사이클 메서드에 파라미터를 선언하면, 그 파라미터는 ParameterResolver 에 의해 런타임시 파라미터가 반드시 선택된다. ParameterResolver 는 기본적으로 내장되있거나, 사용자가 등록할 수 있다. 일반적으로 파라미터는 이름,타입,어노테이션 또는 이들의 조합으로 선택된다.

타입으로 파라미터를 선택하고 싶으면 ParameterResolver를 커스텀한 TypeBasedParameterResolver를 쓰면 된다.

같이 보면 좋은 클래스는 CustomTypeParameterResolver, CustomAnnotationParameterResolver, MapOfListsTypeBasedParameterResolver 를 보면 좋을 것 같다.

테스트 결과 처리

TestWatcher테스트 메서드 실행의 결과를 처리하고 싶을 때 사용하는 Extension이다. TestWatch는 다음의 이벤트에 대한 컨텍스트 정보와 함께 호출된다.

  • testDisabled : 비활성화 된 테스트 메서드가 스킵 된 후 호출 된다.
  • testSuccessful : 테스트 메서드가 성공적으로 완료된 후 호출된다.
  • testAborted : 테스트가 중지되고 나서 호출 된다.
  • testFailed : 테스트 메서드가 실패 된 후 호출 된다.

테스트 라이플사이클 콜백

다음 인터페이스는 테스트 실행 수명주기의 다양한 지점에서 테스트를 확장하기위한 API를 정의 한다.

더욱 자세한 내용과 예제를 알고 싶으면 org.junit.jupiter.api.extension 패키지안의 인터페이스를 JavaDoc에서 살펴보면 된다.

  • BeforeAllCallback
    • BeforeEachCallback
      • BeforeTestExecutionCallback
      • AfterTestExecutionCallback
    • AfterEachCallback
  • AfterAllCallback

여러 개의 Extension API 구현 하기

Extension 개발자들은 하나의 extension에 이런 인터페이스들을 구현했을 지도 모른다. 자세한 예제를 보려면 SpringExtension 소스코드를 살펴보자.

테스트 실행 전, 후 콜백

BeforeTestExecutionCallback과 AfterTestExecutionCallback은 테스트 메서드 실행 전이나 후에 즉시 실행하고 싶은 행동들을 정의한 Extension이다. 예를 들어 이 콜백은 시간을 재거나, 추적할 때 가장 잘 알맞다. 만약 @BeforeEach나 @AfterEach 어노테이션이 붙은 메서드 근처에서 호출하고 싶으면 BeforeEachCallback과 AfterEachCallback을 구현하자.

다음의 예제는 테스트 메서드의 실행시간을 계산하고, 로그를 찍는콜백이다. TimingExteion은 BeforeTestExecutionCallbackAfterTestExecutionCallback 두개 모두 구현 했다.

import java.lang.reflect.Method;
import java.util.logging.Logger;

import org.junit.jupiter.api.extension.AfterTestExecutionCallback;
import org.junit.jupiter.api.extension.BeforeTestExecutionCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ExtensionContext.Namespace;
import org.junit.jupiter.api.extension.ExtensionContext.Store;

public class TimingExtension implements BeforeTestExecutionCallback, AfterTestExecutionCallback {

    private static final Logger logger = Logger.getLogger(TimingExtension.class.getName());

    private static final String START_TIME = "start time";

    @Override
    public void beforeTestExecution(ExtensionContext context) {
        getStore(context).put(START_TIME, System.currentTimeMillis());
    }

    @Override
    public void afterTestExecution(ExtensionContext context) {
        Method testMethod = context.getRequiredTestMethod();
        long startTime = getStore(context).remove(START_TIME, long.class);
        long duration = System.currentTimeMillis() - startTime;

        logger.info(() ->
                String.format("Method [%s] took %s ms.", testMethod.getName(), duration));
    }

    private Store getStore(ExtensionContext context) {
        return context.getStore(Namespace.create(getClass(), context.getRequiredTestMethod()));
    }

}

TimingExtensionTests 클래스는 @ExtendWith 어노테이션을 이용해서 TimingExtension을 등록했기 때문에, 실행시 실행시간을 잴 수 있다.

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

@ExtendWith(TimingExtension.class)
class TimingExtensionTests {

    @Test
    void sleep20ms() throws Exception {
        Thread.sleep(20);
    }

    @Test
    void sleep50ms() throws Exception {
        Thread.sleep(50);
    }

}
INFO: Method [sleep20ms] took 24 ms.
INFO: Method [sleep50ms] took 53 ms.

예외 핸들링

테스트 실행 중에 발생한 예외는 추가적으로 전파되기 전에 그에 따라 가로채서 처리할 수 있다. 그래서 에러 로깅이나 리소스 해제 같은 특정 액션들은 특별화된 Extension으로 정의하기도 한다. JUnit Jupiter는 TestExecutionExceptionHandler를 통해 @Test 메서드 중에 던져진 예외를 처리하고 LifecycleMethodExecutionExceptionHandler를 통해 테스트 라이플사이클 메서드 (@BeforeAll, @BeforeEach, @AfterEach 및 @AfterAll) 중 하나에서 던져진 예외를 처리하려는 확장 API를 제공한다.

다음 예제는 IOException이 모든 인스턴스를 삼키gi 다른 유형의 예외를 다시 발생시키는 Extension이다.

class IgnoreIOExceptionExtension implements TestExecutionExceptionHandler {

    @Override
    public void handleTestExecutionException(ExtensionContext context, Throwable throwable)
            throws Throwable {

        if (throwable instanceof IOException) {
            return;
        }
        throw throwable;
    }

}
class RecordStateOnErrorExtension implements LifecycleMethodExecutionExceptionHandler {

    @Override
    public void handleBeforeAllMethodExecutionException(ExtensionContext context, Throwable ex)
            throws Throwable {
        memoryDumpForFurtherInvestigation("Failure recorded during class setup");
        throw ex;
    }

    @Override
    public void handleBeforeEachMethodExecutionException(ExtensionContext context, Throwable ex)
            throws Throwable {
        memoryDumpForFurtherInvestigation("Failure recorded during test setup");
        throw ex;
    }

    @Override
    public void handleAfterEachMethodExecutionException(ExtensionContext context, Throwable ex)
            throws Throwable {
        memoryDumpForFurtherInvestigation("Failure recorded during test cleanup");
        throw ex;
    }

    @Override
    public void handleAfterAllMethodExecutionException(ExtensionContext context, Throwable ex)
            throws Throwable {
        memoryDumpForFurtherInvestigation("Failure recorded during class cleanup");
        throw ex;
    }


    private void memoryDumpForFurtherInvestigation(String error) {

    }

}
//@Test, @BeforeEach, @AfterEach @BeforeAll and @AfterAll 에 사용될 핸들러 등록
@ExtendWith(ThirdExecutedHandler.class)
class MultipleHandlersTestCase {

    // @Test, @BeforeEach, @AfterEach 에만 사용할 핸들러를 등록.
    @ExtendWith(SecondExecutedHandler.class)
    @ExtendWith(FirstExecutedHandler.class)
    @Test
    void testMethod() {
        throw new RuntimeException("예외를 던지면 FirstExecutedHandler가 실행된다.");
    }


    static class FirstExecutedHandler implements TestExecutionExceptionHandler {
        @Override
        public void handleTestExecutionException(ExtensionContext context, Throwable ex)
                throws Throwable {
            System.out.println("FirstExecutedHandler");
            throw ex;
        }
    }

    static class SecondExecutedHandler implements LifecycleMethodExecutionExceptionHandler {
        @Override
        public void handleBeforeEachMethodExecutionException(ExtensionContext context, Throwable ex)
                throws Throwable {
            System.out.println("SecondExecutedHandler");
            throw ex;
        }
    }
}

class ThirdExecutedHandler implements LifecycleMethodExecutionExceptionHandler {
    @Override
    public void handleBeforeAllMethodExecutionException(ExtensionContext context, Throwable ex)
            throws Throwable {
        System.out.println("ThirdExecutedHandler");
        throw ex;
    }
}

선언 순서에 따라 동일한 라이플사이클 메서드에 대해 여러 실행 예외 처리기를 호출 할 수 있다. 핸들러 중 하나가 처리 된 예외를 삼키면 나머지 예외가 실행되지 않고, 예외가 한번도 던져지지 않은 것 처럼 테스트 실패가 전파되지 않는다. 핸들러는 예외를 다시 던지거나, 다른 예외를 던지도록 선택할 수 있다.

호출 가로채기

InvocationIntercepot 는 테스트 코드의 호출을 가로채기 위해 사용하는 Extension이다.

다음 예제는 Swing의 이벤트 디스패치 스레드에서 모든 테스트 메소드를 실행하는 Extension 이다.

public class SwingEdtInterceptor implements InvocationInterceptor {

    @Override
    public void interceptTestMethod(Invocation<Void> invocation,
                                    ReflectiveInvocationContext<Method> invocationContext,
                                    ExtensionContext extensionContext) throws Throwable {

        AtomicReference<Throwable> throwable = new AtomicReference<>();

        SwingUtilities.invokeAndWait(() -> {
            try {
                invocation.proceed();
            }
            catch (Throwable t) {
                throwable.set(t);
            }
        });
        Throwable t = throwable.get();
        if (t != null) {
            throw t;
        }
    }
}

테스트 템플릿을 위한 호출 컨텍스트 제공하기

@TestTemplate 메서드는 오직 적어도 하나의 TestTemplateInvocationContextProvider가 등록되어 있을 때만 실행이 된다. 이런 제공자는 TestTemplateInvocationContext 인스턴스의 Stream을 제공하는데 책임을 갖고 있다. 각각의 컨텍스트는 커스텀된 display name을 지정하거나, @TestTemplate 메서드의 다음 호출에 사용될 추가적인 extension의 리스트를 지정해줘야 한다.

다음의 예제는 TestTemplateInvocationContextProvider를 구현하고 등록하는 방법과 테스트 템플릿을 작성하는 방법을 보여준다.

class TestTemplateDemo {


    final List<String> fruits = Arrays.asList("apple", "banana", "lemon");

    @TestTemplate
    @ExtendWith(MyTestTemplateInvocationContextProvider.class)
    void testTemplate(String fruit) {
        assertTrue(fruits.contains(fruit));
    }


    static public class MyTestTemplateInvocationContextProvider
            implements TestTemplateInvocationContextProvider {

        @Override
        public boolean supportsTestTemplate(ExtensionContext context) {
            return true;
        }

        @Override
        public Stream<TestTemplateInvocationContext> provideTestTemplateInvocationContexts(
                ExtensionContext context) {

            return Stream.of(invocationContext("apple"), invocationContext("banana"));
        }

        private TestTemplateInvocationContext invocationContext(String parameter) {
            return new TestTemplateInvocationContext() {
                @Override
                public String getDisplayName(int invocationIndex) {
                    return parameter;
                }

                @Override
                public List<Extension> getAdditionalExtensions() {
                    return Collections.singletonList(new ParameterResolver() {
                        @Override
                        public boolean supportsParameter(ParameterContext parameterContext,
                                                         ExtensionContext extensionContext) {
                            return parameterContext.getParameter().getType().equals(String.class);
                        }

                        @Override
                        public Object resolveParameter(ParameterContext parameterContext,
                                                       ExtensionContext extensionContext) {
                            return parameter;
                        }
                    });
                }
            };
        }
    }
}

위에 예제에서 테스트 템플릿은 두번 호출 된다. 호출의 디스플레이 네임은 invocation 컨텍스트에 지정된 것 처럼 applebanana가 출력이 된다. 각각의 호출은 메소드 파라미터를 찾기 위한 커스텀된 ParameterResolver를 등록한다.

└─ testTemplate(String) ✔
	├─ apple ✔
  └─ banana ✔

TestTemplateInvocationContextProvider Extension API는 주로 다른 컨텍스트에서 테스트와 유사한 메서드의 반복적인 호출에 의존하는 다양한 종류의 테스트를 구현하기위한 것이다. 예를 들어, 다른 매개 변수를 사용하여 테스트 클래스 인스턴스를 다르게 준비하거나 컨텍스트를 수정하지 않고 여러 번 준비한다. 기능을 제공하기 위해 이 extension을 사용하는 Repeated Test 또는 Parameteterized Test 구현을 참조하자.

Extension 안에서 상태 유지

보통, extension은 오직 한번만 초기화가 된다. 그래서 다음의 질문을 던질 수 있다.” 호출된 extension 상태를 다음 호출까지 어떻게 상태를 유지시킬 수 있을 까?” ExtensionContext API는 이런 목적을 위해 Store 라는 것을 제공한다. 나중을 위해 store에 저장해 뒀다가 꺼내서 쓸 수 있다. 위에서 살펴본 TimingExtension이 메소드 레벨 범위로 Store를 사용한 예제다. 테스트 실행 동안 ExtensionContext 에 저장된 값들은 ExtensionContext 에 둘러쌓여있으면 사용이 불가능 하다는걸 명심하자. ExtensionContext 는 중첩되기 때문에 안쪽의 context로 제한이 된다.

ExtensionContext.store.CloseableResource

extension context store는 extension context 라이플사이클과 바운드 된다. extension context의 라이플사이클이 끝나면, 관련된 store는 닫히게 된다. CloseableResource의 인스턴스는 저장된 모든 값은 추가 된 역순으로 close () 메서드를 호출하여 알림을 받게 된다.

Extension 안에서 지원되는 유틸리티

junit-platform-commons 는 어노테이션, 클래스, 리플렉션 , 클래스패스 스캔과 함께 작동하는 메서드들이 있다. TestEngineExtension 저자는 JUnit 플랫폼과 합을 맞추기 위해 이런 지원되는 메서드들을 사용하기를 권장하고 있다.

Annotation Support

Annotation Support 는 패키지, 어노테이션, 클래스, 인터페이스, 생성자, 메서드 , 필드 등과 같은 어노테이션이 붙은 엘리먼트와 동작하기 위한 정적 유틸리티 메서드를 제공해준다. 여기에는 엘리먼트에 어노테이션이 붙었는지 또는 메타 어노테이션이 추가되었는지 확인하고, 특정 어노테이션을 검색하고, 클래스 또는 인터페이스에서 어노테이션이 추가 된 메서드 및 필드를 찾는 메서드가 있다.

Class Support

ClassSupport는 클래스와 관련된 정적 유틸리티 메서드를 제공한다.

Reflection Support

Reflection Support 는 표준 JDK 리플렉션 및 클래스 로딩 메커니즘 관련 정적 유틸리티 메서드를 제공합니다. 여기에는 지정된 술어와 일치하는 클래스를 검색하여 클래스 경로를 스캔하고, 클래스의 새 인스턴스를 로드 및 작성하고, 메소드를 찾고 호출하는 메소드가 포함된다. 이러한 메서드 중 일부는 클래스 계층 구조를 탐색하여 일치하는 메서드를 찾는다.

Modifier Support

ModifierSupport 는 멤버와 클래스 접근자를 동작하기 위한 정적 유틸리티 메서드를 제공한다. 예를 들어 멤버가 public, private, abstract, static 등으로 선언되어있는지 확인할 수 있다.

사용자 코드 및 Extension 상대적 실행 순서

하나 이상의 테스트 메서드를 포함하는 테스트 클래스를 실행할 때 사용자 제공 테스트 및 라이플사이클 메서드 외에도 여러 Extension 콜백이 호출 된다.

유저 코드와 Extension 코드

다음의 다이어그램은 사용자 제공 코드와 extension 코드의 상대적 순서를 표현한 것이다. 유저가 제공한 테스트와 라이플사이클 메서드는 오렌지로 표시되었고, extension이 구현한 콜백 코드는 파란색으로 표시되었다. 회색 박스는 단일 테스트 메서드의 실행이고, 테스트 클래스 안에서 모든 테스트에 대해 반복적으로 실행되는걸 표시했다.

콜백 동작 랩핑하기

JUnit Jupiter는 항상 BeforeAllCallback, AfterAllCallback, BeforeEachCallback, AfterEachCallback, BeforeTestExecutionCallback, AfterTestExecutionCallback 처럼 라이플사이클 콜백을 구현한 여러개의 등록된 extension에 대하여 동작을 랩핑할 수 있게 해준다.

그 말은 즉슨 Extension1Extension2 두개의 Extension이 있는데, Extension1이 먼저 등록이 되면 항상 Extension1의 before 콜백이 먼저 실행 된다. 따라서 Extension1은 Extension2를 랩핑한다고 한다.

또한 JUnit Jupiter는 사용자가 제공한 라이플사이클메서드에 대해 같은 클래스와 인터페이스 계층 안에 있다면 랩핑을 지원해준다.

  • BeforeAll 메서드는 숨겨져있거나 오버라이딩이 되어있지 않는한 부모클래스로부터 상속을 받는다. 거기에다가, 부모클래스의 @BeforeAll 는 서브클래스의 @beforeAll보다 먼저 실행된다.
    • 이와 비슷하게,인터페이스 안에선언된 @BeforeAll 메서드는 숨겨져있거나 오버라이딩 되지않는 한 상속된다. 그리고 그 인터페이스를 구현한 구현체의 @BeforeAll 코드가 실행된다.
  • @AfterAll는 숨겨져있거나 오버라이딩 되어있지 않는 한 부모클래스로부터 상속을 받는다. 위와 마찬가지로 부모클래스의 @AfterAll이 먼저 실행 된 후 자식 클래스의 @AfterAll이 실행 된다.
    • AfterAll 또한 인터페이스 안에 선언된 @AfterAll 메서드는 숨겨져있거나 오버라이딩 되지 않는 한 상속 된다. 그리고 그 인터페이스를 구현한 구현체의 @AfterAll 코드가 실행 된다.
  • @BeforeEach 메서드는 오버라이딩 되지 않는 한 부모클래스로 부터 상속 된다. @BeforeEach 메서드도 또한 부모 클래스에 있는 게 먼저 실행 된다.
    • 인터페이스에도 위와 같음
  • @AfterEach 또한 @BeforeEach와 같음.

다음의 예제는 랩핑 동작을 증명한다.

abstract class AbstractDatabaseTests {

    @BeforeAll
    static void createDatabase() {
        beforeAllMethod(AbstractDatabaseTests.class.getSimpleName() + ".createDatabase()");
    }

    @BeforeEach
    void connectToDatabase() {
        beforeEachMethod(AbstractDatabaseTests.class.getSimpleName() + ".connectToDatabase()");
    }

    @AfterEach
    void disconnectFromDatabase() {
        afterEachMethod(AbstractDatabaseTests.class.getSimpleName() + ".disconnectFromDatabase()");
    }

    @AfterAll
    static void destroyDatabase() {
        afterAllMethod(AbstractDatabaseTests.class.getSimpleName() + ".destroyDatabase()");
    }

}
@ExtendWith({ Extension1.class, Extension2.class })
class DatabaseTestsDemo extends AbstractDatabaseTests {

    @BeforeAll
    static void beforeAll() {
        beforeAllMethod(DatabaseTestsDemo.class.getSimpleName() + ".beforeAll()");
    }

    @BeforeEach
    void insertTestDataIntoDatabase() {
        beforeEachMethod(getClass().getSimpleName() + ".insertTestDataIntoDatabase()");
    }

    @Test
    void testDatabaseFunctionality() {
        testMethod(getClass().getSimpleName() + ".testDatabaseFunctionality()");
    }

    @AfterEach
    void deleteTestDataFromDatabase() {
        afterEachMethod(getClass().getSimpleName() + ".deleteTestDataFromDatabase()");
    }

    @AfterAll
    static void afterAll() {
        beforeAllMethod(DatabaseTestsDemo.class.getSimpleName() + ".afterAll()");
    }

}
@BeforeAll AbstractDatabaseTests.createDatabase() 
@BeforeAll DatabaseTestsDemo.beforeAll() 
	Extension1.beforeEach() 
	Extension2.beforeEach() 
		@BeforeEach AbstractDatabaseTests.connectToDatabase() 
		@BeforeEach DatabaseTestsDemo.insertTestDataIntoDatabase() 
			@Test DatabaseTestsDemo.testDatabaseFunctionality() 
		@AfterEach DatabaseTestsDemo.deleteTestDataFromDatabase() 
		@AfterEach AbstractDatabaseTests.disconnectFromDatabase() 
	Extension2.afterEach() 
	Extension1.afterEach() 
@BeforeAll DatabaseTestsDemo.afterAll() 
@AfterAll AbstractDatabaseTests.destroyDatabase()

JUnit Jupiter는 하나의 테스트 클래스나 테스트 인터페이스에 선언된 여러 개의 라이플사이클 메서드의 실행 순서를 보장해 주지 않는다. 이런 메소드들은 알파벳 순서로 호출되는 것 처럼 보이지만, 사실은 아니다. 순서는 단일 테스트 클래스 내에서 @Test 메서드의 순서와 유사하다.

단일 테스트 클래스 또는 테스트 인터페이스 내에서 선언 된 라이플사이클 메서드는 결정적이지만 의도적으로 명확하지 않은 알고리즘을 사용하여 정렬된다. 이렇게하면 테스트의 후속 실행이 동일한 순서로 라이프 사이클 메소드를 실행하여 반복 가능한 빌드를 허용해준다.

추가적으로 JUnit Jupiter는 단일 테스트 클래스나 테스트 인터페이스에 선언된 여러개의 라이플사이클 메서드에 대해 랩핑을 지원하지 않는다.

다음의 예제는 특이하게, 지역적으로 선언된 라이플사이클 메서드가 실행되서 라이플사이클 메서드 설정이 부서지게 되었다.

@ExtendWith({ Extension1.class, Extension2.class })
class BrokenLifecycleMethodConfigDemo {

    @BeforeEach
    void connectToDatabase() {
        beforeEachMethod(getClass().getSimpleName() + ".connectToDatabase()");
    }

    @BeforeEach
    void insertTestDataIntoDatabase() {
        beforeEachMethod(getClass().getSimpleName() + ".insertTestDataIntoDatabase()");
    }

    @Test
    void testDatabaseFunctionality() {
        testMethod(getClass().getSimpleName() + ".testDatabaseFunctionality()");
    }

    @AfterEach
    void deleteTestDataFromDatabase() {
        afterEachMethod(getClass().getSimpleName() + ".deleteTestDataFromDatabase()");
    }

    @AfterEach
    void disconnectFromDatabase() {
        afterEachMethod(getClass().getSimpleName() + ".disconnectFromDatabase()");
    }

}

위에 코드는 다음과 같이 출력 된다.

Extension1.beforeEach() 
Extension2.beforeEach() 
	@BeforeEach BrokenLifecycleMethodConfigDemo.insertTestDataIntoDatabase() 
	@BeforeEach BrokenLifecycleMethodConfigDemo.connectToDatabase() 
		@Test BrokenLifecycleMethodConfigDemo.testDatabaseFunctionality() 
	@AfterEach BrokenLifecycleMethodConfigDemo.disconnectFromDatabase() 
	@AfterEach BrokenLifecycleMethodConfigDemo.deleteTestDataFromDatabase() Extension2.afterEach() 
Extension1.afterEach()