JUnit5는 개발자 테스트를 작성하는 데 도움을 주는 여러 새로운 기능을 제공한다. 그 중 하나가 parameterized test이다. parameterized test를 사용하면 하나의 테스트 메서드를 다양한 매개변수로 여러 번 실행할 수 있다.
의존성
parameterized test를 사용하려면 junit-jupiter-params artifact를 프로젝트에 추가해야 한다.
Maven을 사용하는 경우, `pom.xml`에 다음을 추가한다.
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>5.11.0</version>
<scope>test</scope>
</dependency>
Gradle을 사용하는 경우에는 다음과 같이 설정한다.
testCompile("org.junit.jupiter:junit-jupiter-params:5.11.0")
Argument Sources
parameterized test는 다양한 인수를 사용해 동일한 테스트를 여러 번 실행한다.
Simple Values
`@ValueSource` 어노테이션을 사용하여 리터럴 값의 배열을 테스트 메서드에 전달할 수 있다.
예를 들어, `isBlank` 메서드를 테스트한다고 가정해보자.
public class Strings {
public static boolean isBlank(String input) {
return input == null || input.trim().isEmpty();
}
}
`null` 또는 공백 문자열에 대해 true를 반환하는지 확인하려면 다음과 같은 parameterized test를 작성할 수 있다.
@ParameterizedTest
@ValueSource(strings = {"", " "})
void isBlank_ShouldReturnTrueForNullOrBlankStrings(String input) {
assertTrue(Strings.isBlank(input));
}
이 테스트는 두 번 실행되며, 매번 다른 문자열이 `input` 매개변수로 전달된다.
`@ValueSource`가 지원하는 데이터 유형에는 제한이 있다. 지원하는 유형은 다음과 같다.
- short (shorts 속성)
- byte (bytes 속성)
- int (ints 속성)
- long (longs 속성)
- float (floats 속성)
- double (doubles 속성)
- char (chars 속성)
- java.lang.String (strings 속성)
- java.lang.Class (classes 속성)
또한, 테스트 메서드에 매번 하나의 인수만 전달할 수 있다.
중요한 점은 null을 인수로 전달하지 않았다는 것이다. 이는 또 다른 제한 사항으로, String 및 Class의 경우에도 @ValueSource를 통해 null을 전달할 수 없다.
Null and Empty Values
JUnit 5.4부터는 `@NullSource`를 사용하여 parameterized test 메서드에 단일 `null` 값을 전달할 수 있다.
@ParameterizedTest
@NullSource
void isBlank_ShouldReturnTrueForNullInputs(String input) {
assertTrue(Strings.isBlank(input));
}
기본 데이터 타입(primitive data types)은 `null` 값을 허용하지 않기 때문에, @NullSource는 기본형 매개변수에는 사용할 수 없다(ex. `int`, `boolean`).
유사하게, `@EmptySource` 애너테이션을 사용하여 빈 값을 전달할 수 있다.
@ParameterizedTest
@EmptySource
void isBlank_ShouldReturnTrueForEmptyStrings(String input) {
assertTrue(Strings.isBlank(input));
}
@EmptySource는 단일 빈 인수를 애너테이션된 메서드에 전달한다.
String 매개변수의 경우, 전달된 값은 단순히 빈 문자열이 된다. 또한, 이 매개변수 소스는 Collection 타입과 배열에 대해 빈 값을 제공할 수 있다.
null과 빈 값을 모두 전달하려면, `@NullAndEmptySource` 애너테이션을 사용할 수 있다.
@ParameterizedTest
@NullAndEmptySource
void isBlank_ShouldReturnTrueForNullAndEmptyStrings(String input) {
assertTrue(Strings.isBlank(input));
}
`@EmptySource`와 마찬가지로, 이 복합 애너테이션은 문자열, Collection, 배열에 대해 동작한다.
몇 가지 더 다양한 빈 문자열 변형을 매개변수화된 테스트에 전달하려면, @ValueSource, @NullSource, @EmptySource를 결합할 수 있다.
@ParameterizedTest
@NullAndEmptySource
@ValueSource(strings = {" ", "\t", "\n"})
void isBlank_ShouldReturnTrueForAllTypesOfBlankStrings(String input) {
assertTrue(Strings.isBlank(input));
}
Enum
테스트를 열거형(enum)의 다양한 값으로 실행하려면 `@EnumSource` 애너테이션을 사용할 수 있다.
예를 들어, 모든 월(month)의 숫자가 1에서 12 사이에 있는지 확인할 수 있다.
@ParameterizedTest
@EnumSource(Month.class) // 12개의 월 전달
void getValueForAMonth_IsAlwaysBetweenOneAndTwelve(Month month) {
int monthNumber = month.getValue();
assertTrue(monthNumber >= 1 && monthNumber <= 12);
}
또한, `names` 속성을 사용해 특정 몇 개월만 필터링할 수 있다.
다음과 같이, 4월, 9월, 6월, 11월이 30일로 구성되어 있다는 것을 확인할 수 있다.
@ParameterizedTest
@EnumSource(value = Month.class, names = {"APRIL", "JUNE", "SEPTEMBER", "NOVEMBER"})
void someMonths_Are30DaysLong(Month month) {
final boolean isALeapYear = false;
assertEquals(30, month.length(isALeapYear));
}
기본적으로, `names`는 전달된 열거형 값과 일치하는 값들만 유지한다.
이 동작을 반대로 설정하려면 `mode` 속성을 EXCLUDE로 설정할 수 있다.
@ParameterizedTest
@EnumSource(
value = Month.class,
names = {"APRIL", "JUNE", "SEPTEMBER", "NOVEMBER", "FEBRUARY"},
mode = EnumSource.Mode.EXCLUDE)
void exceptFourMonths_OthersAre31DaysLong(Month month) {
final boolean isALeapYear = false;
assertEquals(31, month.length(isALeapYear));
}
여기서 4월, 6월, 9월, 11월, 2월을 제외한 나머지 달들이 모두 31일이라는 것을 확인할 수 있다.
`names` 속성에는 문자열뿐만 아니라 정규 표현식도 전달할 수 있다.
@ParameterizedTest
@EnumSource(value = Month.class, names = ".+BER", mode = EnumSource.Mode.MATCH_ANY)
void fourMonths_AreEndingWithBer(Month month) {
EnumSet<Month> months =
EnumSet.of(Month.SEPTEMBER, Month.OCTOBER, Month.NOVEMBER, Month.DECEMBER);
assertTrue(months.contains(month));
}
이 예제에서는 "BER"로 끝나는 4개월(9월, 10월, 11월, 12월)을 테스트한다.
`@EnumSource`는 `@ValueSource`와 유사하게, 테스트 실행 당 하나의 인수만 전달할 때 적용된다.
CSV Literals
`String` 클래스의 toUpperCase() 메서드가 예상한 대로 대문자를 생성하는지 확인하려고 한다고 가졍해보다. 이때 `@ValueSource`만으로는 충분하지 않는다.
이런 시나리오를 위한 parameterize test를 작성하려면:
- 입력 값과 예상 값을 테스트 메서드에 전달해야 한다.
- 그 입력 값으로 실제 결과를 계산한다.
- 실제 값과 예상 값을 비교하여 확인한다.
즉, 여러 인수를 전달할 수 있는 인수 소스가 필요하다.
그 중 하나가 `@CsvSource`이다.
@ParameterizedTest
@CsvSource({"test,TEST", "tEst,TEST", "Java,JAVA"})
void toUpperCase_ShouldGenerateTheExpectedUppercaseValue(String input, String expected) {
String actualValue = input.toUpperCase();
assertEquals(expected, actualValue);
}
`@CsvSource`는 쉼표로 구분된 값(CSV)을 배열 형태로 받아들이며, 각 배열 항목은 CSV 파일의 한 줄에 해당한다.
이 소스는 배열의 각 항목을 쉼표로 나누어 parameterized test 메서드의 매개변수로 전달한다.
기본적으로 쉼표가 열 구분자이지만, delimiter 속성을 사용해 이를 커스터마이즈할 수 있다.
@ParameterizedTest
@CsvSource(value = {"test:test", "tEst:test", "Java:java"}, delimiter = ':')
void toLowerCase_ShouldGenerateTheExpectedLowercaseValue(String input, String expected) {
String actualValue = input.toLowerCase();
assertEquals(expected, actualValue);
}
이제 콜론(`:`)을 사용해 구분된 값이 전달되며, 여전히 CSV 형식으로 처리된다.
따라서, `@CsvSource`는 쉼표 또는 커스텀 구분자를 사용해 여러 인수를 전달할 수 있는 유용한 방식이다.
CSV Files
코드 내에서 CSV 값을 직접 전달하는 대신, 실제 CSV 파일을 참조할 수 있다.
예를 들어, 다음과 같은 내용을 가진 CSV 파일을 사용할 수 있다.
input,expected
test,TEST
tEst,TEST
Java,JAVA
이 CSV 파일을 읽고, 헤더 열을 무시하기 위해 `@CsvFileSource`를 사용할 수 있다.
@ParameterizedTest
@CsvFileSource(resources = "/data.csv", numLinesToSkip = 1)
void toUpperCase_ShouldGenerateTheExpectedUppercaseValueCSVFile(
String input, String expected) {
String actualValue = input.toUpperCase();
assertEquals(expected, actualValue);
}
- resources 속성은 클래스패스 내의 CSV 파일 리소스를 카리킨다. 여기에 여러 개의 파일을 전달할 수도 있다.
- numLinesToSkip 속성은 CSV 파일을 읽을 때 건너뛸 줄 수를 나타낸다. 기본적으로 `@CsvFileSource`는 아무 줄도 건너뛰지 않지만, 이 기능은 여기서처럼 헤더 줄을 건너뛸 때 유용하다.
추가 기능
- 구분자(delimiter): `@CsvSource`와 마찬가지로 구분자를 delimiter 속성으로 커스터마이즈할 수 있다.
- 줄 구분자(line seperator): lineSeperator 속성을 사용해 줄 구분자를 커스터마이즈할 수 있으며, 기본값은 새 줄(new line)이다.
- 파일 인코딩: encoding 속성으로 파일 인코딩을 커스터마이즈할 수 있으며, 기본값은 UTF-8이다.
이를 통해 코드 외부의 CSV 파일을 사용해 더 유연한 parameterized test를 수행할 수 있다.
Method
지금까지 다룬 argument source는 단순하여, 복잡한 객체를 전달하기에는 어려움이 있다.
더 복잡한 argument를 제공하기 위한 방법 중 하나는 메서드를 인수 소스로 사용하는 것이다. `@MethodSouce` 애너테이션을 사용하여 메서드에서 인수를 제공할 수 있다.
예를 들어, isBlank 메서드를 `MethodSource`로 테스트할 수 있다.
@ParameterizedTest
@MethodSource("provideStringsForIsBlank")
void isBlank_ShouldReturnTrueForNullOrBlankStrings(String input, boolean expected) {
assertEquals(expected, Strings.isBlank(input));
}
여기서 `MethodSource`에 전달된 이름은 실제 존재하는 메서드와 일치해야 한다. 다음으로 `provideStringsForIsBlank`라는 메서드를 작성해보자. 이 메서드는 Stream으로 인수를 반환한다.
private static Stream<Arguments> provideStringsForIsBlank() {
return Stream.of(
Arguments.of(null, true),
Arguments.of("", true),
Arguments.of(" ", true),
Arguments.of("not blank", false)
);
}
여기서는 Stream으로 인수를 반환하고 있지만 꼭 Stream이어야 할 필요는 없다. List와 같은 다른 컬렉션 타입도 사용할 수 있다.
단일 인수 제공
만약 각 테스트 실행에 대해 하나의 인수만 제공할 계획이라면, Arguments abstraction을 사용할 필요가 없다.
@ParameterizedTest
@MethodSource // hmm, no method name ...
void isBlank_ShouldReturnTrueForNullOrBlankStringsOneArgument(String input) {
assertTrue(Strings.isBlank(input));
}
private static Stream<String> isBlank_ShouldReturnTrueForNullOrBlankStringsOneArgument() {
return Stream.of(null, "", " ");
}
여기서 `@MethodSource`에 메서드 이름을 제공하지 않으면, JUnit은 테스트 메서드와 동일한 이름을 가진 소스 메서드를 찾는다.
외부 클래스의 메서드 사용
다른 테스트 클래스 간에 인수를 공유하는 것이 유용할 수 있다. 이 경우, 현재 클래스 외부에 있는 메서드를 정규화된 이름(fully qualified name)을 사용하여 참조할 수 있다.
class StringsUnitTest {
@ParameterizedTest
@MethodSource("com.baeldung.parameterized.StringParams#blankStrings")
void isBlank_ShouldReturnTrueForNullOrBlankStringsExternalSource(String input) {
assertTrue(Strings.isBlank(input));
}
}
public class StringParams {
static Stream<String> blankStrings() {
return Stream.of(null, "", " ");
}
}
Field
테스트 데이터 공급 방법으로 메서드를 argument source로 사용하는 것이 유용하다는 점에서, JUnit 5.11부터는 정적 필드(static field)를 통해 유사한 기능을 제공하는 `@FieldSource` 애너테이션이 추가되었다.
예를 들어, 다음과 같이 사용할 수 있다.
static List<String> cities = Arrays.asList("Madrid", "Rome", "Paris", "London");
@ParameterizedTest
@FieldSource("cities")
void isBlank_ShouldReturnFalseWhenTheArgHasAtLeastOneCharacter(String arg) {
assertFalse(Strings.isBlank(arg));
}
위 예제에서 볼 수 있듯이, `@FieldSource` 애너테이션은 테스트 데이터를 참조하는 정적 필드를 가리킨다. 이 필드는 Collection, Iterable, 객체 배열, 또는 Supplier<Stream>으로 표현될 수 있다. 이후 parameterized test는 각 테스트 입력마다 실행된다.
`@MethodSource`처럼, 정적 필드의 이름이 테스트 메서드와 동일한 경우 필드 이름을 생략할 수도 있다.
static String[] isEmpty_ShouldReturnFalseWhenTheArgHasAtLeastOneCharacter = { "Spain", "Italy", "France", "England" };
@ParameterizedTest
@FieldSource
void isEmpty_ShouldReturnFalseWhenTheArgHasAtLeastOneCharacter(String arg) {
assertFalse(arg.isEmpty());
}
Custom Argument Provider
테스트 인수를 전달하는 또 다른 접근법은 ArgumentsProvider라는 인터페이스를 구현하여 사용자 정의 인스 제공자를 사용하는 것이다.
먼저, ArgumentsProvider 인터페이스를 구현하는 클래스를 정의한다.
class BlankStringsArgumentsProvider implements ArgumentsProvider {
@Override
public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
return Stream.of(
Arguments.of((String) null),
Arguments.of(""),
Arguments.of(" ")
);
}
}
이 클래스는 Stream으로 인수를 반환하며, 각각 `null`, 빈 문자열, 공백 문자열을 포함한 값을 제공한다.
이제 `@ArgumentsSource` 애너테이션을 사용하여 이 사용자 정의 제공자를 테스트에 연결할 수 있다.
@ParameterizedTest
@ArgumentsSource(BlankStringsArgumentsProvider.class)
void isBlank_ShouldReturnTrueForNullOrBlankStringsArgProvider(String input) {
assertTrue(Strings.isBlank(input));
}
Custom Annotation
테스트 인수를 정적 변수에서 가져오고 싶다고 가정해보자.
static Stream<Arguments> arguments = Stream.of(
Arguments.of(null, true), // null 문자열은 공백으로 간주되어야 함
Arguments.of("", true),
Arguments.of(" ", true),
Arguments.of("not blank", false)
);
@ParameterizedTest
@VariableSource("arguments")
void isBlank_ShouldReturnTrueForNullOrBlankStringsVariableSource(
String input, boolean expected) {
assertEquals(expected, Strings.isBlank(input));
}
JUnit 5는 직접적으로 이 기능을 제공하지 않지만, 이를 구현할 수 있다.
사용자 정의 애너테이션 만들기
먼저, 애너테이션을 정의한다.
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@ArgumentsSource(VariableArgumentsProvider.class)
public @interface VariableSource {
/**
* 정적 변수 이름
*/
String value();
}
이제 애너테이션의 세부 정보를 소비하고 인수를 제공하기 위한 클래스를 작성해야 한다. JUnit5에서는 이를 위해 두 가지 abstractions을 제공한다.
- AnnotationConsumer: 애너테이션 세부 정보를 소비
- ArgumentsProvider: 테스트 인수를 제공
VariableArgumentsProvider 클래스 작성
다음으로, `VariableArgumentsProvider` 클래스에서 지정된 정적 변수의 값을 읽어 인수로 반환하는 메서드를 작성한다.
class VariableArgumentsProvider
implements ArgumentsProvider, AnnotationConsumer<VariableSource> {
private String variableName;
@Override
public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
return context.getTestClass()
.map(this::getField)
.map(this::getValue)
.orElseThrow(() ->
new IllegalArgumentException("Failed to load test arguments"));
}
@Override
public void accept(VariableSource variableSource) {
variableName = variableSource.value();
}
private Field getField(Class<?> clazz) {
try {
return clazz.getDeclaredField(variableName);
} catch (Exception e) {
return null;
}
}
@SuppressWarnings("unchecked")
private Stream<Arguments> getValue(Field field) {
Object value = null;
try {
value = field.get(null);
} catch (Exception ignored) {}
return value == null ? null : (Stream<Arguments>) value;
}
}
- AnnotationConsumer의 `accept` 메서드는 `VariableSource` 애너테이션의 값(즉, 변수 이름)을 읽어 `variableName`에 저장한다.
- `provideArguments` 메서드는 현재 테스트 클래스에서 이 변수의 필드를 찾아, 해당 값을 Stream<Arguments>로 반환한다.
- 이 모든 과정으로, 애너테이션에 명시된 정적 변수가 테스트 인수로 사용된다.
이로써 @VariableSource 애너테이션을 통해 정적 변수의 테스트 데이터를 간편하게 제공할 수 있다.
Repeatable Argument Source Annotations
JUnit 5.11부터 반복 가능한 argument source 애너테이션이 도입되었다. 이제 parameterized test에서 같은 argument source 애너테이션을 여러 번 사용할 수 있다.
예를 들어, 서로 다른 메서드에서 제공하는 데이터를 통해 테스트를 실행하기 위해 `@MethodSource`를 두 번 사용할 수 있다.
static List<String> asia() {
return Arrays.asList("Japan", "India", "Thailand");
}
static List<String> europe() {
return Arrays.asList("Spain", "Italy", "England");
}
@ParameterizedTest
@MethodSource("asia")
@MethodSource("europe")
void whenStringIsLargerThanThreeCharacters_thenReturnTrue(String country) {
assertTrue(country.length() > 3);
}
이 예제에서, `@MethodSource("asia")`와 `@MethodSource("europe")` 두 가지 메서드에서 데이터를 받아 테스트를 실행한다. 각 메서드에서 제공된 모든 인수에 대해 테스트가 실행된다.
반복 가능한 argument resource 애너테이션 목록
- `@VariableSource`
- `@EnumSource`
- `@MethodSource`
- `@FieldSource`
- `@CsvSource`
- `@CsvFileSource`
- `@ArgumentsSource`
이를 통해 여러 소스에서 데이터를 받아 유연하고 효율적인 parameterized test가 가능하다.
Argument Conversion
Argument converters는 parameterized test의 기본 인수를 더 복잡한 데이터 구조로 변환하는 편리한 방법이다.
Implicit Conversion(암시적 변환)
`@EnumTest` 중 하나를 `@CsvSource`를 사용해 다시 작성해 보자.
@ParameterizedTest
@CsvSource({"APRIL", "JUNE", "SEPTEMBER", "NOVEMBER"}) // 문자열 전달
void someMonths_Are30DaysLongCsv(Month month) {
final boolean isALeapYear = false;
assertEquals(30, month.length(isALeapYear));
}
보기에는 작동하지 않을 것 같지만, JUnit 5는 `String` 인수를 지정된 열겨형 타입으로 변환한다. 이와 같은 케이스를 지원하기 위해, JUnit Jupiter는 여러 내장된 암시적 타입 변환기를 제공한다.
변환 과정은 각 메서드 매개변수의 선언된 타입에 따라 달라지며, `String` 인스턴스는 다음과 같은 타입으로 변환할 수 있다.
- UUID
- Locale
- LocalDate, LocalTime, LocalDateTime, Year, Month 등
- File과 Path
- URL과 URI
- Enum 서브클래스
Explicit Conversion (명시적 변환)
때로는 사용자 정의 및 명시적 변환기를 사용해야 하는 경우도 있다.
예를 들어, `yyyy/mm/dd` 형식의 문자열을 LocalDate로 변환하려면:
먼저, ArgumentConverter 인터페이스를 구현한다.
class SlashyDateConverter implements ArgumentConverter {
@Override
public Object convert(Object source, ParameterContext context)
throws ArgumentConversionException {
if (!(source instanceof String)) {
throw new IllegalArgumentException(
"The argument should be a string: " + source);
}
try {
String[] parts = ((String) source).split("/");
int year = Integer.parseInt(parts[0]);
int month = Integer.parseInt(parts[1]);
int day = Integer.parseInt(parts[2]);
return LocalDate.of(year, month, day);
} catch (Exception e) {
throw new IllegalArgumentException("Failed to convert", e);
}
}
}
여기서 SlashyDateConverter는 `yyyy/mm/dd` 형식의 문자열을 분리하여 LocalDate 인스턴스로 변환한다.
그 다음, `@ConvertWith` 애너테이션을 통해 변환기를 참조한다.
@ParameterizedTest
@CsvSource({"2018/12/25,2018", "2019/02/11,2019"})
void getYear_ShouldWorkAsExpected(
@ConvertWith(SlashyDateConverter.class) LocalDate date, int expected) {
assertEquals(expected, date.getYear());
}
이제 `@ConvertWith` 애너테이션을 통해 `SlashDateConverter`를 적용하여 문자열을 `LocalDate`로 변환한 후, 해당 year 값을 테스트할 수 있다.
Argument Accessor
parameterized test에서 각 인수는 메서드 매개변수에 해당한다. 따라서 여러 개의 인수를 전달할 때, 테스트 메서드의 서명이 매우 길고 복잡해질 수 있다.
이를 해결하는 한 가지 방법은 전달된 모든 인수를 ArgumentAccessor 인스턴스에 캡슐화하고, 인덱스와 타입으로 인수를 검색하는 것이다.
예를 들어, `Person` 클래스가 다음과 같다고 가정해보자.
class Person {
String firstName;
String middleName;
String lastName;
// 생성자
public String fullName() {
if (middleName == null || middleName.trim().isEmpty()) {
return String.format("%s %s", firstName, lastName);
}
return String.format("%s %s %s", firstName, middleName, lastName);
}
}
이제 `fullName()` 메서드를 테스트하기 위해 네 개의 인수(`firstName`, `middleName`, `lastName`, `expectedFullName`)를 전달할 것이다. ArgumentsAccessor를 사용해 테스트 인수를 직접 검색하여 메서드 매개변수로 선언하지 않고도 사용할 수 있다.
@ParameterizedTest
@CsvSource({"Isaac,,Newton,Isaac Newton", "Charles,Robert,Darwin,Charles Robert Darwin"})
void fullName_ShouldGenerateTheExpectedFullName(ArgumentsAccessor argumentsAccessor) {
String firstName = argumentsAccessor.getString(0);
String middleName = (String) argumentsAccessor.get(1);
String lastName = argumentsAccessor.get(2, String.class);
String expectedFullName = argumentsAccessor.getString(3);
Person person = new Person(firstName, middleName, lastName);
assertEquals(expectedFullName, person.fullName());
}
여기서는 전달된 모든 인수를 ArgumentsAccessor에 캡슐화하여 테스트 메서드 본문에서 인덱스로 각 인수를 검색한다.
ArgumentsAccessor의 메서드 설명
- `getString(index)`: 특정 인덱스의 요소를 `String`으로 변환하여 반환. 기본 타입도 이와 유사하게 변환할 수 있다.
- `get(index)`: 특정 인덱스의 요소를 `Object`로 반환
- `get(index, type)`: 특정 인덱스의 요소를 주어진 타입으로 변환하여 반환
ArgumentsAccessor를 사용하면 인수의 인덱스와 타입을 명시적으로 지정해 코드의 가독성을 높일 수 있다.
Argument Aggregator
ArgumentsAccessor를 직접 사용하는 경우, 테스트 코드가 덜 읽기 쉬워지거나 재사용성이 떨어질 수 있다. 이를 해결하기 위해, ArgumentsAggregator 인터페이스를 구현하여 커스텀 인수 집계자를 작성할 수 있다.
먼저, ArgumentsAggregator 인터페이스를 구현하여 `PersonAggregator` 클래스를 작성한다.
class PersonAggregator implements ArgumentsAggregator {
@Override
public Object aggregateArguments(ArgumentsAccessor accessor, ParameterContext context)
throws ArgumentsAggregationException {
return new Person(
accessor.getString(1), accessor.getString(2), accessor.getString(3));
}
}
`PersonAggregator`는 전달된 마지막 세 개의 인수를 사용하여 `Person` 객체를 생성한다.
그 다음 `@AggregateWith` 애너테이션을 통해 이 `PersonAggregator`를 참조하여 인수를 한 번에 `Person` 객체로 집계할 수 있다.
@ParameterizedTest
@CsvSource({"Isaac Newton,Isaac,,Newton", "Charles Robert Darwin,Charles,Robert,Darwin"})
void fullName_ShouldGenerateTheExpectedFullName(
String expectedFullName,
@AggregateWith(PersonAggregator.class) Person person) {
assertEquals(expectedFullName, person.fullName());
}
Customizing Display Names
parameterized test에서 기본적으로 각 테스트의 display 이름은 호출 인덱스와 전달된 모든 인수의 문자열 표현을 포함한다.
├─ someMonths_Are30DaysLongCsv(Month)
│ │ ├─ [1] APRIL
│ │ ├─ [2] JUNE
│ │ ├─ [3] SEPTEMBER
│ │ └─ [4] NOVEMBER
하지만, `@ParameterizedTest` 애너테이션의 `name` 속성을 사용해 이 디스플레이를 커스터마이징할 수 있다.
@ParameterizedTest(name = "{index} {0} is 30 days long")
@EnumSource(value = Month.class, names = {"APRIL", "JUNE", "SEPTEMBER", "NOVEMBER"})
void someMonths_Are30DaysLong(Month month) {
final boolean isALeapYear = false;
assertEquals(30, month.length(isALeapYear));
}
├─ someMonths_Are30DaysLong(Month)
│ │ ├─ 1 APRIL is 30 days long
│ │ ├─ 2 JUNE is 30 days long
│ │ ├─ 3 SEPTEMBER is 30 days long
│ │ └─ 4 NOVEMBER is 30 days long
사용할 수 있는 플레이스 홀더
- `{displayName}`: 메서드의 디스플레이 이름으로 대체된다. `@Display` 어노테이션이 제공된 경우, 그 값으로 대체된다.
- `{index}`: 호출 인덱스
- `{arguments}`: 쉼표로 구분된 모든 인수의 리스트
- `{argumentsWithName}`: 이름이 지정된 인수 표시
- `{argumentsSetName}`: 첫 번째 인수 세트 이름(argumentSet 사용 시)
- `{0}, {1}, ...`: 개별 인수
public record Country(String name, long population) {
}
public class CountryUtil {
private static final long TRESHOLD = 100_000_000;
public boolean isBigCountry(Country country) {
return country.population() > TRESHOLD;
}
}
`arguments(...ARGS)` 팩토리 사용
`@FieldSource`와 함께 `arguments` 팩토리를 사용하여 국가 이름과 인구 데이터를 테스트:
private static List<Arguments> simpleArguments = Arrays.asList(
arguments("India", 1_450_935_791),
arguments("China", 1_419_321_278),
arguments("United States", 345_426_571)
);
├─ Big Countries - Simple
│ │ ├─ [India, 1450935791]: 'India' with population '1450935791' people
│ │ ├─ [China, 1419321278]: 'China' with population '1419321278' people
│ │ └─ [United States, 345426571]: 'United States' with population '345426571' people
`arguments( named(NAME, ARG), ...ARGS)` 팩토리 사용
다음 예제는 이전 예제와 유사하다. 하지만 이번에는 `named(NAME, ARG)`를 사용하여 각 인수에 대한 커스텀 이름을 제공한다.
@DisplayName("Big Countries - With Named Arguments")
@ParameterizedTest(name = "{argumentsWithNames}")
@FieldSource("namedArguments")
public void givenBigCountryData_usingCountryUtil_isBigCountry_shouldReturnTrue_namedArguments(
String countryName, long countryPopulation) {
Country country = new Country(countryName, countryPopulation);
boolean isBigCountry = CountryUtil.isBigCountry(country);
assertTrue(isBigCountry, "The country provided is not considered big!");
}
private static List<Arguments> namedArguments = Arrays.asList(
arguments(named("Most populated country in Asia", "India"), 1_450_935_791),
arguments("China", 1_419_321_278),
arguments(named("Biggest country in America", "United States"), 345_426_571)
);
├─ Big Countries - With Named Arguments
│ │ ├─ countryName=Most populated country in Asia, countryPopulation=1450935791
│ │ ├─ countryName=China, countryPopulation=1419321278
│ │ └─ countryName=Biggest country in America, countryPopulation=345426571
`argumentSet( NAME_OF_ARGS)` 팩토리 사용
전체 인수 세트에 이름을 붙여 더 구체적으로 설명
@DisplayName("Big Countries - Named Set")
@ParameterizedTest(name = "''{0}'' is considered {argumentSetName} due to his population of ''{1}'' people")
@FieldSource("argumentSets")
public void notablePeople_withSetName(String name, long population) {
Country country = new Country(name, population);
boolean isBigCountry = CountryUtil.isBigCountry(country);
assertTrue(isBigCountry, "The country provided is not considered big!");
}
private static List<Arguments> argumentSets = Arrays.asList(
argumentSet("the most populated country in Asia", "India", 1_450_935_791),
argumentSet("the second most populated country in Asia", "China", 1_419_321_278),
argumentSet("biggest country in America", "United States", 345_426_571)
);
├─ Big Countries - Named Set
│ ├─ 'India' is considered the most populated country in Asia due to his population of '1450935791' people
│ ├─ 'China' is considered the second most populated country in Asia due to his population of '1419321278' people
│ └─ 'United States' is considered biggest country in America due to his population of '345426571' people
참조
https://www.baeldung.com/parameterized-tests-junit-5
'Java' 카테고리의 다른 글
[AssertJ] Exception Assertions (0) | 2024.10.31 |
---|---|
객체 지향 프로그래밍(Object-Oriented Programming, OOP) (0) | 2024.10.26 |
Google Java Style Guide (2) | 2024.10.25 |
[Java/김영한] 패키지 (1) | 2024.09.08 |
[Java/김영한] 생성자 (1) | 2024.09.08 |