작성중
📂 목차
- JUnit 6.0.1
- JUnit Platform
- JUnit Jupiter
- JUnit Vintage
- Annotations
- Test Classes and Methods
- DisplayName
- Assertions
- Assumptions
- Disabling Tests
- Conditional Test Execution
- Test Instance Lifecycle
- Nested Tests
- Interoperability
- Dependency Injection for Constructors and Methods
- Test Interfaces and Default Methods
- Sources of Arguments
- Argument Conversion
📚 본문
JUnit 6.0.1
JUnit 6.0.1 = JUnit Platform + JUnit Jupiter + JUnit 빈티지(JUnit4)
JUnit Platform
JUnit 5 는 크게 3 계층이며, 그중 맨 아래 바닥이 JUnit Platform 이다. 이는 테스트를 돌릴 수 있도록 판을 마련하며, 여러 종류의 테스트 프레임워크를 JVM 에서 실행해줄 수 있는 공통 모듈이다.
- JUnit 4
- JUnit 5
- KotlinTest
- Spock
…
JUnit Platform 은 다양한 기능들을 주고 있다.
TestEngine API
위를 전부 동작시키게 하여 호환성을 가져갔다. 이런 일을 가능하게 해주는게 TestEngine API 이며, 테스트 엔진을 만드는 규격으로 이를 지켜주면 JUnit 에서 돌아가는 테스트 프레임워크를 만들 수 있게 된다.
Console Launcher
더 나아가 기능적으로는 Console Launcher(터미널에서 JUnit Platform 실행기) 라는 CLI 도 지원해주기 때문에 커맨드라인에서 직접 테스트를 실행할 수 있다.
JUnit Platform Suite Engine
여러 테스트를 섞어서 돌릴 수도 있는데, JUnit5, JUnit4, 다른 커스텀 엔진 테스트 들 또한 돌리고 싶을때 이를 사용하여 테스트 묶음을 만들 수 있다.
IDE & Build Tool 지원
IntelliJ, Eclipse, VS Code 와 같은 IDE 에서 테스트를 위해 손 쉽게 사용할 수 있도록 JUnit Platform 을 쉽게 호출할 수 있다.
JUnit Jupiter
우리가 쓰는 JUnit 5 의 진짜 본체이며, @Test, @BeforeEach, … 등을 제공하는 프로그래밍 모듈이다.
확장 모델(Extension Model: @ExtendsWith, @BeforeEachCallback, …) 같은 기능도 있다.
SpringExtensionMockitoExtensionCustomExtension- …
따라서 Jupiter 테스트 를 실행할 수 있는 테스트 엔진을 제공, 이 엔진을 JUnit Platform 이 실행하는 것.
JUnit Vintage
옛날 테스트 실행기이며, JUnit3/4 테스트 코드가 남아있을 수 있기 때문에 이를 JUnit 5 플랫폼에서 실행할 수 있도록 해주는 TestEngine 이다. 다만,
- JUnit 4.12+ 라이브러리가 classpath 에 있어야 한다
- 지금은 Deprecated 이며, Vintage 엔진은 더이상 지원하지 않는다.
Annotations
여기서는 기본적인 애노테이션은 뺐다.
Meta-Annotations and Composed Annotations
JUnit 5 에서는 JUnit 애노테이션을 다른 애노테이션 위에 붙여서 조합 애노테이션을 만들 수 있다.
@Tag("fast")위 처럼 내가 직접 @Test 같은 기능을 가진 나만의 @XXX 어노테이션을 만들 수 있다. Tag 를 굳이 쓰지 않아도 된다는 것이다.
@Fast한 번 만들어보자.
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Tag("fast")
@Test
public @interface FastTest { }이제 이를 클래스와 메서드에 붙일 수 있고, 다음과 같이 사용할 수 있다.
@FastTest
void myFastTest() { ... }Definitions
JUnit Platform 에서 쓰이는 용어들을 보자.
Platform
- Container: 안에 무언가(테스트 클래스,
@Nested클래스)를 여러 개 담을 수 있는 노드, 여러 테스트를 포함하는 상위 노드 - Test: 실제로 실행해서 성공 혹은 실패를 판단하는 노드
Jupiter 에서 쓰이는 용어들을 보자.
Jupiter
- Lifecycle Method
@BeforeAll@AfterAll@BeforeEach@AfterEach
- Test Class: 테스트를 담을 수 있는 클래스들
- 적어도 하나의 테스트 메서드 포함
- 추상 클래스이면 안됨
- 생성자는 단 하나만 있어야 함
- record 클래스도 허용
- Test Method: 다음 아래 중 하나로 어노테이션 된 메서드
@Test@RepeatedTest@ParameterizedTest@TestFactory@TestTemplate
오직 @Test 만 단일 Test 노드를 만들며, 나머지 어노테이션은 테스트 메서드들은 테스트들을 그룹으로 묶는 Container 역할도 한다.
Test Classes and Methods
테스트/라이프사이클 메서드는 다음에서 올 수 있다.
- 현재 테스트 클래스 안에서 직접 선언
- 부모 클래스에서 상속
- 인터페이스에서 상속
단 테스트/라이프사이클 메서드는 조건이 있다:
- 추상이면 안되며
- 리턴 값이 없어야 하며(단,
@TestFactory는 예외임) - 클래스/메서드 visibility 규칙을 지켜야 한다
- private 안됨
- public 일 필요 없음
- default 로 사용하는 것을 권장
- public 을 사용이 필요하다면 그때 public 사용
- 필드 & 메서드 상속 규칙
- 필드는 무조건 상속됨
- 테스트/라이프사이클 메서드도 상속
- override 가능하면 대체되며, 불가능 시 상속 유지이다(그냥 일반 자바 생각)
즉,
- 테스트/라이프사이클 메서드는 상속 가능
@TestFactory제외하고void+non-abstract여야 함private만 아니면 됨(보통public안 붙이는 것을 추천).- 필드도 상속되고, 패키지 다르면
override못하면 그대로 상속됨. - 부모에 있는 테스트도 자식에서
override하지 않으면 그대로 실행됨.
다음 구조를 따르자.
class SampleTest {
@BeforeAll
static void beforeAll() { }
@BeforeEach
void beforeEach() { }
@Test
@DisplayName("👾")
void failingTest() { }
@Test
@DisplayName("👾👾")
void successTest() { }
@Test
@DisplayName("👾👾👾")
void abortedTest() { }
@AfterEach
void afterEach() { }
@AfterAll
static void afterAll() { }
}물론 레코드 클래스도 쓸 수 있다
@Test
void addition() {
assertEquals(2, new Calculator().add(1, 1));
}DisplayName
JUnit 5 의 테스트는 표시 이름을 자유롭게 바꿀 수 있다. 이런 이름은 다양한 IDE 창에 띄워질 수 있다. 테스트에 들어갈 수 있는 이름은 원하는 대로 설정할 수 있고 특수문자도 들어갈 수 있다.
@DisplayName("A special test cases")
class DisplayTests {
@Test
@DisplayName("Custom test")
void customTest() { }
@Test
@DisplayName("╯°□°)╯")
void testWithDisplayNameContainingSpecialCharacters() {
}
@Test
@DisplayName("😱")
void testWithDisplayNameContainingEmoji() {
}
}이를 지원한 이유는 더 읽기 좋은 테스트 리포트를 위함이며, 메서드명이 given/when/then 의 형식을 따르면 snake 형태를 써야 하는 것을 바꿀 수 있고, BDD 스타일과도 잘 어울리기 때문이다.
Given user is logged in, when viewing dashboard, then show stats
Display Name 안의 제어 문자(Control Characters) 처리 규칙인데, JUnit 5 에서는 이름 안에 제어문자(보이지 않는 특수문자)가 들어 있다면, JUnit 이 자동으로 보기 좋은 형태로 바꿔주게 된다.
- \n ->
<CR> - \r ->
<LR> - 기타 눈에 안보이는 문자 -> �(U+FFFD)
Display Name Generators
테스트 이름 자동변환기인데, 기본적으로 JUnit 은 클래스 이름 / 메서드 이름을 그대로 보여주게 된다. 하지만 DisplayNameGenerator를 사용한다면, 자동으로 더 읽기 좋은 테스트 이름으로 만들어준다. 사용은 @DisplayNameGeneration 애노테이션으로 할 수 있다.
DisplayNameGenerator 종류
- Standard: JUnit 5 기본 방식, 원래 이름을 그대로
- Simple: Standard 와 동일하지만, 파라미터 없는 메서드의
()를 제거 - ReplaceUnderscores: 메서드/클래스 이름의 언더스코어를 공백으로 바꿈
- IndicativeSentences: 클래스 + 메서드 이름을 연결해서 문장처럼 만듦
여기서
DisplayName과DisplayNameGenerator들 중 우선 순위는DisplayName이 우선시 됨
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
public class A_year_is_not_supported {
@Test
void if_it_is_zero() { }
@DisplayName("A negative value for year is not supported by the leap year computation.")
@ParameterizedTest(name = "For example, year {0} is not supported.")
@ValueSource(ints = { -1, -4})
void if_it_is_negative(int year) { }
}
@ParameterizedTest는{0}으로 파라미터를 넣을 수 있고, 안에 값이 들어가며 개별 테스트 이름이 생성된다.
@IndicativeSentencesGeneration(separator = " -> ", generator = ReplaceUnderscores.class)
class A_year_is_a_leap_year {
@Test
void if_it_is_divisible_by_4_but_not_by_100() {}
@ParameterizedTest(name = "Year {0} is a leap year.")
@ValueSource(ints={2016,2020,2048})
void if_it_is_one_of_the_following_years(int year) {}
}다음과 같이 출력된다고 한다.
A year is a leap year ✔
├─ A year is a leap year -> if it is divisible by 4 but not by 100 ✔
└─ A year is a leap year -> if it is one of the following years ✔
├─ Year 2016 is a leap year. ✔
├─ Year 2020 is a leap year. ✔
└─ Year 2048 is a leap year. ✔위에서 클래스/메서드 이름에 _ 를 붙이기 싫다면 다음을 사용한다.
@SentenceFragment("A year is a leap year")
@IndicativeSentencesGeneration
class LeapYearTests {
@SentenceFragment("if it is divisible by 4 but not by 100")
@Test
void divisibleBy4ButNotBy100() { }
@SentenceFragment("if it is one of the following years")
@ParameterizedTest(name = "{0}")
@ValueSource(ints = { 2016, 2020, 2048 })
void validLeapYear(int year) { }
}
separator의 기본값은,이다.
다음은 설정에서 DisplayNameGenerator 기본 값을 바꾸는 설정값이다.
# src/test/resources/junit-platform.properties
junit.jupiter.displayname.generator.default = org.junit.jupiter.api.DisplayNameGenerator$ReplaceUnderscores적용 순서:
@DisplayName항상 우선 >@DisplayNameGeneration>junit.jupiter.displayname.generator.default> 그 외
Assertions
Assert 구문은 생략하지만 생소한 것들만 소개한다.
assertThrowsExactly: 정확히 그throwable타입만(not subclass)assertDoesNotThrow: 예외가 발생하면 FailassertThrowassertInstanceOf<T>(): 객체가 특정 타입 클래스인지 확인assertTimeoutPreemptively: 코드 블록이 지정한 시간 안에 실행되는지 확인, 시간을 초과하면 바로 중단(preemptive)
Assumptions
가정은 테스트를 특정 조건에서만 실행하고 싶을 때 사용한다.
- 특정 OS에서만 테스트 실행
- DB 나 환경 변수가 있어야 테스트 실행
- 네트워크 연결이 필요할 때
조건이 맞지 않으면 해당 테스트를 건너뛰게 된다(Skip/Abort). 보통 가장 첫줄에 사용된다.
assumeTrueassumingThat
Disabling Tests
말 그대로 테스트를 실행하지 않도록 비활성화하는 애노테이션이며, 전체 클래스 또는 개별 메서드에 적용이 가능하다. skip 으로 쓰여지고 실패로 동작이 안된다.
@Disabled("Disabled until bug #99 has been fixed")
class DisabledClassDemo {
@Test
void testWillBeSkipped() {
}
}Conditional Test Execution
JUnit Jupiter 에서는 조건에 따라 테스트를 실행하거나 건너뛸 수 있다. 건너뛰는건 위에 있으니 넘긴다.
Condition
조건 기반 어노테이션을 쓴다면 Disabled 외에도 OS, 환경, JDK 버전 등 조건부 실행 애노테이션을 제공할 수 있다.
@EnabledOnOs(OS.MAC)
@EnabledOnJre(JRE.JAVA_17)
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Test
@DisabledOnOs(WINDOWS)
@interface TestOnWindow{ }Architecture 에 따른 조건부 실행
@Test
@EnabledOnOs(architectures = "aarch64")
void onAarch64() {
// ...
}
@Test
@DisabledOnOs(architectures = "x86_64")
void notOnX86_64() {
// ...
}
@Test
@EnabledOnOs(value = MAC, architectures = "aarch64")
void onNewMacs() {
// ...
}
@Test
@DisabledOnOs(value = MAC, architectures = "aarch64")
void notOnNewMacs() {
// ...
}Java Runtime Environment 에 따른 테스트 실행
@EnableOnJre@DisabledOnJre@EnabledForJreRange@DisabledForJreRange
@Test @EnabledOnJre(JAVA_17) void onlyOnJava17() {}
@Test @EnabledOnJre({ JAVA_17, JAVA_21 }) void onJava17And21() {}
@Test @EnabledForJreRange(min = JAVA_21, max = JAVA_25) void fromJava21To25() {}
@Test @EnabledForJreRange(min = JAVA_21) void onJava21ndHigher() {}
@Test @EnabledForJreRange(max = JAVA_18) void fromJava17To18() {}
@Test @DisabledOnJre(JAVA_19) void notOnJava19() {}
@Test @DisabledForJreRange(min = JAVA_17, max = JAVA_17) void notFromJava17To19() {}
@Test @DisabledForJreRange(min = JAVA_19) void notOnJava19AndHigher() {}
@Test @DisabledForJreRange(max = JAVA_18) void notFromJava17To18() {}versions 속성을 사용하면 숫자만 쓸 수도 있다.
@Test @EnabledOnJre(versions = 26) void onlyOnJava26() {}
@Test @EnabledOnJre(versions = { 25, 26 }) void onJava25And26() {}
@Test @EnabledForJreRange(minVersion = 26) void onJava26AndHigher() {}
@Test @EnabledForJreRange(minVersion = 25, maxVersion = 27) void fromJava25To27() {}
@Test @DisabledOnJre(versions = 26) void notOnJava26() {}
@Test @DisabledOnJre(versions = { 25, 26 }) void notOnJava25And26() {}
@Test @DisabledForJreRange(minVersion = 26) void notOnJava26AndHigher() {}
@Test @DisabledForJreRange(minVersion = 25, maxVersion = 27) void notFromJava25To27() {}이 외에도 환경변수 등을 통해서도 조건부 실행이 가능하다.
Test Instance Lifecycle
테스트 클래스는 보통 두 가지의 인스턴스 생성 방식을 가진다.
PER_METHOD(기본값): 각 테스트 메서드 실행 전마다 새로운 테스트 클래스 생성PER_CLASS:@TestInstance(Lifecycle.PER_CLASS)애노테이션 사용하며 테스트 클래스 하나당 하나의 인스턴스만 생성한다. 모든 테스트 메서드가 동일한 인스턴스에서 실행되게 된다.- 인스턴스 변수에 상태 저장 가능
@BeforeAll,@AfterAll을 non-static 하게 사용 가능
PER_CLASS 로 세팅하고 싶다면, 다음 key-value 를 파일에 입력해준다.
Djunit.jupiter.testinstance.lifecycle.default=per_classNested Tests
@Nested 테스트는 테스트 클래스 안에 또 다른 테스트 클래스를 만들어 테스트 간의 계층적 관계를 유지하면서 테스트 그룹을 묶어 논리적 구조로 표현할 수 있게 한다. 이때 inner class 는 자신만의 독자적인 @BeforeEach / @AfterEach 를 가지며, 상위 부모의 필드를 참조할 수 있는 장점이 있어 상태를 공유할 수 있게 된다.
@DisplayName("A stack")
class TestingAStackDemo {
@Test
@DisplayName("is instantiated with new Stack()")
void isInstantiatedWithNew() {
new Stack<>();
}
@Nested
@DisplayName("when new")
class WhenNew {
Stack<Object> stack;
@BeforeEach
void createNewStack() {
stack = new Stack<>();
}
@Test
@DisplayName("is empty")
void isEmpty() {
assertTrue(stack.isEmpty());
}
@Test
@DisplayName("throws EmptyStackException when popped")
void throwsExceptionWhenPopped() {
assertThrows(EmptyStackException.class, stack::pop);
}
@Test
@DisplayName("throws EmptyStackException when peeked")
void throwsExceptionWhenPeeked() {
assertThrows(EmptyStackException.class, stack::peek);
}
@Nested
@DisplayName("after pushing an element")
class AfterPushing {
String anElement = "an element";
@BeforeEach
void pushAnElement() {
stack.push(anElement);
}
@Test
@DisplayName("it is no longer empty")
void isNotEmpty() {
assertFalse(stack.isEmpty());
}
@Test
@DisplayName("returns the element when popped and is empty")
void returnElementWhenPopped() {
assertEquals(anElement, stack.pop());
assertTrue(stack.isEmpty());
}
@Test
@DisplayName("returns the element when peeked but remains not empty")
void returnElementWhenPeeked() {
assertEquals(anElement, stack.peek());
assertFalse(stack.isEmpty());
}
}
}
}Interoperability
@Nested 클래스는 @ParameterizedClass 와 결합이 가능하며, 순서는 아래와 같이 진행될 것이다.
- Outer 클래스 매개변수별 반복
- Inner 클래스 매개변수별 반복
- Inner 클래스 @ParameterizedTest 매개변수별 반복
@Execution(SAME_THREAD)
@ParameterizedClass
@ValueSource(strings = { "apple", "banana" })
class FruitTests {
@Parameter
String fruit;
@Nested
@ParameterizedClass
@ValueSource(ints = { 23, 42 })
class QuantityTests {
@Parameter
int quantity;
@ParameterizedTest
@ValueSource(strings = { "PT1H", "PT2H" })
void test(Duration duration) {
assertFruit(fruit);
assertQuantity(quantity);
assertFalse(duration.isNegative());
}
}
}FruitTests
├─ fruit = "apple"
│ └─ QuantityTests
│ ├─ quantity = 23
│ │ └─ test(Duration) → PT1H, PT2H
│ └─ quantity = 42
│ └─ test(Duration) → PT1H, PT2H
└─ fruit = "banana"
└─ QuantityTests
├─ quantity = 23
│ └─ test(Duration) → PT1H, PT2H
└─ quantity = 42
└─ test(Duration) → PT1H, PT2HDependency Injection for Constructors and Methods
JUnit Jupiter 에서는 테스트 생성자와 테스트 메서드에 매개변수를 넣을 수 있고, 이 덕분에 DI 가 가능하며, 이전 버전 JUnit4 보다 훨씬 유연하게 테스트가 작성이 가능해졌다.
ParameterResolver
런타임에 테스트 매개변수를 동적으로 제공하는 API 이며, 테스트 생성자, 메서드, 라이프사이클 메서드에서 사용이 가능하며, 다음 3가지 ParameterResolver 를 제공한다.
- TestInfoParameterResolver: 현재 테스트 정보 제공
@BeforeAll
static void beforeAll(TestInfo testInfo) {
System.out.println(testInfo.getDisplayName());
}
@Test
@DisplayName("TEST 1")
@Tag("my-tag")
void test1(TestInfo testInfo) {
System.out.println(testInfo.getDisplayName()); // TEST 1
System.out.println(testInfo.getTags()); // [my-tag]
}- ReptitionExtension:
@RepeatedTest,@BeforeEach,@AfterEach에서 현재 반복 정도를 제공한다. - TestReporterParameterResolver: 테스트 실행 중 추가 데이터 출력
@Test
void reportSingleValue(TestReporter testReporter) {
testReporter.publishEntry("status", "ok");
}
@Test
void reportKeyValuePair(TestReporter testReporter) {
testReporter.publishEntry("user", "dk38");
}@Test
void reportFiles(TestReporter testReporter, @TempDir Path tempDir) throws Exception {
Path file = tempDir.resolve("test.txt");
Files.write(file, List.of("Hello"));
testReporter.publishFile(file, MediaType.TEXT_PLAIN_UTF_8);
}Custom ParameterResolver
JUnit Jupiter 에서는 사용자 정의 ParameterResolver 를 만들어서 테스트 메서드 매개변수를 동적으로 주입할 수 있다.
public class CustomParameterResolver
implements ParameterResolver {
@Override
public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext)
throws ParameterResolutionException {
return false;
}
@Override
public @Nullable Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext)
throws ParameterResolutionException {
return null;
}
}위와 같이 만들 수 있고, 메서드 각각은 다음 역할을 지닌다.
supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext): 해당 매개변수를 내가 처리할 수 있는지 판단. 조건(true/false) 반환resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext): 실제 매개변수를 생성하여 반환
이를 통해 랜덤 값을 내뱉는 ParameterResolver 를 만들어보자.
public class CustomParameterResolver
implements ParameterResolver {
@Override
public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext)
throws ParameterResolutionException {
// 타입이 int 라면 지원
return parameterContext.getParameter().getType() == int.class;
}
@Override
public @Nullable Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext)
throws ParameterResolutionException {
return (int) (Math.random() * 100);
}
}RandomParametersExtension
이제 위를 테스트 클래스에 적용시킬 수 있겠다.
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@ExtendWith(CustomParameterResolver.class)
class MyTest {
@Test
void testWithInjectedInt(int value) {
System.out.println("Injected value: " + value);
}
}필요에 따라 애노테이션을 사용할 수 있는데, supportParameter 메서드에서 parameterContext.isAnnotated(RandomInt.class) 로 조건이 추가되었다면, 다음 RandomInt 애노테이션을 정의해야 한다.
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface RandomInt {}여기서 ExtendWith 에 들어갈 수 있는 것들이 뭐 있는지 더 보자.
MockitoExtension.class
org.mockito.junit.jupiter.MockitoExtension
Mockito 의 Mock 객체를 자동 주입하기 위해 사용되며, @Mock 으로 선언한 Mock 객체를 자동 초기화를 시켜준다.
@Mock으로 선언한 Mock 객체 자동 초기화@InjectMocks로 의존성 주입 자동 처리- 생성자, 필드, 메서드 매개변수에도 Mock 객체 주입 가능
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
class MyServiceTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private UserService userService;
@Test
void testService() {
// userRepository는 Mockito가 자동으로 주입
userService.doSomething();
}
}장점: 매번
MocktoAnnotations.openMocks(this)를 호출할 필요가 없음
SpringExtension
org.springframework.test.context.junit.jupiter.SpringExtension
스프링 IoC 도 여기에 연결 가능한데, 다만 SpringBootTest 어노테이션을 반드시 붙여줘야 한다(안붙여주면 알아서 ApplicationContext 를 로딩할 방법을 찾아서 로딩을 시켜야 한다 아래는 그 예시).
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
@ExtendWith(SpringExtension.class)
class MySpringTest {
@Test
void testContext() {
AnnotationConfigApplicationContext ctx =
new AnnotationConfigApplicationContext(MyConfig.class);
// 직접 ApplicationContext 생성 가능
}
}- @Autowired로 스프링 빈 주입 가능
- @MockBean 등 Spring Test용 Mock 객체 지원
- 트랜잭션 관리, ApplicationContext 초기화 등 Spring 환경과 통합
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;
@ExtendWith(SpringExtension.class)
@SpringBootTest
class MyServiceSpringTest {
@Autowired
private UserService userService;
@Test
void testService() {
userService.doSomething(); // Spring Bean 자동 주입
}
}Test Interfaces and Default Methods
테스트 인터페이스의 default method 에 다음 어노테이션을 사용 가능하다.
@Test@RepeatedTest@ParameterizedTest@TestFactory@BeforeEach@AfterEach
TestInstance(Lifecycle.PER_CLASS) 필요
@BeforeAll@AfterAll
예시: Dynamic Test Interface
interface TestInterfaceDynamicTestsDemo {
@TestFactory
default Stream<DynamicTest> dynamicTestsForPalindromes() {
return Stream.of("rececar", "radar", "mom", "dad")
.map(test -> dynamicTest(text, () -> assertTrue(isPalindrome(text))));
}
}Sources of Arguments
@ParameterizedTest 와 함께 쓰이는 애노테이션을 보자.
@ValueSource
가장 기본적인 파라미터 소스이며, 리터럴 값 배열 하나만 제공할 수 있다. 각 테스트 호출마다 하나의 인자만 전달 가능하다.
지원 리터럴 타입
shortbyteintlongfloatdoublecharbooleanStringClass
@NullSource & @EmptySource
null, empty, blank 값을 테스트하여 입력 검증 로직의 견고성을 확인할 수 있게 하기 위해 이 애노테이션을 쓸 수 있다.
@NullSource
@ParameterizedTest
@NullSource
void testNull(String text) { ... }당연하겠지만, primitive 에서는 이를 쓸 수 없다.
@EmptySource
지원 타입은 다음과 같다.
StringCollection,List,Set,SortedSet,NavigableSetMap,SortedMap,NavigableMap- primitive 배열(
int[],char[][]등) - object 배열(
String[],Integer[][]등)
@ParameterizedTest
@EmptySource
void testOnlyEmptyString(String text) {
assertEquals("", text);
}
@ParameterizedTest
@EmptySource
void testEmptyIntArray(int[] numbers) {
assertEquals(0, numbers.length);
}
@ParameterizedTest
@EmptySource
void testEmptyList(List<String> items) {
assertTrue(items.isEmpty());
}@NullAndEmptySource
@ParameterizedTest
@NullAndEmptySource
void testNullAndEmptyStrings(String text) {
// text == null 또는 text.isEmpty() 인 케이스가 들어옴
assertTrue(text == null || text.isEmpty());
}@EnumSource
Enum 의 모든 상수 또는 일부 상수를 파라미터로 넣어서 반복 테스트할 때 사용한다.
// 명시적 Enum 타입 지정
@ParameterizedTest
@EnumSource(ChronoUnit.class)
void testWithEnumSource(TemporalUnit unit) {
assertNotNull(unit);
}
// 암묵적 타입 자동 감지
@ParameterizedTest
@EnumSource
void testWithEnumSourceWithAutoDetection(ChronoUnit unit) {
assertNotNull(unit);
}
// 특정 상수 선택
@ParameterizedTest
@EnumSource(names = { "DAYS", "HOURS" })
void testWithEnumSourceInclude(ChronoUnit unit) {
assertTrue(EnumSet.of(ChronoUnit.DAYS, ChronoUnit.HOURS).contains(unit));
}
// from / to 속성
@ParameterizedTest
@EnumSource(from = "HOURS", to = "DAYS")
void testWithEnumSourceRange(ChronoUnit unit) {
assertTrue(EnumSet.of(
ChronoUnit.HOURS,
ChronoUnit.HALF_DAYS,
ChronoUnit.DAYS
).contains(unit));
}
// mode = EXCLUDE, names 속성으로 제외 시키기
@ParameterizedTest
@EnumSource(mode = EXCLUDE, names = { "ERAS", "FOREVER" })
void testWithEnumSourceExclude(ChronoUnit unit) {
assertFalse(EnumSet.of(ChronoUnit.ERAS, ChronoUnit.FOREVER).contains(unit));
}
// mode = MATCH_ALL 정규식 매칭
@ParameterizedTest
@EnumSource(mode = MATCH_ALL, names = "^.*DAYS$")
void testWithEnumSourceRegex(ChronoUnit unit) {
assertTrue(unit.name().endsWith("DAYS"));
}⭐️ @MethodSource
외부 혹은 현재 테스트 클래스의 factory method 에서 생성된 값을 파라미터로 사용하기 위해 존재하는 애노테이션이다.
기본 규칙
- Test 클래스 내부 factory method
- static
@TestInstance(Lifecycle.PER_CLASS)라면 non-static 허용
- 외부 클래스 factory method
- static
- Factory method 가 제공하는 데이터
- Stream
- Collection
- Iterable
- Iterator
- 배열
- primitive stream
@ParameterizedTest
@MethodSource("stringProvider")
void testWithExplicitLocalMethodSource(String argument) {
assertNotNull(argument);
}
static Stream<String> stringProvider() {
return Stream.of("apple", "banana");
}메서드 자동 탐색
이름을 생략할 수 있다.
@ParameterizedTest
@MethodSource
void testWithDefaultLocalMethodSource(String argument) {
assertNotNull(argument);
}
static Stream<String> testWithDefaultLocalMethodSource() {
return Stream.of("apple", "banana");
}다중 파라미터 사용
@ParameterizedTest
@MethodSource("stringIntAndListProvider")
void testWithMultiArgMethodSource(String str, int num, List<String> list) {
assertEquals(5, str.length());
assertTrue(num >= 1 && num <= 2);
assertEquals(2, list.size());
}
static Stream<Arguments> stringIntAndListProvider() {
return Stream.of(
arguments("apple", 1, Arrays.asList("a", "b")),
arguments("lemon", 2, Arrays.asList("x", "y"))
);
}외부 클래스의 factory method 사용
class ExternalMethodSourceDemo {
@ParameterizedTest
@MethodSource("example.StringsProviders#tinyStrings")
void testWithExternalMethodSource(String tinyString) {
// test
}
}
class StringsProviders {
static Stream<String> tinyStrings() {
return Stream.of(".", "oo", "OOO");
}
}⭐️ factory method 에 파라미터 전달하기
Test:
@RegisterExtension
static final IntegerResoler integerResolver
= new IntegerResolver();
@ParameterizedTest
@MethodSource("factoryMethodWithArguments")
void testWithFactoryMethodWithArguments(String argument) {
assertTrue(argument.startsWith("2"));
}factory method:
// Extension 이 int 파라미터 제공
static Stream<Arguments> factoryMethodWithArguments(
int quantity
) {
return Stream.of(
arguments(quantity + " apples"),
arguments(quantity + " lemons")
);
}ParameterResolver 구현:
static class IntegerResolver implements ParameterResolver {
@Override
public boolean supportsParameter(ParameterContext pc, ExtensionContext ec) {
return pc.getParameter().getType() == int.class;
}
@Override
public Object resolveParameter(ParameterContext pc, ExtensionContext ec) {
return 2;
}
}가장 많이 사용함
@FieldSource
테스트 클래스 혹은 외부 클래스의 필드를 파라미터 소스로 사용하기 위해 존재하는 애노테이션이며, 필드는 테스트 클래스 안 이거나 외부 클래스 일때 항상 static 으로 선언해주도록 하면 된다.
단
@TestInstance(Lifecycle.PER_CLASS)는 non-static
필드 타입은 Collection, Interable, Array, Supplier, Supplier 등 거의 다 가능하다.
Stream은Supplier로 감싸야 한다.
같은 이름의 필드를 자동으로 찾기
@ParameterizedTest
@FieldSource
void arrayOfFruits(String fruit) {
assertNotNull(fruit);
}
static final String[] arrayOfFruits = { "apple", "banana" };명시적으로 필드 이름 지정
@ParameterizedTest
@FieldSource("listOfFruits")
void singleFieldSource(String fruit) {
assertNotNull(fruit);
}
static final List<String> listOfFruits = Arrays.asList("apple", "banana");여러 필드 합쳐서 사용
@ParameterizedTest
@FieldSource({ "listOfFruits", "additionalFruits" })
void multipleFieldSources(String fruit) {
assertNotNull(fruit);
}
static final List<String> listOfFruits = Arrays.asList("apple", "banana");
static final Collection<String> additionalFruits = Arrays.asList("cherry", "dewberry");Stream 을 Supplier 로 제공
@ParameterizedTest
@FieldSource
void namedArgumentsSupplier(String fruit) {
assertNotNull(fruit);
}
static final Supplier<Stream<Arguments>> namedArgumentsSupplier = () -> Stream.of(
arguments(named("Apple", "apple")),
arguments(named("Banana", "banana"))
);여러 인자 받기
@ParameterizedTest
@FieldSource("stringIntAndListArguments")
void testWithMultiArgFieldSource(String str, int num, List<String> list) {
assertEquals(5, str.length());
assertTrue(num >= 1 && num <= 2);
assertEquals(2, list.size());
}
static final List<Arguments> stringIntAndListArguments = Arrays.asList(
arguments("apple", 1, Arrays.asList("a", "b")),
arguments("lemon", 2, Arrays.asList("x", "y"))
);@CsvSource
값이 2개 이상일 때 적당히 깔끔하게 테스트 가능
@ParameterizedTest
@CsvSource({
"apple, 5",
"banana, 6"
})
void test(String fruit, int length) {}Argument Conversion
Widening Primitive Conversion
JUnit 은 primitive 타입을 더 큰 primitive 타입으로 자동 변환한다.
Implicit Conversion
JUnit 5 는 문자열을 많은 타입으로 자동 변환해준다.
@ParameterizedTest
@ValueSource(strings = "SECONDS")
void testWithImplicitArgumentConversion(ChronoUnit unit) {
assertNotNull(unit.name()); // "SECONDS" → ChronoUnit.SECONDS 자동 변환
}String 을 String -> decimal, hex, octal, boolean, Enum, File, Path, Locale, Charset, URI, URL 로 변환할 수 있다.