들어가기 전에
- < 이전 포스트 : JUnit 5 (4)
- 모든 소스코드는 JUnit 5 공식 유저 가이드의 것을 사용하였다.
- 3장은 내용이 길어서, 몇 개의 포스트로 나눌 예정이고, 작성 과정에서 내용의 순서가 변경되거나 빠질 수 있다.
- 결과 : 3개의 포스트로 나눠졌고, 순서가 변경되거나 일부 내용이 누락되었다.
- 번역이라기 보다는 요약정리
공식 유저 가이드 의 3장을 정리하였다.
생명주기
개별 태스트의 독립성을 보장하고, 테스트 사이의 상호관계에서 발생하는 부작용을 방지하기 위해, JUnit는 개별 테스트 메서드 의 실행 전, 새로운 인스턴스를 생성한다. 이를 통해 개별 테스트 메서드는 완전히 독립적인 객체 환경에서 동작하며, 이를 메서드 단위 생명주기라 한다.
- 메서드 단위 생명주기는 이전 버전의 JUnit의 동작 방식과 유사하다.
만약 모든 테스트 메서드를 동일한 인스턴스 환경에서 동작시키고 싶다면, 단순히 @TestInstance
어노테이션을 사용하면 된다.
@TestInstance(Lifecycle.PER_CLASS)
를 선언한 클래스는 클래스 단위 생명주기를 가진다.
클래스 단위 생명주기를 가지는 클래스는, 테스트 실행 중 단 하나의 인스턴스를 생성하며, 만약 해당 클래스가 인스턴스 속성을 가진다면 @BeforeEach
나 @AfterEach
에서드를 사용하여 이를 초기화 해야 할 필요가 발생할 수 있다.
약간 잉여해보이는 생명주기지만, 메서드 단위 생명주기와 비교하였을 때 클래스 단위 생명주기가 가지는 장점도 있다.
@BeforeAll
이나@AfterAll
메서드가 정적 메서드 일 필요가 없다.@Nested
클래스에서@BeforeAll
이나@AfterAll
메서드를 사용할 수 있게 된다.
기본 생명주기 변경하기
- JVM :
-Djunit.jupiter.testinstance.lifecycle.default=per_class
- junit-platform.properties :
junit.jupiter.testinstance.lifecycle.default = per_class
중첩된 테스트
@Nested
어노테이션을 사용한 중첩된 테스트는 테스트 그룹 간의 관계를 다양한 방법으로, 더욱 명확하게 표시할 수 있다.
- 정확히는 중첩된 테스트 클래스
내포된 테스트 클래스를 사용할 때에는 다음과 같은 점을 염두에 두어야 한다.
@Nested
클래스는 정적이 아닌 내포된 클래스여야 한다.- 기본 메서드 단위 생명주기에서는
@BeforeAll
이나@AfterAll
메서드를 사용할 수 없다.- Java는 내포된 클래스 내에서 정적 메서드를 허용하지 않기 때문
- 클래스 단위 생명주기를 사용 시 이는 해결할 수 있다.
DI
주피터JUnit Jupiter
로 넘어오며 생긴 큰 변경점이라고 하면, 생성자와 메서드가 인자를 받을 수 있게 되었다는 것이다. 이러한 변화는 생성자와 메서드에 유연함을 불어넣어줄 뿐만 아니라, 의존성 주입 또한 가능하게 하였다.
- 이전 버전까지의 JUnit은, 생성자나 메서드가 인자를 받을 수 있는 방법이 -적어도 기본 러너
Runner
에선- 없었다.
이를 위해 인자 해석기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
와 같은 어노테이션을 인터페이스의 기본 메서드에 적용하는 것을 허용한다.
- 만약 클래스 단위 생명주기가 적용된 경우,
@BeforeAll
와@AfterAll
메서드는 정적 메서드나 기본 메서드 어느 것으로든 선언 가능하다.
@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
와 유사한 동작을 하며, 다음과 같은 특징을 가진다
- 정적이다.
- 정적 테스트 케이스로서, 컴파일 시간에 모든 내용이 결정된다.
- 실행 시간 동안에는 테스트 케이스의 동작을 변경할 수 없다.
- 추정
Assumption
메서드를 활용하여 동적인 동작을 추가할 수 있으나, 역시나 제한된 점이 있다.
이러한 문제점을 해결하기 위해, 팩토리@TestFactory
를 사용하여 실행 시간 동안에 동적으로 테스트를 생성해주는 테스트 모델이 주피터에 추가되었다.
@TestFactory
는 테스트로 동작하지만, 테스트 케이스보다는 그것을 생산하는 팩토리에 가깝다. 모든 @TestFactory
는 DynamicNode
객체를 가지는 Stream
, Collection
, Iterable
, Iterator
을 반환해야 하며, 실제로 실행되는 테스트는 팩토리가 반환하는 DynamicNode
내에 있다.
- 사용 가능한
DynamicNode
에는DynamicContainer
와DynamicTest
이 있다.DynamicContainer
는 디스플레이 네임 과 하위DynamicNode
의 집합이다.DynamicTest
는 실제로 실행 가능한 테스트이다.
동적 테스트의 생명 주기
동적 테스트의 생명 주기는 @Test
와 다르다. 간단하게 말하면, 동적으로 생성되는 테스트에는 개별적인 생명 주기가 존재하지 않는다. @BeforeEach
와 ` @AfterEach 는
@TestFactory` 메서드에는 동작할 것이나, 팩토리가 반환하는 테스트 케이스에는 그렇지 않을 것이다.