아래 사이트에 게시된 글을 정리/번역해 작성한 글입니다. 작성 중에 생략된 내용이 있을 수 있습니다.
https://www.baeldung.com/parameterized-tests-junit-5
JUnit5의 새로운 기능 중 하나가 바로 Parameterized test이다. 이 기능은 하나의 테스트 메서드를 다른 파라미터들로 여러번 실행시킬 수 있다.
testCompile("org.junit.jupiter:junit-jupiter-params:5.10.0")
아주 간단한 예시를 통해 Parameterized Test를 살펴보자
public class Numbers {
public static boolean isOdd(int number) {
return number % 2 != 0;
}
}
@ParameterizedTest
@ValueSource(ints = {1, 3, 5, -3, 15, Integer.MAX_VALUE}) // six numbers
void isOdd_ShouldReturnTrueForOddNumbers(int number) {
assertTrue(Numbers.isOdd(number));
}
@ParameterizedTest 어노테이션으로 Parameterized Test를 실행시킬 수 있다.@ValueSource의 ints 배열은 6개의 숫자를 가지고 있다. 테스트 함수가 6번 실행되면서 각각의 값을 number 파라미터로 넘겨준다.Parameterized test는 다른 인자를 전달하면서 같은 테스트를 여러개 실행시킨다. 어떤 인자 값들을 전달할 수 있는지 알아보자.
@ValueSource 어노테이션을 사용하면, 어떤 리터럴 값의 배열이든 전달 가능하다.
String 배열을 이용하는 간단한 예시
public class Strings {
public static boolean isBlank(String input) {
return input == null || input.trim().isEmpty();
}
}
@ParameterizedTest
@ValueSource(strings = {"", " "})
void isBlank_ShouldReturnTrueForNullOrBlankStrings(String input) {
assertTrue(Strings.isBlank(input));
}
@ValueSource는 아래의 값들을 지원한다.
shortbyteintlongfoatdoublecharjava.lang.Stringjava.lang.Class또한, @ValueSource는 테스트 메서드에 한번에 하나의 인자만 전달할 수 있다. 그리고 @ValueSource에는 null값을 이용할 수 없다는 것 또한 알아두자.
JUnit 5.4부터 @NullSource 어노테이션으로 null 값을 인자로 넘겨줄 수있다.
@ParameterizedTest
@NullSource
void isBlank_ShouldReturnTrueForNullInputs(String input) {
assertTrue(Strings.isBlank(input));
}
또한 @EmptySource 로 빈 값을 인자로 전달할 수 있다. 빈 값이란 빈 문자열, 빈 배열, 빈 콜렉션 같은 값들이 해당된다.
@ParameterizedTest
@EmptySource
void isBlank_ShouldReturnTrueForEmptyStrings(String input) {
assertTrue(Strings.isBlank(input));
}
아래와 같이 @NullAndEmptySource로 null 과 빈 값을 모두 넘겨줄 수 있다.
@ParameterizedTest
@NullAndEmptySource
void isBlank_ShouldReturnTrueForNullAndEmptyStrings(String input) {
assertTrue(Strings.isBlank(input));
}
다양한 인자를 함께 전달하고 싶다면 @ValueSource와 같이 사용할 수도 있다.
@ParameterizedTest
@NullAndEmptySource
@ValueSource(strings = {" ", "\t", "\n"})
void isBlank_ShouldReturnTrueForAllTypesOfBlankStrings(String input) {
assertTrue(Strings.isBlank(input));
}
@EnumSource 어노테이션으로 열거형의 값들을 테스트 함수의 인자로 하나씩 넘겨줄 수 있다.
@ParameterizedTest
@EnumSource(Month.class) // passing all 12 months
void getValueForAMonth_IsAlwaysBetweenOneAndTwelve(Month month) {
int monthNumber = month.getValue();
assertTrue(monthNumber >= 1 && monthNumber <= 12);
}
names 로 아래처럼 필터링을 사용할 수 있다.
@ParameterizedTest
@EnumSource(value = Month.class, names = {"APRIL", "JUNE", "SEPTEMBER", "NOVEMBER"})
void someMonths_Are30DaysLong(Month month) {
final boolean isALeapYear = false;
assertEquals(30, month.length(isALeapYear));
}
mode를 EXCLUDE로 설정할 경우 names에 해당하는 것을 제외시킨다. (default: INCLUDE)
@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));
}
아래와 같이 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));
}
@ValueSource는 한 번에 하나의 값만 넘겨주기 때문에 입력 값과 기대 값을 동시에 입력할 수 없다. 여러개의 값을 전달하고자 할 경우 @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 파일에서 하나의 열을 의미한다고 볼 수 있다.
기본적으로 구분자로 쉼표를 사용하는데 아래처럼 커스텀 딜리미터를 사용할 수 있다. 넘겨줄 인자 값에 쉼표가 포함되어 있다면 유용하게 사용할 수 있다.
@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 파일을 작성한 후 @CsvFileSource 어노테이션으로 불러와서 작성할 수 있다.
예시 CSV 파일:
input,expected
test,TEST
tEst,TEST
Java,JAVA
@ParameterizedTest
@CsvFileSource(resources = "/data.csv", numLinesToSkip = 1)
void toUpperCase_ShouldGenerateTheExpectedUppercaseValueCSVFile(
String input, String expected) {
String actualValue = input.toUpperCase();
assertEquals(expected, actualValue);
}
resources 속성에 CSV 파일 경로를 작성해주면 된다. 여러개의 파일을 넘겨줄 수 있다. numLinesToSkip 속성을 이용해 헤더 열을 스킵하도록 설정했다.
@CsvSource에서 커스텀 딜리미터를 사용한 것처럼 @CsvFileSource에서도 여러 옵션을 설정할 수 있다.
lineSeperator 속성으로 줄 구분자를 커스터마이징 할 수 있다. 기본 구분자는 newline이다.encoding 속성으로 인코딩 방식을 설정할 수 있다. 기본 값은 UTF-8이다.보다 복잡한 값을 넘겨주기 위해 @MethodSource 어노테이션과 함께 메서드를 사용할 수 있다.
@ParameterizedTest
@MethodSource("provideStringsForIsBlank")
void isBlank_ShouldReturnTrueForNullOrBlankStrings(String input, boolean expected) {
assertEquals(expected, Strings.isBlank(input));
}
private static Stream<Arguments> provideStringsForIsBlank() {
return Stream.of(
Arguments.of(null, true),
Arguments.of("", true),
Arguments.of(" ", true),
Arguments.of("not blank", false)
);
}
@MethodSource에 메서드 이름을 지정하지 않은 경우, 테스트 메서드의 이름과 동일한 메서드를 찾아서 사용한다.
@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, "", " ");
}
아래와 같은 형식으로 다른 클래스의 메서드를 사용할 수도 있다.
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, "", " ");
}
}
JUnit 5.11 부터 @FieldSource 어노테이션을 사용하여 스테틱 필드를 사용할 수 있다.
static List<String> cities = Arrays.asList("Madrid", "Rome", "Paris", "London");
@ParameterizedTest
@FieldSource("cities")
void isBlank_ShouldReturnFalseWhenTheArgHasAtLEastOneCharacter(String arg) {
assertFalse(Strings.isBlank(arg));
}
@MethodSource 처럼 @FieldSource에 필드 이름을 명시하지 않을 경우, 테스트 함수의 이름과 같은 필드를 사용한다.
static String[] isEmpty_ShouldReturnFalseWhenTheArgHasAtLEastOneCharacter = { "Spain", "Italy", "France", "England" };
@ParameterizedTest
@FieldSource
void isEmpty_ShouldReturnFalseWhenTheArgHasAtLEastOneCharacter(String arg) {
assertFalse(arg.isEmpty());
}
ArgumentProvider 인터페이스를 사용한 custom argument provider를 구현해 사용할 수 있다.
class BlankStringsArgumentsProvider implements ArgumentsProvider {
@Override
public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
return Stream.of(
Arguments.of((String) null),
Arguments.of(""),
Arguments.of(" ")
);
}
}
테스트 메서드에 @ArgumentsSource 어노테이션을 붙여 커스텀 프로바이더를 사용한다.
@ParameterizedTest
@ArgumentsSource(BlankStringsArgumentsProvider.class)
void isBlank_ShouldReturnTrueForNullOrBlankStringsArgProvider(String input) {
assertTrue(Strings.isBlank(input));
}
https://www.baeldung.com/parameterized-tests-junit-5#9-custom-annotation
JUnit 5.11부터 대부분의 어노테이션들이 반복가능해졌다. 동일한 argument source annotation으로 parameterized test에 여러번 적용할 수 있다. 예를 들어 다음과 같이 @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);
}
다음과 같은 argument source annotation들에 이 기능을 적용가능하다
*@ValueSource**@EnumSource**@MethodSource**@FieldSource**@CsvSource**@CsvFileSource**@ArgumentsSource*Argument converter는 primitive argument를 더 복잡한 데이터 구조에 매핑할 수 있도록 한다.
@ParameterizedTest
@CsvSource({"APRIL", "JUNE", "SEPTEMBER", "NOVEMBER"}) // Passing strings
void someMonths_Are30DaysLongCsv(Month month) {
final boolean isALeapYear = false;
assertEquals(30, month.length(isALeapYear));
}
위의 코드에서 @CsvSource로 값을 넘겨주고 있는데 테스트 메서드의 파라미터는 Enum 값을 받고 있다. 오류가 발생할 것같지만 위의 코드는 정상적으로 작동한다.
JUnit5는 built-in implicit type converter를 통해 암시적 형변환을 제공한다. String 인스턴스를 다음과 같은 타입으로 변환할 수 있다.
Argument Source를 명시적으로 변환하고자 하는 경우 ArgumentConverter 인터페이스를 구현해 커스텀 컨버터를 만들 수 있다.
아래 예시 코드는 yyyy/mm/dd 형식의 문자열을 LocalDate 인스턴스로 변환한다.
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);
}
}
}
아래와 같이 @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());
}
ArgumentAccessor를 사용하면 테스트 메서드에 여러개의 인자를 넘겨줄 때 하나의 값으로 묶어서 넘겨줄 수 있다.
예를 들어 다음과 같은 클래스가 있을 때 fullName() 메서드를 테스트하기 위해서 firstName, middleName, lastName, expectdName 4개의 인자가 필요하다.
class Person {
String firstName;
String middleName;
String lastName;
// constructor
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() 의 테스트 메서드를 ArgumentAccessor를 이용해 작성하면 다음과 같다.
@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());
}
ArgumentAccessor 의 가독성과 재사용성이 떨어진다는 단점을 보완하기 위해 ArgumentAggregator 인터페이스를 구현한 커스텀 Aggregator를 사용할 수 있다.
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를 아래처럼 @AggregateWith 어노테이션과 함께 사용하면 입력값을 받아서 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());
}
Parameterized Test의 테스트 이름을 커스텀하려면 @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
Display name 커스터마이징을 위해 사용할 수 있는 형식은 다음과 같다.
https://www.baeldung.com/parameterized-tests-junit-5#display-name
이 글에서는 JUnit 5에서 매개변수화된 테스트의 핵심을 살펴보았다.
매개변수화된 테스트는 두 가지 측면에서 일반 테스트와 다르다는 것을 배웠다. @ParameterizedTest로 주석을 달고, 선언된 인수의 소스가 필요하다.
또한 JUnit이 인수를 사용자 정의 대상 유형으로 변환하거나 테스트 이름을 사용자 정의하는 몇 가지 기능을 제공한다.