개발 공부 기록하기/01. JAVA & Kotlin

Junit5 Extension 알아보기 (org.junit.jupiter.api.extension)

lannstark 2021. 9. 23. 09:26

Extension

  • 모든 extension을 위한 마커 인터페이스이다.
  • Extension은 @ExtendWith 를 통해 선언적으로 등록될 수도 있고, @RegisterExtension 을 사용해 프로그램에 따라 적용될 수도 있가. ServiceLoader 매커니즘을 이용해 자동 등록될 수도 있다.
  • ServiceLoader 또는 @ExtendWith 를 이용해 등록되는 Extension 구현체는 default constructor를 무조건 가지고 있어야 한다. @ExtendWith 를 이용해 등록될 때 default constructor는 반드시 public이 아니어도 된다. ServiceLoader를 통해 등록될 때 default constructor는 반드시 public 이어야 한다. @RegisterExtension 을 이용해 등록할 때에는 생성자들이 모두 public 이거나 static factory method를 제공하거나 builder API를 제공해야 한다.

아래는 모두 LifeCycle Extension이다. 순서는 이미지를 참고하면 참 좋다.

BeforeAllCallback : Extension

  • BeforeAllCallback을 구현한 Extension은 무조건 class level에 등록되어야 한다.

BeforeEachCallback : Extension

BeforeTestExecutionCallback : Extension

AfterTestExecutionCallback : Extension

AfterEachCallback : Extension

  • LifeCycle 콜백 Extension
  • 각 테스트 후 또는 (AfterEach가 있다면) AfterEach 가 붙은 메소드가 수행된 후에 수행된다.

AfterAllCallback : Extension

  • LifeCycle 콜백 Extension
  • AfterAllCallback을 구현한 Extension은 무조건 class level에 등록되어야 한다.
  • Test Class의 AfterAll이 붙은 메소드가 수행된 후에 수행된다

실제로 다음과 같은 CustomExtension을 ClassMethod에 적용시키면 결과가 이렇게 나온다.

package com.lannstark.extension

import org.junit.jupiter.api.extension.*

class CustomAfterAllCallbackExtension() : AfterAllCallback, AfterEachCallback, AfterTestExecutionCallback,
    BeforeAllCallback, BeforeEachCallback, BeforeTestExecutionCallback {

    override fun afterAll(context: ExtensionContext?) {
        println("AfterAll in Custom Extension")
    }

    override fun afterEach(context: ExtensionContext?) {
        println("AfterEach in Custom Extension")
    }

    override fun afterTestExecution(context: ExtensionContext?) {
        println("AfterExecution in Custom Extension")
    }

    override fun beforeAll(context: ExtensionContext?) {
        println("BeforeAll in Custom Extension")
    }

    override fun beforeEach(context: ExtensionContext?) {
        println("BeforeEach in Custom Extension")
    }

    override fun beforeTestExecution(context: ExtensionContext?) {
        println("BeforeExecution in Custom Extension")
    }

}
package com.lannstark

import com.lannstark.extension.CustomAfterAllCallbackExtension
import org.junit.jupiter.api.*
import org.junit.jupiter.api.extension.ExtendWith

@ExtendWith(CustomAfterAllCallbackExtension::class)
class CallbackLifeCycleTest {

    @BeforeEach
    fun beforeEachInTestClass() {
        println("BeforeEach in Test Class")
    }

    companion object {
        @JvmStatic
        @BeforeAll
        fun beforeAllTestClass() {
            println("BeforeAll in Test Class")
        }

        @JvmStatic
        @AfterAll
        fun afterAllInTestClass() {
            println("AfterAll in Test Class")
        }
    }

    @AfterEach
    fun afterEachInTestClass() {
        println("AfterEach in Test Class")
    }

    @Test
    fun afterAllCallbackTest() {

    }

}
BeforeAll in Custom Extension
BeforeAll in Test Class

BeforeEach in Custom Extension
BeforeEach in Test Class

BeforeExecution in Custom Extension
AfterExecution in Custom Extension

AfterEach in Test Class
AfterEach in Custom Extension

AfterAll in Test Class
AfterAll in Custom Extension

ExtensionContext

한 가지 흥미로운 점이 있다. LifeCycle Extension Interface 가 가지고 있는 method parameter로 들어오는 ExtensionContext는 무엇일까?

ExtensionContext는 다음과 같은 설명을 가지고 있다.

  • ExtensionContext는 현재 실행되고 있는 테스트 또는 컨테이너의 context를 가지고 있다.
  • Extensions는 ExtensionContext의 인스턴스를 제공한다.

ExtensionContext가 가지고 있는 몇 가지 메소드를 보면 바로 이해가 된다.

// java code
public interface ExtensionContext {

  /**
   * 가능하다면 현재 테스트나 컨테이너와 관련 있는 Class를 반환한다.
   * Optional은 empty일 수 있지만 null일 수는 없다.
   */
  Optional<Class<?>> getTestClass();

  /**
   * 가능하다면 현재 테스트나 컨테이너와 관련 있는 test instance를 반환한다. (위와 동일)
   */
  Optional<Class<?>> getTestInstance();

