본 글은 Baeldung의 Gide to JUnit 5 Parameterized Tests를 일부 번역한 글입니다.
정확한 정보는 원문을 통해 확인해주시길 바랍니다!
JUnit 5 (OverView)
Parameterized Test는 JUnit5의 새로운 기능 중 하나로, 하나의 테스트 메서드를 서로 다른 인자들을 이용해 여러번 실행할 수 있는 테스트 도구이다. 다음과 같은 유틸성 함수를 테스트한다고 해보자.
public class Numbers {
public static boolean isOdd(int number) {
return number % 2 != 0;
}
}
`@ParameterizedTest` 어노테이션을 사용해 Parameterized Test를 구현할 수 있다.
@ParameterizedTest
@ValueSource(ints = {1, 3, 5, -3, 15, Integer.MAX_VALUE}) // six numbers
void isOdd_ShouldReturnTrueForOddNumbers(int number) {
assertTrue(Numbers.isOdd(number));
}
`@ValueSource` 어노테이션을 통해 주어진 6개의 값을 이용해, `isOdd_ShouldReturnTrueForOddNumbers` 메서드가 6번 실행된다.
위 예시에서처럼, parameterized test를 하기 위해서는 2가지 요소를 지정해야 한다.
- a source of argument: 실행 인자를 지정해야 한다, 위 예시에서는 int 배열로 지정
- a way to access then: 인자에 접근할 방법을 지정해야 한다, 위 예시에서는 number 인자로 지정
테스트 인자 지정(Argument Sources)
Simple Values
- `@ValueSource` 어노테이션으로 primitive 값의 배열을 전달할 수 있다.
- short, int, long, float, double, char, String, Class
- 단, null 값은 사용할 수 없다.
@ParameterizedTest
@ValueSource(strings = {"", " "})
void isBlank_ShouldReturnTrueForNullOrBlankStrings(String input) {
assertTrue(Strings.isBlank(input));
}
Null and Empty Values
- `@NullSource` 어노테이션으로 null 값을 전달할 수 있다.
- `@EmptySource` 어노테이션으로 빈 값을 전달할 수 있다.
- String, Collection, Array 타입 인자에 대해, 빈 값을 전달할 수 있다.
- composed 어노테이션인 `@NullAndEmptySource`로 null, empty 값을 동시에 사용할 수도 있다.
- 위 어노테이션들은 `@ValueSource`와도 함께 사용할 수 있다.
@ParameterizedTest
@NullAndEmptySource
@ValueSource(strings = {" ", "\\t", "\\n"})
void isBlank_ShouldReturnTrueForAllTypesOfBlankStrings(String input) {
assertTrue(Strings.isBlank(input));
}
Enum
- `@EnumSource` 어노테이션으로 Enum 클래스의 값으로 테스트를 실행할 수 있다.
- Enum 값을 모두 사용할 수도 있고, `names` 속성으로 일부 값만 사용할 수도 있다.
- `mode` 속성값을 `EXCLUDE`로 지정하면, `names`에 명시한 값을 제외한 값만 사용할 수 있다.
- `mode` 속성값을 `MATCH_ANY`로 지정하고 `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));
}
CSV Literals
- 여러개의 인자를 넘겨야할 때, `@CSVSource` 어노테이션을 사용할 수 있다.
- 구분자(디폴트: “,”)로 연결된 값의 array를 입력할 수 있다.
- `delimiter` 속성으로 다른 구분자를 지정할 수 있다.
- `@CsvFileSource` 어노테이션을 사용해 CSV 파일로 인자를 넘길 수도 있다.
@ParameterizedTest
@CsvSource({"test,TEST", "tEst,TEST", "Java,JAVA"})
void toUpperCase_ShouldGenerateTheExpectedUppercaseValue(String input, String expected) {
String actualValue = input.toUpperCase();
assertEquals(expected, actualValue);
}
Method
- 더 복잡한 인자(객체 등)를 사용해야할 때 `@MethodSource` 어노테이션을 사용할 수 있다.
- `@MethodSource`에서 사용하는 메서드는 `Collection`(collection-like 인터페이스)을 반환해야한다.
- 어노테이션에 메서드 이름을 지정하지 않으면, 테스트 메서드와 동일한 메서드가 디폴트로 지정된다.
@ParameterizedTest
@MethodSource("provideStringsForIsBlank")
void isBlank_ShouldReturnTrueForNullOrBlankStrings(String input, boolean expected) {
assertEquals(expected, Strings.isBlank(input));
}
테스트 인자 형변환(Argument Conversion)
인자 변환 기능을 통해 primitive 인자 값을 더 복잡한 데이터 구조로 간단하게 맵핑할 수 있다.
Implicit Conversion
@ParameterizedTest
@CsvSource({"APRIL", "JUNE", "SEPTEMBER", "NOVEMBER"}) // Passing strings
void someMonths_Are30DaysLongCsv(Month month) {
final boolean isALeapYear = false;
assertEquals(30, month.length(isALeapYear));
}
위 예시에서, `@CsvSource` 를 통해 넘긴 인자 타입은 `String`이고, 테스트 메서드에서 받는 인자 타입은 Enum(`Month`) 이기때문에 테스트 메서드가 동작하지 않을 것처럼 보인다.
하지만 이 테스트 메서드는 정상적으로 동작하는데, JUnit 5에서 `String` 타입 인자를 특정 Enum 타입으로 변환해주기 때문이다. 인자 형변환은 테스트 메서드에 명시된 파라미터의 타입에 따라 달라지고, `String` 타입의 인자를 다음과 같은 타입으로 형변환할 수 있다.
- UUID
- Locale
- LocalDate, LocalTime, LocalDateTime, Year, Month
- File, Path
- URL, URI
- Enum subclasses
Explicit Conversion
Implicit Conversion으로 불가능한 형변환의 경우 커스텀 형변환 converter를 설정할 수 있다.
- `ArgumentConverter` 인터페이스를 구현한다.
- 다음의 예제는 `yyyy/mm/dd` 형식의 `String` 인자를 `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` 어노테이션을 사용해 converter를 지정한다.
@ParameterizedTest
@CsvSource({"2018/12/25,2018", "2019/02/11,2019"})
void getYear_ShouldWorkAsExpected(
@ConvertWith(SlashyDateConverter.class) LocalDate date, int expected) {
assertEquals(expected, date.getYear());
}
테스트 인자 접근자(Argument Accessor)
Parameterized Test를 할 때, 넘기는 인자의 개수가 많아질수록 테스트 메서드의 시그니처가 복잡해지는 문제가 있다. 이때, 모든 인자를 `ArgumentsAccesor`의 인스턴스로 캡슐화해서 이 문제를 해결할 수 있다. 각 인자는 인덱트와 타입을 통해 값을 읽을 수 있다.
@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());
}
- `getString(index)` : 인덱스의 값을 String으로 변환한 값을 반환한다.
- `get(index)` : 인덱스의 값을 Object 타입으로 반환한다.
- `get(index, type)` : 인덱스의 값을 주어진 type으로 변환한 값을 반환한다.
테스트 인자 어그리게이터(Argument Aggregator)
`ArgumentsAccessor`를 사용해 테스트를 구현했을 때, 테스트 코드의 가독성이 떨어질 수 있다. 이때, `ArgumentsAggregator`를 사용해 테스트 코드를 개선할 수 있다.
- `ArgumentsAggregator` 인터페이스를 구현한다.
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));
}
}
- `@AggregateWith` 어노테이션을 사용해 aggregator를 지정한다.
@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());
}