티스토리 뷰

Extension Model

5.1. Overview

JUnit5 에서는 Extension 이라는 개념으로 JUnit4의 Runner, TestRule, MethodRule 을 모두 대체 한다.

5.2. Registering Extensions

Extension 은 선언적으로(@ExtendWith), 프로그래밍적으로(@RegisterExtension), 혹은 자동으로(ServiceLoader) 등록할 수 있다.

5.2.1. Declarative Extension Registration

개발자는 하나 이상의 Extension을 test interface, test class, test method에 선언적으로 선언 함으로서 등록 할 수있다. JUnit 5.8 부터 @ExtendsWith 어노테이션은 클래스 생성자의 field와 parameter, 테스트 메서드 안, 라이프사이클 메서드(@BeforeAll, @AfterAll, @BeforeEach, and @AfterEach)에 붙을 수 있다.

예를 들어아래와 같이 사용 할 수 있다.

@Test
@ExtendWith(WebServerExtension.class)
void getProductList(@WebServerUrl String serverUrl) {
    WebClient webClient = new WebClient();
    // Use WebClient to connect to web server using serverUrl and verify response
    assertEquals(200, webClient.get(serverUrl + "/products").getResponseStatus());
}

Extension 등록 순서
@Extension을 통해 Extension이 등록 되면, 등록 된 순서 대로 동작하게 된다.

만약 여러 Extension을 사용 했을 때, 이것을 재사용 하고싶다면 composed annotation을 생성 하여 사용하면 된다.

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

위의 예 는 클래스 수준 또는 메서드 수준에서 @ExtendWith을 적용할 수 있는 방법을 보여 준다. 그러나 특정 사용 사례의 경우 Extension을 필드 또는 매개변수 수준에서 선언적으로 등록하는 것이 좋다. Random Number를 생성 하는 RandomNumberExtension의 경우 필드에 삽입하거나 생성자, 테스트 메서드 또는 lifecycle 메서드의 매개 변수를 통해 삽입할 수 있다. RandomNumberExtension을 메타데이터로 갖는 @Random 을 사용 하면 다음 예제와 같이 확장을 투명하게 사용할 수 있습니다.

@Target({ ElementType.FIELD, ElementType.PARAMETER })
@Retention(RetentionPolicy.RUNTIME)
@ExtendWith(RandomNumberExtension.class)
public @interface Random {
}

class RandomNumberDemo {

    // use random number field in test methods and @BeforeEach
    // or @AfterEach lifecycle methods
    @Random
    private int randomNumber1;

    RandomNumberDemo(@Random int randomNumber2) {
        // use random number in constructor
    }

    @BeforeEach
    void beforeEach(@Random int randomNumber3) {
        // use random number in @BeforeEach method
    }

    @Test
    void test(@Random int randomNumber4) {
        // use random number in test method
    }

}

Extension Registration Order for @ExtendWith on Fields
Extensions registered declaratively via @ExtendWith on fields will be ordered relative to @RegisterExtension fields and other @ExtendWith fields using an algorithm that is deterministic but intentionally nonobvious. However, @ExtendWith fields can be ordered using the @Order annotation. See the Extension Registration Order tip for @RegisterExtension fields for details.

5.3. Conditional Test Execution

@Disabled라는 어노테이션을 사용 하여 몇몇 실행 조건들을 disabled 할 수 있다.

5.4. Test Instance factories

TestInstanceFactory는 테스트 인스턴스를 만들 때 사용할 수 있는 Extensions을 위한 API를 정의한다.

일반적인 use case 로는 DI 프레임워크 에서 테스트 인스턴스를 얻거나 static factory 메서드를 호출하여 테스트 클래스 인스턴스를 생성하는 것이 있다.

TestInstanceFactory 가 등록되지 않은 경우 프레임워크는 테스트 클래스의 sole 생성자를 호출하여 인스턴스화하며, 등록된 ParameterResolver extension을 통해 잠재적으로 생성자 인수를 가져온다.

TestInstanceFactory 를 구현하는 extension 은 테스트 인터페이스, 최상위 테스트 클래스 또는 @Nested 테스트 클래스에 등록할 수 있습니다.

5.5. Test Instance Post-processing

TestInstancePostProcessor defines the API for Extensions that wish to post process test instances.(?)

일반적인 usecase는 테스트 인스턴스 등에 초기화 메서드를 실행시키며 테스트 인스턴스에 의존성을 주입하는 것이다.(?)

구체적인 예로, MockitoExtensionSpringExtension을 참조해 보자.

5.6. Test Instance Pre-destroy Callback

TestInstancePreDestroyCallback 은 테스트 인스턴스가 사용 된 다음, 그리고 destroy 되기 전 프로세스를 정의하는 Extension API 이다.

5.7. Parameter Resolution

ParameterResolver는 runtime에 동적으로 파라미터를 resolving(?)하는 Extension API 이다.

만약 test class 생성자, test 메소드나 lifecycle method에서 파라미터를 선언(declare)한다면, 파라미터는 ParameterResolver를 통해 runtime에 정해진다. ParameterResolver는 built-in을 사용할 수도 있고, user가 등록한 것을 사용할 수도 있다. parameters는 일반적으로, 이름, 타입, 어노테이션, 아니면 이들 중 하나의 조합으로 정해진다.

