JUnit4는 단일 jar로 구성되어 있었지만, JUnit5는 JUnit Platfrom, JUnit Jupiter, JUnit Vintage 모듈 세가지로 구성된다. JUnit 5는 Java 8 이상의 버전을 요구한다.
import static org.junit.jupiter.api.Assertions.assertEquals;
import example.util.Calculator;
import org.junit.jupiter.api.Test;
class MyFirstJUnitJupiterTests {
private final Calculator calculator = new Calculator();
@Test
void addition() {
assertEquals(2, calculator.add(1, 1));
}
}
Junit5에서 제공하는 org.junit.jupiter.api 패키지 내의 어노테이션을 설명합니다.
@Test
@BeforeEach
@AfterEach
@BeforeAll
@AfterAll
@ExtendWith
@Disabled
@ParameterizedTest
@RepeatedTest
@TestFactory
@TestMethodOrder
@DisplayName
@DisplayNameGeneration
@DisplayName
처럼 별도의 이름을 주는 것이 아닌 코딩한 클래스, 메소드 이름을 이용해 변형시키는 어노테이션이다.@IndicativeSentencesGeneration
@IndicativeSentencesGeneration(separator = " -> ", generator = DisplayNameGenerator.ReplaceUnderscores.class)
public class IndicativeSentencesGenerationAnnotation {
@Test void test_display_name_generation() {
// "클래스명 -> test display name generation" 으로 표시됨
}
}
📝
JUnit 테스트는 기본적으로 TestInfo 타입의 인자도 받을 수 있습니다.
TestInfo 클래스는 아래와 같은 메서드를 가집니다.
getDisplayName()
: String,@DisplayName
값과 동일
getTags()
: Set\,@Tag
배열 값
getTestClass()
: Optional\<Class\<?>>, 패키지 + 테스트 클래스명
getTestMethod()
: Optional\, 패키지 + 테스트 클래스 명 + 테스트 메서드
Junit5와 Junit4 는 일부 어노테이션에서도 차이가 있다.
이전에 Junit 4 기반으로 작성된 파일들은 위의 내용과 같이 junit-vintage-engine
이 있다면 자동으로 Junit Platform 런처에서 Junit 3, 4기반 테스트 코드를 선택한다. 이미 만들어진 Junit 4 파일을 5버전으로 수정할 필요는 없다.
Assertion
의 위치가 Junit5에서는 org.junit.jupiter.api.Assertions
으로 변경되었고 AssertJ, Hamcrest, Trust에서 제공하는 org.junit.Assert는 그대로 사용할 수 있다.@Before
과 @After
어노테이션이 사라지고, 각각 @BeforeEach
와 @AfterEach
로 변경@BeforeClass
와 @AfterClass
어노테이션이 사라지고, @BeforeAll
과 @AfterAll
로 변경@Ignore
이 사라지고, @Disabled
로 변경@EnableJUnit4MigrationSupport
어노테이션을 붙여준다.@Category
가 사라지고, @Tag
로 변경@RunWith
이 사라지고 @ExtendWith
으로 변경@Rule
과 @ClassRule
이 사라지고, @ExtendWith
과 @RegisterExtention
으로 대체되었다.dependencies {
testRuntimeOnly("org.junit.platform:junit-platform-launcher:1.7.0")
testImplementation('org.junit.jupiter:junit-jupiter:5.7.0')
testRuntimeOnly('org.junit.vintage:junit-vintage-engine:5.3.1')
@TestMethodOrder(OrderAnnotation.class)
@Tag
를 사용해 태깅하기@Slf4j
public class TaggingTest {
@Test
@Tag("홀수")
void 테스트01() {
log.info("01번 테스트");
}
@Test
@Tag("짝수")
void 테스트02() {
log.info("02번 테스트");
}
@Test
@Tag("홀수")
void 테스트03() {
log.info("03번 테스트");
}
@Test
@Tag("짝수")
void 테스트04() {
log.info("04번 테스트");
}
}
위와 같이 홀수번으로 지정된 테스트만 실행된다.
테스트 클래스를 생성할 때마다 @DisplayNameGeneration을 명시해주지 않고 이를 편하게 기본값으로 설정할 수 있다.
2. junit.jupiter.displayname.generator.default에 원하는 DisplayNameGenerator 추가
junit.jupiter.displayname.generator.default = org.junit.jupiter.api.DisplayNameGenerator$ReplaceUnderscores
@DisplayNameGenerator
가 없어도 명시된 방법이 Default로 실행된다.ℹ️
@DisplayNameGeneration
의 적용순서는 다음과 같습니다.
1.@DisplayName
2.@DisplayNameGeneration
3. Properties에 명시한 Default값
4. 1,2,3 중 아무 값도 없을 경우DisplayNameGenerator.Standard.class
@RepeatedTest
, @ParameterizedTest
처럼 어노테이션명이 Test로 끝나면 별도로 @Test 어노테이션이 없어도 테스트가 가능하다.
@RepeatedTest
📝
value : int, 반복 횟수로 반드시 0보다 커야한다.
name: 반복할때 나타나는 테스트명, 지정하지 않을 시
기본값 : "repetition " + 현재 반복 횟수 + " of " + 총 반복 횟수의 형태로 나타난다.
@RepeatedTest(value = 1, name = "{displayName} {currentRepetition}/{totalRepetitions}")
와 같은 형태로 작성할 수도 있다.
RepetationInfo
타입의 인자를 받을 수 있다.
📝
RepetationInfo Class의 구성요소
getCurrentRepetition()
: int, 현재 반복횟수
getTotalRepetation()
: int, 총 반복횟수
DISPLAY_NAME_PLACEHOLDER
: String,@DisplayName
값
SHORT_DISPLAY_NAME
: String, 반복 시 나타나는 테스트명
기본 값 - "repetition" + 현재 반복 횟수 + of + "총 반복 횟수"
LONG_DISPLAY_NAME
: String,DISPLAY_NAME_PLACEHOLDER
+ " :: " +SHORT_DISPLAY_NAME
TOTAL_REPETITIONS_PLACEHOLDER
: String, 현재 반복 횟수
CURRENT_REPETITION_PLACEHOLDER
: String, 총 반복 횟수
⚠️
RepetitionInfo를 테스트 메서드 인자로 받을 시 반드시@RepeatedTest
어노테이션이 선언 되어야 합니다.
@ParameterizedTest
특정 파라미터를 넣어서 테스트를 반복적으로 실행할 수 있게 해주는 어노테이션이다.
⚠️
@ParameterizedTest는 단독으로 사용되지 않고 어떤 파라미터를 사용하는지에 관한 어노테이션을 필수적으로 선언해줘야 한다.
다양한 타입의 파라미터를 배열로 받아서 사용할 수 있게 해주는 어노테이션이다.
short[], byte[], int[], long[], float[], double[], char[], boolean[], String[], Class<?>[]
와 같이 다양한 타입을 사용할 수 있다.
해당 배열의 길이만큼 반복하며, 파라미터를 2개 이상 넣을 시 에러가 발생한다.
@ParameterizedTest
@ValueSource(ints = {1, 2, 3, 4, 5, 6, 7, 8, 9})
void 파라미터_테스트(int n) {
log.info("파라미터 테스트 : {}회 반복", n);
}
// @ParameterizedTest
// @ValueSource(ints = {1, 2, 3, 4, 5}, strings = {"하나", "둘", "셋", "야!"})
// void 파라미터_테스트(int n, String str) {
// // @ValueSource는 파라미터 2개를 받을 수 없음
// }
파라미터로 Null
값을 주거나 빈 값을 줄 때 사용한다.
@ParameterizedTest
@NullSource
void 널임(String str) {
// 원시타입은 사용할 수 없다.
assertNull(str);
}
@ParameterizedTest
@EmptySource
void 비어있음(Map map) {
// 비어있는 Map 객체 {}를 반환하므로 Null이 아니다.
// 원시타입 배열, 객체 배열, Collection 객체 등에 사용 가능하다.
assertNull(map);
}
@ParameterizedTest
@NullSource
@EmptySource
@ValueSource(strings = { "a", "b", "c", "d" })
void 널이고비어있음(String text) {
// 순서대로 NULL, "", "a", "b", "c", "d"
assertTrue(text == null || text.trim().isEmpty());
}
Enum에 정의된 상수들을 테스트하기 위한 어노테이션이다.
@ParameterizedTest
// @EnumSource(value = CountryEnum.class)
@EnumSource // value를 지정하지 않을 시 메서드 인자로 추론한다.
void 이넘_테스트(CountryEnum countryEnum) {
log.info("요거! : {}", countryEnum);
}
@ParameterizedTest
@EnumSource(value = CountryEnum.class, mode = INCLUDE, names = {"CANADA", "TURKEY", "FRANCE"})
void 이넘_모드(CountryEnum countryEnum) {
log.info("names를 포함한 ENUM : {}", countryEnum);
}
mode
에서 지원하는 비교 방식은 INCLUDE
, EXCLUDE
, MATCH_ANY
, MATCH_ALL
이 있으며 names
속성에서 문자열 혹은 정규식을 지정할 수 있다.
Factory 메서드에서 리턴해주는 값을 가지고 반복하는 어노테이션이다.
⚠️
Factory 메서드 조건
- 테스트 클래스에
@TestInstance(Lifecycle.PER_CLASS
)가 선언 되어있는 경우를 제외하고는 반드시 static 클래스여야 한다.- 인자가 없어야 한다.
- Stream\ 형태로 리턴해야한다.
@ParameterizedTest
@MethodSource(value = "methodTestProvider")
void 메서드_테스트(String str, int num, List<String> list) {
log.info("문자열 : {}, 정수 : {}, 리스트 : {}", str, num, list);
}
static Stream<Arguments> methodTestProvider() {
return Stream.of(
Arguments.arguments("가", 1, Arrays.asList("A", "B")),
Arguments.arguments("나", 2, Arrays.asList("C", "D"))
);
}
CSV (Comma Seperated Value) 형식의 데이터로 반복하는 어노테이션이다.
📝
@CsvSource의 속성 값value : String[], CSV형식의 데이터
delimiter: char, delimiter를 char형으로 변경,delimiterString
과 같이 사용 불가
delimiterString: String, delimiter를 String형으로 변경
emptyValue: String, CSV 데이터 중 빈 값인 경우 대체되는 값
nullValues: String[], CSV 데이터 중 null 값으로 대체할 값
@ParameterizedTest
@CsvSource(value = {
"가, A",
"나, B",
"다, C",
"라, ''",
"ImNull, Metoo"
},
emptyValue = "Empty",
nullValues = {"ImNull", "Metoo"})
void CSV_테스트(String hangul, String alpha) {
log.info("한글 : {}, 알파벳 : {}", hangul, alpha);
// assertNull(hangul, alpha); // nullValue로 지정된 값만 Null로 변경됨
}
ℹ️
테스트 인스턴스란?JUnit은 설정된 테스트 단위로 테스트 인스턴스를 생성한다.
JUnit 5에서 테스트 인스턴스의 기본 단위는 메서드 단위로 지정되어
각 메서드의 실행 결과가 다른 테스트에 영향을 주지 못하므로 단위 테스트에 적합하지만
테스트 클래스가 가진 각 테스트들을 하나의 단위로 묶어야 할 시 테스트 인스턴스 단위를
클래스 단위로 변경해줄 수 있다.
다음과 같은 예시의 경우
public int i=0;
@Test
void test_1() {
assertTrue(++i == 1);
}
@Test
void test_2() {
assertTrue(++i == 1);
}
결과가 모두 성공으로 나온다(두 Case 모두 결과가 1.)
기본 테스트 인스턴스의 단위가 메서드 단위이기 때문.
@TestInstance(value = PER_CLASS)
public class TestInstanceAnnotation {
public int i=0;
@Test void test_1() {
assertTrue(++i == 1);
}
@Test void test_2() {
assertTrue(++i == 1);
}
}
위와 같이 테스트 인스턴스의 단위를 클래스로 변경할 경우
test_2
에서 실패하게 된다. (전역변수 i
를 클래스간 공유하게 된다.)
모든 테스트 인스턴스의 기본값을 @TestInstance(value = PER_CLASS)
로 지정하고 싶은 경우
# resources/junit-platform.properties
junit.jupiter.testinstance.lifecycle.default = per_class
JUnit 5는 테스트 순서가 명시적으로 정해지지 않은 경우 기본 알고리즘에 의해 정해지지만
아닌 경우도 있다. (순서가 보장되지 않는다.)
[!cation]
@TestMethodOrder 주석 중
- If {@code @TestMethodOrder} is not explicitly declared on a test class,
inherited from a parent class, or declared on a test interface implemented by
a test class, test methods will be ordered using a default algorithm that is
deterministic but intentionally nonobvious.
만약 @TestInstance
를 통해 클래스 단위로 테스트를 실행하여 각 테스트 간 실행 순서가 명확하게 보장되어야 한다면 @TestMethodOrder
를 사용해야 한다.
@TestMethodOrder
어노테이션의 value 속성에 정렬 타입을 지정할 수가 있다.
MethodName
, DisplayName
, OrderAnnotation
, Random
으로 선택 가능하다.
혹은 Class<? extends MethodOrderer>
형태의 커스텀 클래스를 직접 구현하여 사용할 수도 있다.
MethodName
: 메서드 명칭 순으로 실행한다.DisplayName
: @DisplayName
의 명칭순으로 실행한다.OrderAnnotation
: @Order(n)
으로 지정한 순서대로 실행한다.Random
: 랜덤으로 실행한다.아래 모두 disabledReason
이라는 속성을 포함하며 @Disabled
어노테이션이 선언되어있는 경우에만 사용하게 된다.
OS 환경 별 테스트를 할 수 있게 해주는 어노테이션이다.
value
: OS[], LINUX
, MAC
, SOLARIS
, WINDOWS
, OTHER
JRE 환경 별 테스트를 할 수 있게 해주는 어노테이션이다.
value
: JRE[], 테스트 JRE (JAVA_8, JAVA_9, JAVA10, .... OTHER)JRE 버전 범위로 테스트를 할 수 있게 해주는 어노테이션이다.
min
: JRE, 테스트를 할 최소 버전의 JREmax
: JRE, 테스트를 할 최대 버전의 JREJVM의 System Property 별 테스트를 할 수 있게 해주는 어노테이션이다.
복수형으로 사용 가능하다.
named
: String, JVM의 System Property명matches
: String, 찾고자 하는 이름(정규식)환경 변수 별로 테스트 할 수 있게 해주는 어노테이션이다.
복수형으로 사용 가능하다.
named
: String, 환경변수명matches
: String, 찾고자 하는 이름(정규식)개발자가 원하는 조건으로 테스트를 진행할 수 있게 해주는 어노테이션이다.
value
: 조건 로직이 있는 메서드명@EnabledIf("customCondition")
@Test void testEnabledIf() {
}
@Test
@DisabledIf("customCondition")
void testDisabledIf() {
}
boolean customCondition() {
return true;
}
JUnit 4 | JUnit 5 |
---|---|
org.junit | org.junit.jupiter.api |
@Before / @After | @BeforeEach / @AfterEach |
@BeforeClass / @AfterClass | @BeforeAll / @AfterAll |
@Ignore | @Disabled |
@Category | @Tag |
@Runwith | @ExtendWith |
@Rule / @ClassRule | @ExtendWith / @RegisterExtension |
JUni4를 5로 올리면서 기존 작성된 테스트 코드에 있는 어노테이션을 굳이 바꿀 필요는 없다.
이유는 JUnit 5의 구성 요소 중 JUnit 3, 4 를 지원해주는 JUnit Vintage가 있다.
만약 기존 소스 중 아래 명시된 JUnit 4의 기능을 사용하고 있다면 junit-jupiter-migrationsupport 모듈 의존성을 추가후 @EnableRuleMigrationSupport를 클래스에 붙여주어야 한다.
아래 기능들은 JUnit Vintage에서 제공해주지 않아 별도의 모듈이 필요하다.
@Rule
의 subclass 3가지org.junit.rules.ExternalResource (including org.junit.rules.TemporaryFolder)
org.junit.rules.Verifier (including org.junit.rules.ErrorCollector)
org.junit.rules.ExpectedException
@Ignore
테스트를 더 가독성 있게 구조화하고 유지보수하기 쉽도록 구조화 하는 방법론을 의미한다.
이 패턴을 따르게 되면 테스트 케이스가 더 구체적이고 이해하기 쉬워지며 각각의 단계가 분리되어 테스트의 각 부분이 어떤 역할을 담당하는지 명확해진다.
섹션 | 설명 |
---|---|
Given | 설정, 테스트의 초기 상태 또는 사전 조건을 설정한다. 입력 데이터나 테스트가 실행될 문맥을 지정한다. |
When | 동작, 테스트되는 동작 또는 이벤트를 설명한다. 테스트되는 특정 메서드나 동작을 나타낸다. |
Then | 검증, "When" 섹션에서 설명한 동작으로 인해 기대되는 결과 또는 동작을 정의한다. |
[예시]
Given-when-then 패턴을 이용한 예시Given 섹션에서는 '초기 상태'를 설정하고 When 섹션에서는 '특정 동작을 수행'하며, Then 섹션에서는 '예상 결과를 검증'하는 어서션(assertion)을 사용하여 검증한다.
class 테스트패턴 {
@Test
void 테스트_사이즈() {
// Given
List<String> list = new ArrayList<>();
//When
list.add("가");
list.add("나");
list.add("다");
//Then
assertEquals(3, list.size());
assertTrue(list.contains("가"));
assertFalse(list.isEmpty());
}
}
메서드명_테스트대상의상태_예상동작
형식으로 JUnit 메서드 명을 지정하는 방식이다.
메서드 이름이 코드 리팩터링의 일환으로 변경이 되는 경우 테스트 이름도 변경되어야 하므로 단점이 있다.
test
접두어를 가진 JUnit 메서드 명으로 지정하는 방식이다.
"test" 접두어가 중복된다는 주장도 있지만, 일부 개발자들은 이 기법을 사용하는 것을 선호한다. 또한 SonarLint에 code smells를 피할 수 있다는 이유로 권장되기도 한다.
Should_예상동작_When_테스트대상상태
으로 JUnit 메서드 명을 지정하는 방식이다.
When_테스트대상상태_Expect_예상동작
으로 메서드 명을 지정하는 방식이다.
- Behavior-Driven Development (BDD)의 일부로 개발된 명명 규칙이다. 테스트를 사전 조건 (Given), 테스트 대상 상태 (When), 예상 동작 (Then)으로 세 가지 부분으로 나누어 작성한다.
이 접근 방식은 Behavior-Driven Development (BDD)의 일부로 개발된 명명 규칙에 기반을 두고 있다. 개별 테스트를 세 부분으로 나눠 사전 조건, 테스트 대상 상태 및 예상 동작을 위의 형식에 맞춰 작성한다.