JUnit 5 알아보기
JUnit 5 란?
https://junit.org/junit5/docs/current/user-guide/
Junit 5 는 자바의 테스팅 프레임워크 5번째 버전임
The 5th major version of the programmer-friendly testing framework for Java and the JVM
JUnit 5 의 세부모듈
- Jupiter : TestEngine API 구현체로 JUnit 5 를 제공 -> 우리가 주로 사용할 구현체
- Jupiter 의 친구들 : AssertJ, Hemcrest, Truth
- JUnit Platform : 실행가능한 런처 제공, TestEngine API 제공
- Vintage : JUnit 4, 3 를 지원하느 구현체
JUnit 5 는 Java 8 이상 버전에서 동작
시작하기
스프링부트 프로젝트 생성하는 방법 : https://start.spring.io/ 에서 Spring web 추가
기존 프로젝트에 의존성 추가하는 방법
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.5.2</version>
<scope>test</scope>
</dependency>
지난번 스프링부트로 쇼핑몰 만들기 프로젝트 With JPA 를 활용하여 테스트 작성법 확인
https://github.com/cmjeon/spring-boot-project-jpa-001
$ git checkout mock-testcase
테스트 관련 IntelliJ 단축키
- 테스트할 클래스를 열고 shift + command + t : 테스트 클래스 생성
- 메소드를 선택하고 shift + ctrl + r : 해당 메소드 테스트 케이스 실행
- 클래스를 선택하고 shift + ctrl + r : 해당 클래스 테스트 케이스 실행
Annotation
대표적인 Annotation 을 소개한다.
전체 Annotation 은 https://junit.org/junit5/docs/current/user-guide/#writing-tests-annotations 에서 확인
@Test : 이 메소드가 테스트임을 나타낸다. denote
@ParameterizedTest : 메개변수화된 테스트임을 나타낸다.
@RepeatedTest : 반복테스트임을 나타낸다.
@DisplayName : 테스트에 표시될 이름을 선언한다.
@BeforeEach : 각 @Test, @RepeatedTest, @ParameterizedTest, @TestFactory 들 전에 실행됨을 나타낸다.
@AfterEach : 각 @Test, @RepeatedTest, @ParameterizedTest, @TestFactory 들 후에 실행됨을 나타낸다.
@BeforeAll : 모든 @Test, @RepeatedTest, @ParameterizedTest, @TestFactory 들 전에 실행됨을 나타낸다.
@AfterAll : 모든 @Test, @RepeatedTest, @ParameterizedTest, @TestFactory 들 후에 실행됨을 나타낸다.
@Tag : 테스트 필터링을 위해 선언한다.
@Disabled : 테스트 메소드가 아님을 나타냄
@Timeout : 테스트에 성공에 주어진 시간을 나타냄
Meta-Annotations(Composed Annotations)
커스텀 Annotation 을 만들 수 있는 방법
@Fast 만들기
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.junit.jupiter.api.Tag;
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Tag("fast")
public @interface Fast {
}
@Fast 사용하기
@Fast
@Test
void myFastTest() {
// ...
}
Definitions 정의
Platform Concepts
컨테이너 : 다른 컨테이너나 테스트를 자식으로 가지고 있는 테스트 트리의 노드
테스트 : 실행되어 예상되는 동작을 확인하는 테스트 트리의 노드
Jupiter Concepts
라이프사이클 메서드 : @BeforeAll, @AfterAll, @BeforeEach, @AfterEach 같은 메소드
테스트 클래스 : 하나 이상의 테스트 메서드나 컨테이너를 포함하는 최상위 클래스, static or @Nested 클래스
테스트 메서드 : @Test, @RepeatedTest, @ParameterizedTest, @TestFactory, @TestTemplate 으로 지정된 메소드
Test Classes And Methods
테스트 메서드, 라이프사이클 메서드는 테스트 클래스에 선언되거나, 수퍼클래스에서 상속되거나, 인터페이스에서 구현되거나 할 수 있다.
테스트 메서드, 라이프사이클 메서드는 abstract 가 아니어야 하고 리턴값이 void 이어야 한다. 단, 값을 반환하는 @TestFactory 는 제외
테스트 클래스, 테스트 메서드, 라이프사이클 메서드는 public 일 필요는 없지만 private 는 안된다. -> 일반적으로는 public 도 생략하는 것이 좋다
Display Names
테스트 클래스, 테스트 메서드의 이름을 지정하는 Annotation
@DisplayNameGenerators 보다 우선적으로 표현된다.
Display Name Generators
@DisplayNameGeneration 를 통해 이름을 자동으로 생성할 수 있다.
DisplayNameGenerator 종류
- Standard : argument 를 포함한 메서드명 그대로 표현
- Simple : 메서드명에서 뒤에 () 제거하고 표현
- ReplaceUnderscores : _ 를 공백으로 표현
-
IndicativeSentences : 테스트 클래스명 + separator + 메소드명으로 표현
@IndicativeSentencesGeneration(separator = " -> ", generator = DisplayNameGenerator.ReplaceUnderscores.class) class this_is_my_test_class { @Test void it_is_my_test_method() { } }
Setting the Default Display Name Generator
Display Name Generator 는 properties 에 등록하여 기본 생성자로 선언할 수 있다.
# src/test/resources/junit-platform.properties
junit.jupiter.displayname.generator.default = \
org.junit.jupiter.api.DisplayNameGenerator$ReplaceUnderscores
Display Name 은 아래와 같은 순서로 표현됨
- @DisplayName
- @DisplayNameGeneration
- default DisplayNameGenerator
- org.junit.jupiter.api.DisplayNameGenerator.Standard
IntelliJ 에서 Gradle 실행 시 Display Name Generators 가 작동하지 않으면 preferences > Build, Executrion, Deployment > Build Tools > Gradle 에 들어가서 Run Tests using 을 IntelliJ IDEA 로 변경
Assertions
JUnit Jupiter 가 제공하는 기능
모든 Jupiter Assertion 은 org.junit.jupiter.api.Assertions 클래스의 static 메서드이다.
Java 8 lambdas 사용가능
Kotlin Assertion Support
JUnit Jupiter 는 Kotlin 에서 사용가능한 assertion 메소드도 제공한다.
Third-party Assertion Libraries
JUnit Jupiter 외에 다른 기능이 필요할 수도 있다(ex matcher)
이럴 때는 AssertJ, Hamcrest, Truth 를 사용할 수 있다.
matcher 와 풍부한 API 의 조합을 사용해서 assertion 을 더 설명적이고 읽기 쉽게 만들 수 있다.
JUnit Jupiter 의 Assertion 클래스는 assertThat 메서드를 제공하지 않기 때문에 Thrid-party 의 matcher 를 사용하는 것이 좋다.
Assumptions
JUnit Jupiter 의 assumptions 은 람다표현식 가능하고, static 이어야 한다.
Disabling Tests
클래스나 메서드에 @Disabled Annotation 으로 비활성화 할 수 있다.
Conditional Test Execution
JUnit Jupiter 의 ExecutionCondition 은 특정 기반으르 테스트틀 활성화/비활성화 할 수 있다. (ex @Disabled)
org.junit.jupiter.api.condition 패키지에서 선언적으로 컨테이너 및 테스트를 활성화/비활성화할 수 있는 조건을 지원한다.
여러 ExecutionCondition 이 등록되면 조건 중 하나라도 비활성화되면 테스트는 비활성화 된다.
Operating System and Architecture Conditions
@EnabledOnOs, @DisabledOnOs 으로 특정 운영체제나 아키텍쳐 에서 활성화/비활성화 할 수 있다.
Java Runtime Environment Conditions
@EnabledOnJre, @DisabledOnJre 으로 특정 JRE 에서 활성화/비활성화 할 수 있다.
@EnabledForJreRange, @DisabledForJreRange 로 특정 범위안의 JRE 에서 활성화/비활성화 할 수 있다. (기본적으로 가장 하단은 JAVA_8)
System Property Conditions
@EnabledIfSystemProperty, @DisabledIfSystemProperty 로 JVM System Property 의 값을 기반으로 활성화/비활성화 할 수 있다.
Environment Variable Conditions
@EnabledIfEnvironmentVariable, @DisabledIfEnvironmentVariable 로 환경변수의 값을 기반으로 활성화/비활성화 할 수 있다.
Custom Conditions
@EnabledIf 로 조건 메서드를 기반으로 활성화/비활성화 할 수 있다.
조건 메서드는 테스트 클래스 밖에 있을 수 있다.
Tagging and Filtering
@Tag 를 통해 테스트의 검색, 실행을 필터링하는데 사용할 수 있다.
Test Execution Order
테스트 클래스, 테스트 메서드의 순서를 보장되지는 않는다.
Method Order
실행 순서를 적용해야하는 경우에 사용에 사용한다.
@TestMethodOrder Annotation 을 지정하고 MethodOrderer 구현체를 지정한다.
MethodOrderer 인터페이스의 구현체를 만들거나 내장된 MethodOrderer 구현체를 사용할 수 있다.
- DisplayName: DisplayName 영문자 순으로
- MethodName: 메서드명 영문자순으로(6.0 에서 제거될 예정)
- OrderAnnotation: @Order 에 정의된 순서 순으로
- Random: 무작위 순으로
- Alphanumeric: 메서드명과 parameter 목록 순으로
Setting the Default Method Orderer
Method Orderer 는 properties 에 등록하여 기본 생성자로 선언할 수 있다.
# src/test/resources/junit-platform.properties
junit.jupiter.testmethod.order.default = \
org.junit.jupiter.api.MethodOrderer$OrderAnnotation
Class Order
테스트 클래스는 실행 순서에 의존하지 않지만 어떤 경우에는 실행 순서를 적용해야할 때도 있다.
테스트 클래스는 빌드 시간을 최적화하기 위해 아래 시나리오 모드를 사용
- fail fast: 이전에 실패한 테스트와 더 빠른 테스트 먼저 실행
- shortest test plan execution duration: 병렬 실행이 활성화된 상태에서 긴 테스트를 먼저 실행
ClassOrderer 인터페이스를 구현한 구현체를 사용한다.
사용자 정의 ClassOrderer 를 구현하거나 다음 내장 ClassOrderer 구현체 중 하나를 사용한다.
- ClassName: 클래스명 영문자 순으로
- DisplayName: DisplayName 영문자 순으로
- OrderAnnotation: @Order 에 정의된 순서 순으로
- Random: 무작위 순으로
Class Orderer 는 properties 에 등록하여 기본 생성자로 선언할 수 있다.
# src/test/resources/junit-platform.properties
junit.jupiter.testclass.order.default = \
org.junit.jupiter.api.ClassOrderer$OrderAnnotation
ClassOrderer 는 모든 테스트 클래스 @Nested 클래스에 적용
최상위 클래스들 끼리 순서가 정렬되고, @Nested 클래스는 상위 클래스의 다른 @Nested 클래스와 관련하여 순서가 정렬된다.
@Nested 클래스의 실행 순서를 구성하려면 상위 클래스에 @TestClassOrder 를 선언한다.
Test Instance Lifecycle
JUnit 은 각 테스트 메서드를 실행하기 위해 각 테스트 클래스의 새 인스턴스를 만들어서 실행한다.
이 메서드별 테스트 인스턴스의 생명 주기는 JUnit Jupiter 의 기본 동작이다.
JUnit Jupiter 가 동일한 테스트 인스턴스에서 모든 테스트 메서드를 실행하도록 하려면 @TestInstance(LifeCycle.PER_CLASS) annotation 을 추가핸다.
PER_CLASS 라면 테스트 메서드의 상태를 @BeforeEach, @AfterEach 에서 재설정해야 할 수 도 있다.
PER_CLASS 모드는 PER_METHOD 모드에 비해 @BeforeAll, @AfterAll 을 static 이 아닌 메서드로 선언가능한 이점이 있다.
그래서 @Nested 테스트 클래스에서 @BeforeAll, @AfterAll 메서드를 사용할 수 있다.
Changing the Default Test Instance Lifecycle
@TestInstance annotation 이 없으면 기본 생명 주기 모드인 PER_METHOD 를 사용한다.
Junit.jupiter.testinstance.lifecycle.default 의 구성 매개변수를 설정하여 기본 테스트 인스턴스 생명 주기 모드를 변경할 수 있다.
# src/test/resources
junit.jupiter.testinstance.lifecycle.default = per_class
JVM 설정에서 변경할 수도 있다.
# JVM 설정
Djunit.jupiter.testinstance.lifecycle.default=per_class
Nested Tests
@Nested 테스트는 테스트 작성자에게 여러 테스트 그룹간의 관계를 표현할 수 있는 기능을 제공한다.
외부 테스트의 전제 조건은 내부 테스트에 사용됩니다.
예컨데 @Nested 안에 @BeforeEach annotation 은 하위 테스트에 사용된다.
상위 테스트의 설정 코드가 항상 실행되기 때문에 하위 테스트만 실행할 수도 있다.
static 이 아닌 테스트 클래스만 @Nested 를 사용할 수 있다.
내부 클래스의 정적 메서드를 허용하지 않기 때문에 @Nested 클래스는 @BeforeAll, @AfterAll 이 기본적으로 작동하지 않는다.
@Nested 테스트 클래스에 @TestInstance(Lifecycle.PER_CLASS) annotation 을 추가해서 우회할 수 있다.
Dependency Injection for Constructors and Methods
JUnit Jupiter 의 주요 변경사항 중 하나는 테스트 생성자와 테스트 메서드 모두 parameter 를 가질 수 있다는 것이다.
ParameterResolver 는 런타임에 parameter 를 동적으로 처리할 수 있다.
3개의 내장 resolver 가 있다.
- TestInfoParameterResolver: 생성자나 메서드의 parameter 가 TestInfo 형이면 TestInfoParameterResolver 는 TestInfo 인스턴스를 제공한다.
- RepetitionInfoParameterResolver: @RepeatedTest, @BeforeEach, @AfterEach 메서드의 parameter 가 RepetitionInfo 형이면 RepetitionInfoParameterResolver 는 RepetitionInfo 인스턴스를 제공한다.
- TestReporterParameterResolver: 생성자나 메서드의 parameter 가 TestReporter 형이면 TestReporterParameterResolver 는 TestReporter 인스턴스를 제공한다.
Test Interfaces and Default Methods
@ExtendWith, @Tag 를 테스트 인터페이스에 선언해서 테스트 인터페이스를 구현하는 클래스가 자동으로 상속하도록 할 수 있다.
Repeated Tests
@RepeatedTest 로 원하는 횟수만큼 테스트를 반복할 수 있다.
아래의 placeholders 를 지원한다.
- {displayName}: 테스트 이름
- {currentRepetition}: 현재 반복수
- {totalRepetitions}: 총 반복횟수
기본 표시이름은 “repetition {currentRepetition} of {totalRepetitions}” 이다.
RepeatedTest.LONG_DISPLAY_NAME 은 “{displayName} :: repetition {currentRepetition} of {totalRepetitions}” 이다.
@RepeatedTest, @BeforeEach, @AfterEach 에는 RepetitionInfo 인스턴스를 주입할 수 있다.
Repeated Test Examples
RepetitionInfo 인스턴스를 사용하여 테스트 이름을 사용자 지정 이름으로 표시하는 예시를 보여준다.
https://junit.org/junit5/docs/current/user-guide/#writing-tests-repeated-tests-examples
Parameterized Tests
다른 변수로 여러번 테스트 하는 것이 가능하다.
@Test 대신 @ParameterizedTest 를 사용한다.
최소 하나의 Source annotation 을 선언해줘야 한다.
Required Setup
@ParameterizedTest 를 사용하려면 junit-jupiter-params artifact 를 dependency 에 추가해야 한다.
Consuming Arguments
@ParameterizedTest 는 일반적으로 인수 소스 인덱스, 매개변수 인덱스의 1:1 관계인 인수를 사용한다.
ParameterizedTest 에서 집계 aggregate 된 인수를 사용할 수도 있다.
ParameterResolver 에서 추가 인수를 제공할 수도 있다.
@ParameterizedTest 메서드는 다음 규칙을 따르는 매개변수가 선언되어야 한다.
- 0개 이상의 인덱싱된 인수를 먼저 선언해야 한다.
- 0개 이상의 aggregator 가 선언되어야 한다.
- ParameterResolver 에서 제공하는 0개 이상의 인수는 마지막에 선언된다.
인덱스된 인수는 ArgumentsProvider 에 의해 공급되는 인수이다.
매개변수 목록에서 동일한 인덱스의 인수들이 parameterized methods 에 전달된다.
aggregator 는 argumentsAccessor 형의 매개변수이거나 @AggregateWith annotation 이 있는 매개변수이다.
Sources of Arguments
@ValueSource
단일 배열을 지정할 수 있으며 테스트 호출당 단일인수를 제공한다.
@ParameterizedTest
@ValueSource(ints = { 1, 2, 3 })
void testWithValueSource(int argument) {
assertTrue(argument > 0 && argument < 4);
}
Null And Empty Sources
- @NullSource: 테스트 메소드에 null 인수를 제공한다.
- @EmptySource: 테스트 메소드에 빈 인수를 제공한다.
- @NullAndEmptySource: @NullSource, @EmptySource 의 합성 annotation
@EnumSource
Enum 을 상수를 소스로 제공한다.
@ParameterizedTest
@EnumSource(ChronoUnit.class)
void testWithEnumSource(TemporalUnit unit) {
assertNotNull(unit);
}
annotation 의 값은 선택사항이고, 생략하면 메서드의 첫번째 매개변수의 형식을 사용된다.
테스트 메서드의 매개변수를 ChronoUnit 으로 변경하면 @EnumSource 의 값은 생략가능하다.
https://junit.org/junit5/docs/current/user-guide/#writing-tests-parameterized-tests-sources-EnumSource{target=”_blank”}
@MethodSource
@MethodsSource 는 다른 팩토리 메서드를 참조할 수 있다.
@ParameterizedTest
@MethodSource("stringProvider")
void testWithExplicitLocalMethodSource(String argument) {
assertNotNull(argument);
}
static Stream<String> stringProvider() {
return Stream.of("apple", "banana");
}
Collection, Iterator, Iterable, 객체배열 등 Stream 으로 변환가능한 것을 리턴하면 된다.
팩토리 메서드 이름이 제공되지 않으면 @ParameterizedTest 와 동일한 이름의 메서드를 참조한다.
https://junit.org/junit5/docs/current/user-guide/#writing-tests-parameterized-tests-sources-MethodSource{target=”_blank”}
@CsvSource
@CsvSource 를 사용해서 여러 인수를 테스트에 사용할 수 있다.
@ParameterizedTest
@CsvSource({
"apple, 1",
"banana, 2",
"'lemon, lime', 0xF1",
"strawberry, 700_000"
})
void testWithCsvSource(String fruit, int rank) {
assertNotNull(fruit);
assertNotEquals(0, rank);
}
첫번째 레코드는 CSV 헤더로 사용될 수 있다.
@ParameterizedTest(name = "[{index}] {arguments}")
@CsvSource(useHeadersInDisplayName = true, textBlock = """
FRUIT, RANK
apple, 1
banana, 2
'lemon, lime', 0xF1
strawberry, 700_000
""")
void testWithCsvSource(String fruit, int rank) {
// ...
}
기본 구분자는 쉼표(,) 이지만 delimiter 속성을 이용해 다른 문자를 사용할 수 있고, delimiterString 을 사용해서 문자열을 구분자로 사용할 수 있다.
기본적으로 작은 따옴표를 사용하지만 quoteCharacter 속석으로 변경할 수 있다.
useHeadersInDisplayName 속성으로 인수를 테스트이름에 표현할 수 있다.
@CsvFileSource
@CsvFileSource 를 사용하면 클래스 경로 또는 로컬 파일 시스템에서 파일을 사용할 수 있다.
첫번째 레코드는 CSV 헤더로 사용될 수 있다.
numLinesToSkip 속석으로 헤더를 무시하도록 할 수도 있다.
@ArgumentsSource
@ArgumentsSource 는 ArgumentProvider 클래스를 통해 인수를 공급한다.
ArgumentProvider 클래스는 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);
}
}
Argument Conversion
확대 변환 Widening Conversion : 상위크기의 컨버전을 지원한다.
- byte to short, int, long, float, or double
- short to int, long, float, or double
- char to int, long, float, or double
- int to long, float, or double
- long to float or double
- float to double
암시적 변환 Implicit Conversion : JUnit jupiter 는 암시적 유형 변환기를 제공한다.
https://junit.org/junit5/docs/current/user-guide/#writing-tests-parameterized-tests-argument-conversion-implicit{target=”_blank”}
대체 문자열-객체 변환 : Fallback String-to-Object Conversion : 문자열을 객체로 바꿔준다.
factory method : 단일 String 인수를 받고 대상 형식의 인스턴스를 반환하는 메서드. 대상 형식에 선언된 비공개가 아닌 정적 메서드 메서드의 이름은 임의적일 수 있다.
factory constructor : 단일 String 인수를 허용하는 대상 형식에 선언된 비공개가 아닌 생성자
여러 개의 팩토리 메서드 있어도 무시됨.
팩토리 메서드와 팩토리 생성자가 함께 있으면 팩토리 생성자가 무시됨
@ParameterizedTest
@ValueSource(strings = "42 Cats") // 문자열이 book 객체로 변환되었음
void testWithImplicitFallbackArgumentConversion(Book book) {
assertEquals("42 Cats", book.getTitle());
}
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;
}
}
명시적 변환 Explicit Conversion : @ConvertWith 을 사용하여 매개변수로 사용할 ArgumentConverter 를 지정할 수 있다.
@ParameterizedTest
@EnumSource(ChronoUnit.class)
void testWithExplicitArgumentConversion(
@ConvertWith(ToStringArgumentConverter.class) String argument) {
assertNotNull(ChronoUnit.valueOf(argument));
}
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);
}
}
Argument Aggregation
Custom Aggregators