만약 type 으로만 결정(resolve) 하는 custom ParameterResolver를 구현하고 싶다면, TypeBasedParameterResolver를 확장 하여 사용하는 것이 편하다.

구체적 예로, CustomTypeParameterResolver, CustomAnnotationParameterResolver, and MapOfListsTypeBasedParameterResolver를 찾아보자.

Due to a bug in the byte code generated by javac on JDK versions prior to JDK 9, looking up annotations on parameters directly via the core java.lang.reflect.Parameter API will always fail for inner class constructors (e.g., a constructor in a @Nested test class).
The ParameterContext API supplied to ParameterResolver implementations therefore includes the following convenience methods for correctly looking up annotations on parameters. Extension authors are strongly encouraged to use these methods instead of those provided in java.lang.reflect.Parameter in order to avoid this bug in the JDK.

boolean isAnnotated(Class<? extends Annotation> annotationType)

Optional findAnnotation(Class annotationType)

List findRepeatableAnnotations(Class annotationType)

ParameterResolver defines the Extension API for dynamically resolving parameters at runtime.

5.8. Test Result Processing

TestWatcher는 테스트 메서드 실행 결과를 처리 하는 방법을 정의한 extension API 이다. 특히, TestWatcher는 다음 이벤트들의 문맥상 실행된다.

  • testDisabled: disabled 된 테스트가 skip 된 후 실행됨
  • testSuccessful: 테스트 메서드가 성공한 후 실행 됨.
  • testAborted: 테스트 메서드가 aborted 된 후 실행됨
  • testFailed: 테스트 메서드 fail 인 경우 실행 됨

Test Classes and Methods 에서 기술한 "test method" 정의와는 다르게 여기서 말하는 test method 는 모든 @Test, @TestTemplate 메서드(@RepeatedTest or @ParameterizedTest)를 의미한다.

이 인터페이스로 구현되는 Extensions는 method level, class level에 적용될 수 있다. class level에 적용 되는 경우, Test Result Processing은 @Nested클래스를 포함해 클래스 내부에 있는 모든 test method에 대하여 실행된다.

Any instances of ExtensionContext.Store.CloseableResource stored in the Store of the provided ExtensionContext will be closed before methods in this API are invoked (see Keeping State in Extensions). You can use the parent context’s Store to work with such resources.

5.9. Test Lifecycle Callbacks

다음 interface 들은 테스트 실행 라이프사이클의 각 포인트를 확장(? extending)하기 위한 API를 정의한다. 자세한 사항은 아래 나오는 섹션과 Javadoc을 참고하라

여러 개의 Extension 구현하기

Extension을 개발 하는 개발자는 한 개의 extension에 여러 인터페이스를 선택해서 구현 할 수 있다. 구체적인 예는 SpringExtension의 코들 참고해보라.

5.9.1. Before and After Test Execution Callbacks

BeforeTestExecutionCallback and AfterTestExecutionCallback은 테스트 메서드가 실행되기 바로 전, 바로 후 실행시킬 동작을 정의하는 Extension API 이다. 이 콜백은 timing, tracing, 비슷한 usecase 들에 잘 맞는다. 만약 @BeforeEach, @AfterEach 메서드 앞뒤로 실행 되는 callback 메소드를 구현한다면, BeforeEachCallbackAfterEachCallback 를 구현하라.

아래의 예시는 실행시간을 계산하고 로깅하기 위해 콜백을 어떻게 사용하는 지를 알려주는 예시 이다. TimingExtension 은 이를 위해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) 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()));
    }

}

이렇게 구현하고

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

이렇게 로깅이 남는다.

5.10. Exception Handling

specialized Excension에는 테스트 실행 중 발생하는 Exception은 Exception throwing이 전달 되기 전 Exception Handling을 통하여 intercepted 되어 특정 액션(로깅, resource releasing)이 정의될 수도 있다. Junit Jupiter 는 TestExecutionExceptionHandler를 통하여 이와 같은 기능을 제공하며 이는 @Test 메서드, life cycle method(@BeforeAll, @BeforeEach, @AfterEAch, @AfterAll) 실행 도중에 발생한 예외에 대한 처리를 해준다.

아래의 예시를 보면 IOException은 무시 하고 다른 Exception은 다시 throw 하는 것을 볼 수 있다.

public class IgnoreIOExceptionExtension implements TestExecutionExceptionHandler {

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

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

다른 예제는 설정 및 정리 중에 예기치 않은 예외가 발생하는 지점에서 테스트 중인 응용 프로그램의 상태를 정확하게 기록하는 방법을 보여준다. 테스트 상태에 따라 실행되거나 실행되지 않을 수 있는 라이프사이클 콜백에 의존하는 것과 달리 이 솔루션은 @BeforeAll, @BeforeEach, @AfterEach 또는 @AfterAll에 실패한 직후 실행을 보장한다.

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

동일한 에외에 복수개의 handler가 정의되어 있다면 정의된 순서 대로 실행 된다. 만약 그 중 하나의 handler가 예외를 무시한다면 다음 순서의 handler는 실행 되지 않고 Junit engine으로 실패가 전달되지 않아 예외가 발생하지 않은 것 처럼 보인다. Handler는 발생한 예외를 다시 던질 수 있고, 다른 예외로 바꾸거나 wrapping 한 예외를 던질 수 있다.

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday