JUnit 5 (5)

들어가기 전에

공식 유저 가이드 의 3장을 정리하였다.

생명주기

개별 태스트의 독립성을 보장하고, 테스트 사이의 상호관계에서 발생하는 부작용을 방지하기 위해, JUnit는 개별 테스트 메서드 의 실행 전, 새로운 인스턴스를 생성한다. 이를 통해 개별 테스트 메서드는 완전히 독립적인 객체 환경에서 동작하며, 이를 메서드 단위 생명주기라 한다.

만약 모든 테스트 메서드를 동일한 인스턴스 환경에서 동작시키고 싶다면, 단순히 @TestInstance 어노테이션을 사용하면 된다.

클래스 단위 생명주기를 가지는 클래스는, 테스트 실행 중 단 하나의 인스턴스를 생성하며, 만약 해당 클래스가 인스턴스 속성을 가진다면 @BeforeEach@AfterEach 에서드를 사용하여 이를 초기화 해야 할 필요가 발생할 수 있다.

약간 잉여해보이는 생명주기지만, 메서드 단위 생명주기와 비교하였을 때 클래스 단위 생명주기가 가지는 장점도 있다.

기본 생명주기 변경하기

중첩된 테스트

@Nested 어노테이션을 사용한 중첩된 테스트는 테스트 그룹 간의 관계를 다양한 방법으로, 더욱 명확하게 표시할 수 있다.

내포된 테스트 클래스를 사용할 때에는 다음과 같은 점을 염두에 두어야 한다.

DI

주피터JUnit Jupiter 로 넘어오며 생긴 큰 변경점이라고 하면, 생성자와 메서드가 인자를 받을 수 있게 되었다는 것이다. 이러한 변화는 생성자와 메서드에 유연함을 불어넣어줄 뿐만 아니라, 의존성 주입 또한 가능하게 하였다.

이를 위해 인자 해석기ParameterResolver 인터페이스 는 실행 시간 동안에 동적으로 인자를 해석할 수 있는 API를 정의하고 있다. 만약 테스트 클래스의 생성자나 테스트 메서드, 보조 메서드(@BeforeEach 와 같은) 가 인자를 가진다면, 인자는 등록된 ParameterResolver 에 의해 해석된다.

현재 JUnit 5에는 세 가지 기본 ParameterResolver 가 있으며, 이들은 별 다른 설정 없이도 자동으로 등록된다.

해석기 설명
TestInfoParameterResolver TestInfo 객체에 대한 기본 해석기이다. TestInfo 객체는 테스트 클래스, 매서드 이름이나 디스플레이 네임과 같은, 현재 테스트에 대한 정보를 담고 있다.
RepetitionInfoParameterResolver 반복 실행 가능한 메서드, 이를테면 @RepeatedTest , @BeforeEach , @AfterEach 와 같은 메서드의 정보를 가지는 RepetitionInfo 객체에 대한 기본 해석기이다.
TestReporterParameterResolver 현재 실행하는 테스트에 대한 추가 정보를 디스플레이 등에 표시할 수 있는 TestReporter 객체에 대한 기본 해석기이다.

개인화된 ParameterResolver 를 제작하는 예시는 MockitoExtension 의 소스코드에서 확인 가능하다. 이렇게 생성한 확장 기능은 @ExtendWith 어노테이션을 이용하여 테스트 클래스에 등록 가능하다.

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.when;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import com.example.Person;
import com.example.mockito.MockitoExtension;

@ExtendWith(MockitoExtension.class)
class MyMockitoTest {

    @BeforeEach
    void init(@Mock Person person) {
        when(person.getName()).thenReturn("Dilbert");
    }

    @Test
    void simpleTestWithInjectedMock(@Mock Person person) {
        assertEquals("Dilbert", person.getName());
    }

}

테스트 ‘인터페이스’ 와 기본 메서드

주피터는 @Test , @RepeatedTest , @ParameterizedTest , @TestFactory , @TestTemplate, @BeforeEach, @AfterEach 와 같은 어노테이션을 인터페이스의 기본 메서드에 적용하는 것을 허용한다.

@TestInstance(Lifecycle.PER_CLASS)
interface TestLifecycleLogger {

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

    @BeforeAll
    default void beforeAllTests() {
        LOG.info("Before all tests");
    }

    @AfterAll
    default void afterAllTests() {
        LOG.info("After all tests");
    }
}

이러한 테스트 인터페이스에는 @ExtendWith@Tag 어노테이션 역시 지정 가능하다. 해당 테스트 인터페이스를 구현한 테스트 클래스는 테스트 인터페이스의 어노테이션 설정을 적용받을 것이다.

@Tag("timed")
@ExtendWith(TimingExtension.class)
interface TimeExecutionLogger {
}
class TestInterfaceDemo implements TestLifecycleLogger,
        TimeExecutionLogger, TestInterfaceDynamicTestsDemo {

    @Test
    void isEqualValue() {
        assertEquals(1, 1, "is always equal");
    }

}

이외에, 인터페이스를 상속하여 해당 인터페이스에 대한 테스트 인터페이스 를 작성할 수 있다.

public interface Testable<T> {
    T createValue();
}
public interface EqualsContract<T> extends Testable<T> {

    T createNotEqualValue();

    @Test
    default void valueEqualsItself() {
        T value = createValue();
        assertEquals(value, value);
    }

    @Test
    default void valueDoesNotEqualNull() {
        T value = createValue();
        assertFalse(value.equals(null));
    }

    @Test
    default void valueDoesNotEqualDifferentValue() {
        T value = createValue();
        T differentValue = createNotEqualValue();
        assertNotEquals(value, differentValue);
        assertNotEquals(differentValue, value);
    }

}

테스트 템플릿과 공급자

@TestTemplate 메서드는 그 자체로 동작 가능한 테스트 케이스는 아니다. 대신, @TestTemplate 은 공급자Provider가 제공하는 실행 컨텍스트에 의해 실행되는, 일종의 템플릿으로서 설계되어 있다. 템플릿을 사용하기 위해서는, 해당 템플릿에 공급자TestTemplateInvocationContextProvider 인터페이스의 구현체를 등록하여야 한다. 자세한 설명은 5장 에서 확인 가능하다.

동적 테스트

@Test 메서드는 이전 버전의 @Test 와 유사한 동작을 하며, 다음과 같은 특징을 가진다

이러한 문제점을 해결하기 위해, 팩토리@TestFactory를 사용하여 실행 시간 동안에 동적으로 테스트를 생성해주는 테스트 모델이 주피터에 추가되었다.

@TestFactory 는 테스트로 동작하지만, 테스트 케이스보다는 그것을 생산하는 팩토리에 가깝다. 모든 @TestFactoryDynamicNode 객체를 가지는 Stream , Collection , Iterable , Iterator 을 반환해야 하며, 실제로 실행되는 테스트는 팩토리가 반환하는 DynamicNode 내에 있다.

동적 테스트의 생명 주기

동적 테스트의 생명 주기는 @Test 와 다르다. 간단하게 말하면, 동적으로 생성되는 테스트에는 개별적인 생명 주기가 존재하지 않는다. @BeforeEach 와 ` @AfterEach@TestFactory` 메서드에는 동작할 것이나, 팩토리가 반환하는 테스트 케이스에는 그렇지 않을 것이다.