@ParameterizedTest
@ParameterizedTest를 사용하면 단순히 같은 내용을 반복하는 것이 아닌 여러 파라미터들을 입력해가며 실행할 수 있습니다.
@ParameterizedTest는 @RepeatedTest와 같이 name 속성을 이용해서 Test UI에 어떻게 표현할지 나타낼 수 있습니다.
참고로 @RepeatedTest에서의 currentRepetition은 @ParameterizedTest에서 index로 대체됩니다.
예시 : @ValueSource 선언
// [current] message="value"로 표기
@DisplayName("ValueSource를 이용한 파라미터 입력")
@ParameterizedTest
@ValueSource(strings = { "ValueSource를", "이용한", "파라미터", "입력" })
void test1(String message) {
System.out.println(message);
}
// displayName current message = value로 표기
@DisplayName("ValueSource를 이용한 파라미터 입력")
@ParameterizedTest(name = "{displayName} {index} message = {0}")
@ValueSource(strings = { "ValueSource를", "이용한", "파라미터", "입력" })
void test2(String message) {
System.out.println(message);
}
소스 호출
@ParameterizedTest를 이용 시 각 반복 호출에 대해 파라미터를 제공할 소스를 하나 이상 선언해야 합니다.
Jupiter은 기본적으로 몇 가지 소스 어노테이션들을 제공합니다.
1. @ValueSource
가장 간단한 소스 중 하나입니다. 리터럴 값의 단일 배열을 지정해 각 @ParameterizedTest마다 단일 인수를 제공합니다. (boolean, char, byte, short, int, long, floa, t double + String, Class)
리터럴 값을 입력할때는 각 타입에 복수를 뜻하는 s를 붙여 표현하면 됩니다.
@ValueSource(ints = {1, 2, 3})
@ValueSource(strings = {"value", "source", "test"})
2. @NullSource, @EmptySource, @NullAndEmptySources
잘못된 입력(Null, Empty)을 제공받았을 때 해당 메서드의 작동 여부를 파악하기 위해서 제공하는 소스입니다.
null인수를 제공할 수 있고, 아무것도 제공하지 않을 수 있습니다.
@ParameterizedTest
@NullSource // 합쳐 @NullAndEmptySource로 대체 가능.
@EmptySource
@ValueSource(strings = { " ", " ", "\t", "\n" })
void test3(String message) {
assertTrue(message.trim().isEmpty());
}
해당 예시는 Null 한번, Empty 한번, ValueSource의 각 요소 4번으로 총 6번의 테스트가 이루어집니다.
(이를 고치려면 text==null을 추가하면 되겠네요)
3. @EnumSource
@EnumSource를 이용하면 Enum 상수들을 편리한 방법으로 제공할 수 있습니다.
@ParameterizedTest
@EnumSource(ChronoUnit.class)
void testWithEnumSource(TemporalUnit unit) {
assertNotNull(unit);
}
다음 코드는 ChronoUnit 클래스의 모든 Enum 상수들을 제공합니다.
여기서 @EnumSource 값은 해당 Enum이 파라미터의 타입을 참조하면, 생략이 가능합니다.
이 코드의 경우 ChoronoUnit는 TemporalUnit 인터페이스를 구현하기 때문에 생략이 불가능합니다. 이 경우 파라미터 타입을 ChronoUnit로 변경하면 생략이 가능합니다.
추가적으로 일부 상수를 제외하거나, 포함할 mode 속성을 제공합니다.
@ParameterizedTest
@EnumSource(mode = EXCLUDE, names = { "DAYS", "WEEKS" })
void testWithEnumSource2(ChronoUnit unit) {
assertNotNull(unit);
}
mode | 설명 |
INCLUDE | 해당 상수들만 테스트 |
EXCLUDE | 해당 상수들을 제외하고 테스트 |
MATCH_ALL | 해당 패턴에 일치하는 모든 상수들을 테스트 (보통 정규 표현식 사용) |
MATCH_ANY | 해당 패턴에 일치하는 모든 상수들을 테스트 (보통 정규 표현식 사용) |
4. @MethodSource
테스트 클래스 또는 외부 클래스의 팩토리 메서드를 참조할 수 있는 소스입니다.
테스트 클래스의 메소드
선행 조건
- 참고로 테스트 클래스가 @TestInstance(Lifecycle.PER_CLASS)이 아닌 이상, 테스트 클래스의 팩토리 메서드는 static 해야 합니다. 외부 클래스의 팩토리 메서드의 경우 항상 static 해야 합니다. 또한 파라미터를 받아서도 안됩니다.
- 각 팩토리 메서드는 인수 스트림을 생성해야 하고, 이들은 @ParameterizedTest 메서드에 제공됩니다.
@ParameterizedTest
@MethodSource("stringProvider")
void methodSourceTest1(String argument) {
assertNotNull(argument);
}
@ParameterizedTest
@MethodSource("intProvider")
void methodSourceTest2(int argument) {
assertNotNull(argument);
}
static Stream<String> stringProvider() {
return Stream.of("팩토리", "메서드를", "통해", "스트림을", "제공");
}
// 기본형 특화 스트림 역시 가능
static IntStream intProvider() {
return IntStream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
}
메서드 탐색
기본적으로 테스트 메서드와 똑같은 이름을 가지는 팩토리 메서드를 탐색합니다.
@ParameterizedTest
@MethodSource
void methodSourceTest3(String argument) {
assertNotNull(argument);
}
static Stream<String> methodSourceTest3() {
return Stream.of("기본적으로", "같은", "이름을", "탐색");
}
다른 이름인 경우에는 선행 조건에서 설명한 코드와 같이 직접 이름을 명시하시면 됩니다.
여러 타입을 반환하는 팩토리 메서드의 경우
여러 매개 변수를 선언하는 경우 Arguments 인스턴스나, 컬렉션, 스트림, 배열을 반환해야 합니다.
다양한 타입을 Arguments으로 묶어서 반환하면 됩니다.
import static org.junit.jupiter.params.provider.Arguments.arguments;
@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"))
);
}
외부 클래스의 팩토리 메서드인 경우
외부 클래스의 팩토리 메서드의 경우에는 메소드 명뿐 아니라 전체 클래스 패스(정규화된 이름)를 입력해야 합니다.
class ExternalMethodSourceDemo {
@ParameterizedTest
@MethodSource("example.StringsProviders#tinyStrings")
void testWithExternalMethodSource(String tinyString) {
// test with tiny string
}
}
class StringsProviders {
static Stream<String> tinyStrings() {
return Stream.of(".", "oo", "OOO");
}
}
5. @CsvSource
문자열을 쉼표로 구분해서 간단하게 파라미터로 입력하는 방식입니다.
@ParameterizedTest
@CsvSource({"10", "20", "30"})
void testWithCsvSource1(int num) {
System.out.println(num);
}
문자열 내에서도 추가적인 쉼표를 통해 2차적인 구분이 가능합니다.(split(", ") 기능)
여러 파라미터들을 입력해야 하는 경우에 사용합니다.
@ParameterizedTest
@CsvSource({"10 ,20 ,30"})
void testWithCsvSource2(int num1, int num2, int num3) {
System.out.println(num1 + num2 + num3);
}
추가적으로 텍스트 블록을 지원하는 경우(JAVA SE 15 이상) 중괄호('{', '}')를 textBlock = """text"""로 대체할 수 있습니다. 파라미터들은 쉼표로, 각 테스트들을 행을 기준으로 구분되어집니다.
@ParameterizedTest
@CsvSource(textBlock = """
apple, 1
banana, 2
'lemon, lime', 0xF1
strawberry, 700_000
""")
void testWithCsvSource(String fruit, int rank) {
assertNotNull(fruit);
assertNotEquals(0, rank);
}
다른 구분 방식은 다음 표를 참고합시다.
값 타입 변환 : ArgumentConverter 사용
위 코드에서도 눈치채셨겠지만, @CsvSource는 문자열 내용을 감지해 각 타입으로 변환을 시켜줍니다. (타당한 경우)
이러한 묵시적 타입 변환이 아닌 명시적인 타입 변환을 위해 해당 클래스를 구현하는 SimpleArgumentConverter을 상속받아 사용합니다.
추가적인 타입 변경은 다음을 참고하시면 됩니다.
@DisplayName("ItemConverter 테스트")
@ParameterizedTest
@CsvSource({"itemA, 10", "itemB, 20", "itemC, 30"})
void testWithCsvSource4(@ConvertWith(ItemConverter.class) Item item) throws Exception {
System.out.println(item);
}
static class ItemConverter extends SimpleArgumentConverter {
@Override
protected Object convert(Object o, Class<?> targetType) throws ArgumentConversionException {
assertEquals(Item.class, targetType, "Can only convert to Item");
return new Item(o.toString());
}
}
단순한 문자열(CSV)들을 Item으로 변환 후 테스트가 가능해집니다.
파라미터 조합 : ArgumentsAccessor 사용
여러 파라미터들을 조합해서 새로운 인스턴스를 생성하고 싶다면 어떻게 해야 할까요?
이는 ArgumentsAccerror를 통해 구현이 가능합니다.
@DisplayName("ArgumentsAccessor 테스트")
@ParameterizedTest
@CsvSource({"itemA, 10", "itemB, 20", "itemC, 30"})
void testWithCsvSource4(ArgumentsAccessor argumentsAccessor) throws Exception {
Item item = new Item(argumentsAccessor.getString(0),
argumentsAccessor.getInteger(1));
System.out.println("item = " + item);
}
class Item {
private String name;
private int amount;
Item(String name, int amount){
this.name = name;
this.amount = amount;
}
// toString 구현
}
테스트 메서드가 너무 지저분하다면, 리팩터링을 할 수 있습니다.
이 경우 ArgumentsAggregator의 구현 클래스와, @AggregateWith 어노테이션이 필요합니다.
@DisplayName("ArgumentsAccessor 리팩토링 버전")
@ParameterizedTest
@CsvSource({"itemA, 10", "itemB, 20", "itemC, 30"})
void testWithCsvSource5(@AggregateWith(ItemAggregator.class) Item item) throws Exception {
System.out.println("item = " + item);
}
// 항상 static
static class ItemAggregator implements ArgumentsAggregator {
// 팩토리 메서드
@Override
public Object aggregateArguments(ArgumentsAccessor argumentsAccessor,
ParameterContext parameterContext) throws ArgumentsAggregationException {
return new Item(argumentsAccessor.getString(0), argumentsAccessor.getInteger(1));
}
}
이 역시 외부 클래스의 팩토리 메서드이기 때문에 항상 static 해야 합니다.
6. @CsvFileSource
쉼표로 구분된 값을 가지는 CSV 파일을 사용할 수 있습니다.
호출은 클래스 패스를 이용하시면 됩니다. 추가적으로 numLinesToSkip 속성을 통해 몇 번째 줄부터 시작할지 정할 수 있습니다.
@ParameterizedTest
@CsvFileSource(files = "src/test/resources/two-column.csv", numLinesToSkip = 1)
void testWithCsvFileSourceFromFile(String country, int reference) {
assertNotNull(country);
assertNotEquals(0, reference);
}
7. @ArgumentsSource
커스텀한 ArgumentProvider을 사용할 수 있는 어노테이션입니다.
'JAVA > JUnit' 카테고리의 다른 글
[JUnit5] 병렬 실행 (0) | 2021.10.13 |
---|---|
[JUnit5] Dynamic Test - @TestFactory (0) | 2021.10.13 |
[JUnit5] 테스트 작성(7) - 반복 테스트 (@RepeatedTest) (0) | 2021.10.12 |
[JUnit5] 내부동작(1) - 생성자와, 메서드의 매개변수 (0) | 2021.10.12 |
[JUnit5] 테스트 작성(6) - Nested Test (0) | 2021.10.12 |