  /**
   * 현재 테스트와 관련 있는 Method를 반환한다. (위와 동일)
   */
  Optional<Method> getTestMethod();

}

쉽다. 현재 실행중인 맥락(Context)를 표현한 객체이다.

참고로, 위에 예시로 적어둔 CustomAfterAllCallbackExtension에 들어오는 ExtensionContext의 구현체는 ClassExtensionContext 이다.

LifeCycle 외의 다른 Extension 구현체들도 알아보자.

ExecutionCondition : Extension

  • ExecutionCondition은 프로그래밍적으로 테스트 수행조건을 정의하는 Extension이다.
  • ExecutionCondition은 제공되는 ExtensionContext 하에서 주어진 container 또는 테스트가 수행되어야 하는지 아닌지를 판단하는 역할을 수행한다.
  • 만약 ExecutionCondition이 테스트 메소드를 disables로 체크하더라도, 테스트 클래스의 인스턴스화를 막을 수는 없다. 대신, 테스트 메소드의 수행과 method-level lifecycle callback (@BeforeEach, @AfterEach , 상응하는 확장 API 등)을 막는다.
@FunctionalInterface
@API(status = STABLE, since = "5.0")
public interface ExecutionCondition extends Extension {

  /**
   *  enabled로 표시된 ConditionEvaluationResult 인스턴스를 반환하면 수행되고,
   *  disabled로 표시된 ConditionEvaluationResult 인스턴스를 반환하면 수행되지 않는다.
   */
  ConditionEvaluationResult evaluateExecutionCondition(ExtensionContext context);

}

ParameterResolver : Extension

  • 런타임시 동적으로 argument를 resolve하기 위한 API를 정의한다.
  • 테스트 클래스의 생성자 또는 @Test @BeforeEach @AfterEach @BeforeAll @AfterAll 메소드가 파라미터를 선언하고 있으면, 파라미터를 위한 argument를 런타임시 resolve 해주어야 한다.
@API(status = STABLE, since = "5.0")
public interface ParameterResolver extends Extension {

  /**
   * resolver가 argument의 resolution을 지원하는지 결정한다.
   */
  boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext)
              throws ParameterResolutionException;

  /**
   * 제공되는 파라미터에 대해서 적절한 argument로 resolve 한다.
   * 이 메소드는 supportsParameter 메소드가 true를 반환했을 때만 호출된다.
   */
  Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext)
            throws ParameterResolutionException;

}

ParameterContext : Extension

  • 주어진 파라미터에 대해 Executable이 호출될 때의 context를 가지고 있다.

TestInstanceFactory : Extension

  • 테스트 인스턴스를 생성을 희망하는 Extension API를 정의한다.
  • 특별한 생성 요구사항과 함께 테스트 인스턴스를 생성할 때나 DI framework를 이용해 테스트 인스턴스를 획득해야 할 때 주로 사용된다.
  • TestInstanceFactory를 구현한 Extension은 클래스 level에 등록되어야 한다.
  • 특정 테스트 클래스에는 오직 한 개의 TestInstanceFactory만 허용된다. (그렇지 않을 경우 exception 발생) TestInstanceFactory는 상속이 가능하기 때문에 하나의 TestInstanceFactory만 Test Class에 붙이는 것은 사용자의 책임이다.
@FunctionalInterface
@API(status = STABLE, since = "5.7")
public interface TestInstanceFactory extends Extension {
    Object createTestInstance(TestInstanceFactoryContext factoryContext, ExtensionContext extensionContext)
              throws TestInstantiationException;
}

TestInstancePostProcessor : Extension

  • 테스트 클래스의 인스턴스가 생성된 직후의 동작을 정의할때 사용된다.
  • 테스트 인스턴스에 의존성을 주입하거나 테스트 인스턴스의 custom 초기화 메소드를 호출할 때 주로 사용된다. 클래스 level에 등록되어야 한다.
@FunctionalInterface
@API(status = STABLE, since = "5.0")
public interface TestInstancePostProcessor extends Extension {
    void postProcessTestInstance(Object testInstance, ExtensionContext context) throws Exception;
}

TestInstancePreDestoryCallback : Extension

  • TestInstance가 만들어져 테스트가 모두 수행되고 destory 되기 전의 동작을 정의할 때 사용된다.
  • 테스트 인스턴스에 의해 수행된 resource를 해제하거나 clean up 메소드를 사용하기 위해 주료 사용된다.
  • 테스트 클래스가 @TestInstance(LifeCycle.PRE_CLASS) 로 설정되었다면 class level에 등록되어야 하며, @TestInstance(LifeCycle.PRE_METHOD) 로 설정되었다면 class level 또는 method level에 등록되어야 한다.
  • TestInstancePostProcessor와 다르게 ExtensionContext 마다 한 번만 호출된다.

이 외에도 TestTemplateInvocationContextProvider : Extension, TestExecutionExceptionHandler : Extension, TestWatcher : Extension 등이 있다.