https://www.baeldung.com/parameterized-tests-junit-5
Guide to JUnit 5 Parameterized Tests | Baeldung
Learn how to simplify test coverage in JUnit 5 with parameterized tests
www.baeldung.com
JUnit5 는 여러 유용한 기능으로 테스트를 쉽게 할 수 있게 한다
특히 Parameterized 된 테스트를 이용하면, 다른 매개변수를 사용하여 단일 테스트를 여러번 실행할 수 있어 유용하다
https://junit.org/junit5/docs/current/user-guide/#running-tests-ide-eclipse
JUnit 5 User Guide
Although the JUnit Jupiter programming model and extension model do not support JUnit 4 features such as Rules and Runners natively, it is not expected that source code maintainers will need to update all of their existing tests, test extensions, and custo
junit.org
gradle
testImplementation("org.junit.jupiter:junit-jupiter:5.9.2")
First Impression
test 해볼 코드 작성
public class Numbers {
public static boolean isOdd(int number) {
return number % 2 != 0;
}
}
@ParameterizedTest 애노테이션을 붙인 후
입력한 밸류 값을 @ValueSource에 넣어준다
총 6번 실행될 것이다
@ParameterizedTest
@ValueSource(ints = {1, 3, 5, -3, 15, Integer.MAX_VALUE}) // six numbers
void isOdd_ShouldReturnTrueForOddNumbers(int number) {
assertTrue(Numbers.isOdd(number));
}
it assigns a different value from the @ValueSource array to the number method parameter.
- a source of arguments, in this case, an int array
- a way to access them, in this case, the number parameter
Argument Sources
숫자가 아니 다른 인수도 가능하다
Simple Values
public class Strings {
public static boolean isBlank(String input) {
return input == null || input.trim().isEmpty();
}
}
TestCode작성
@ParameterizedTest
@ValueSource(strings = {"", " "})
void isBlank_ShouldReturnTrueForNullOrBlankStrings(String input) {
assertTrue(Strings.isBlank(input));
}
One of the limitations of value sources is that they only support these types:
- short (with the shorts attribute)
- byte (bytes attribute)
- int (ints attribute)
- long (longs attribute)
- float (floats attribute)
- double (doubles attribute)
- char (chars attribute)
- java.lang.String (strings attribute)
- java.lang.Class (classes attribute)
보면 매번 테스트 메소드에 하나의 parameter만 전달할 수 있다
해당 애노테이션에서 null은 인수로 전달할 수 없다 <<
그치만 ㅎㅎㅎ 다음에서는 @NullSource를 넣어서 null 값을 전달할 수 있다
Null and Empty Values
As of JUnit 5.4, we can pass a single null value to a parameterized test method using @NullSource
@ParameterizedTest
@NullSource
void isBlank_ShouldReturnTrueForNullInputs(String input) {
assertTrue(Strings.isBlank(input));
}
primitive data types can't accept null values, we can't use the @NullSource for primitive arguments.
Quite similarly, we can pass empty values using the @EmptySource annotation:
@ParameterizedTest
@EmptySource
void isBlank_ShouldReturnTrueForEmptyStrings(String input) {
assertTrue(Strings.isBlank(input));
}
@EmptySource 또한 단일 빈 인수를 전달한다
매개변수가 String인수 일 경우에는 Collection type 및 배열에 대해 빈값을 제공할 수 있다
null 및 빈 값을 모두 전달하기 위해 구성된 @NullAndEmptySource 주석을 사용할 수 있다
@ParameterizedTest
@NullAndEmptySource
void isBlank_ShouldReturnTrueForNullAndEmptyStrings(String input) {
assertTrue(Strings.isBlank(input));
}
@EmptySource 와 마찬가지로 Strings, Collections and arrays에 대해 제공해준다
또한 더 많은 매개변수 테스트를 위해서 위의 내용을 전부 합쳐서 애노테이션을 달 수 있다
@ParameterizedTest
@NullAndEmptySource
@ValueSource(strings = {" ", "\t", "\n"})
void isBlank_ShouldReturnTrueForAllTypesOfBlankStrings(String input) {
assertTrue(Strings.isBlank(input));
}
Enum
In order to run a test with different values from an enumeration, we can use the @EnumSource annotation.
예를 들어 월로 Enum 을 열거했다고 치면
@ParameterizedTest
@EnumSource(Month.class) // passing all 12 months
void getValueForAMonth_IsAlwaysBetweenOneAndTwelve(Month month) {
int monthNumber = month.getValue();
assertTrue(monthNumber >= 1 && monthNumber <= 12);
}
해당 속성을 사용하여 필터링이 가능하다
4, 6, 9, 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));
}
또한 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));
}
이러한 리터럴 문자열 말고 정규식도 사용 가능하다
@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 와 매우 유사하며 @EnumSource는 테스트 실행당 하나 의 인수만 전달하려는 경우에만 적용할 수 있다
CSV Literals
Suppose we're going to make sure that the toUpperCase() method from String generates the expected uppercase value. @ValueSource won't be enough.
To write a parameterized test for such scenarios, we have to
- Pass an input value and an expected value to the test method //입력값 , 예상값을 전달
- Compute the actual result with those input values
- Assert the actual value with the expected value
So, we need argument sources capable of passing multiple arguments.
@ParameterizedTest
@CsvSource({"test,TEST", "tEst,TEST", "Java,JAVA"})
void toUpperCase_ShouldGenerateTheExpectedUppercaseValue(String input, String expected) {
String actualValue = input.toUpperCase();
assertEquals(expected, actualValue);
}
보면 쉼표로 구분된 값의 배열을 허용해주고, 각 배열 항목은 CSV파일의 한줄에 해당된다
이 소스는 매번 하나의 어레이 항목을 가져와 쉼표로 분할하고 각 어레이를 별도의 매개변수로 주석이 달린 테스트 메서드에 전달한다.
기본적으로 쉼표는 열 구분 기호이지만 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이다
CSV Files
Instead of passing the CSV values inside the code, we can refer to an actual CSV file.
For example, we could use a CSV file like this:
input,expected
test,TEST
tEst,TEST
Java,JAVACopy
We can load the CSV file and ignore the header column with @CsvFileSource:
@ParameterizedTest
@CsvFileSource(resources = "/data.csv", numLinesToSkip = 1)
void toUpperCase_ShouldGenerateTheExpectedUppercaseValueCSVFile(
String input, String expected) {
String actualValue = input.toUpperCase();
assertEquals(expected, actualValue);
}
The resources attribute represents the CSV file resources on the classpath to read. And, we can pass multiple files to it.
numLinesToSkip 속성은 CSV 파일을 읽을 때 건너뛸 줄 수를 나타낸다
기본적으로 @CsvFileSource는 줄을 건너뛰지 않지만 이 기능은 일반적으로 여기에서 수행한 것처럼 헤더 줄을 건너뛸 때 유용하다.
간단한 @CsvSource 와 마찬가지로 구분 기호는 delimiter 특성으로 사용자 지정할 수 있다.
열 구분 기호 외에도 다음과 같은 기능이 있다.
- 줄 구분 기호는 lineSeparator 특성을 사용하여 사용자 지정할 수 있다. default value = new line
- 인코딩 속성을 사용하여 파일 인코딩을 사용자 정의할 수 있다 . default value = UTF-8
Method
위의 parameter 들은 단순하고 한 가지의 제한 사항을 이용했다면, 복잡한 인수를 제공하는 방식은 메소드를 Source로 사용하는 것이다
test the isBlank method with a @MethodSource:
@ParameterizedTest
@MethodSource("provideStringsForIsBlank")
void isBlank_ShouldReturnTrueForNullOrBlankStrings(String input, boolean expected) {
assertEquals(expected, Strings.isBlank(input));
}
@MethodSource 에 제공하는 이름은 기존 메서드와 일치해야 한다 <<<
provideStringsForIsBlank, a static method that returns a Stream of Arguments
Method 이름이 일치하는 메소드를 작성한다
private static Stream<Arguments> provideStringsForIsBlank() {
return Stream.of(
Arguments.of(null, true),
Arguments.of("", true),
Arguments.of(" ", true),
Arguments.of("not blank", false)
);
}
예를 들어 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은 테스트 메서드와 동일한 이름을 가진 소스 메서드를 검색한다.
경우에 따라 서로 다른 테스트 클래스 간에 인수를 공유해야한다.
이러한 경우 정규화된 이름으로 현재 클래스 외부의 소스 메서드를 참조할 수 있다.
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, "", " ");
}
}
FQN#methodName 형식을 사용하여 외부 정적 메서드를 참조할 수 있다
Custom Argument Provider
Another advanced approach to pass test arguments is to use a custom implementation of an interface called 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(" ")
);
}
}Copy
Then we can annotate our test with the @ArgumentsSource annotation to use this custom provider:
@ParameterizedTest
@ArgumentsSource(BlankStringsArgumentsProvider.class)
void isBlank_ShouldReturnTrueForNullOrBlankStringsArgProvider(String input) {
assertTrue(Strings.isBlank(input));
}Copy
Let's make the custom provider a more pleasant API to use with a custom annotation.
Custom Annotation
static 변수에서 로드하여 테스트해야할 경우가 있을 것이다
실제 JUnit5 가 제공하지는 않으나 커스텀하게 만들 수 있다
static Stream<Arguments> arguments = Stream.of(
Arguments.of(null, true), // null strings should be considered blank
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));
}
이런 코드가 있다면
일단 커스텀애노테이션 주석을 만든다
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@ArgumentsSource(VariableArgumentsProvider.class)
public @interface VariableSource {
/**
* The name of the static variable
*/
String value();
}
그리고 애노테이션 세부 정보를 사용하여 제공한다
이를 위한 Abstraction 이 두가지 있다
- 주석 세부 정보를 사용하는 AnnotationConsumer
- 테스트 인수를 제공하는 ArgumentsProvider
따라서 다음에 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;
}
}
Argument Conversion
Implicit Conversion
@CsvSource 를 사용하여 @EnumTest 중 하나를 다시 작성한 코드이다
@ParameterizedTest
@CsvSource({"APRIL", "JUNE", "SEPTEMBER", "NOVEMBER"}) // Pssing strings
void someMonths_Are30DaysLongCsv(Month month) {
final boolean isALeapYear = false;
assertEquals(30, month.length(isALeapYear));
}
JUnit 5는 String 인수를 지정된 enum 유형으로 변환하여 이와 같은 사용 사례를 지원하기 위해 JUnit Jupiter는 여러 가지 기본 제공 암시적 유형 변환기를 제공하고 있다.
변환 프로세스는 각 메소드 매개변수의 선언된 유형에 따라 다르다.
암시적 변환은 String 인스턴스를 다음과 같은 형식으로 변환할 수 있다.
- UUID
- Locale
- LocalDate, LocalTime, LocalDateTime, Year, Month, etc.
- File and Path
- URL and URI
- Enum subclasses
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);
}
}
}
@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());
}
Argument Accessor
기본적으로 매개변수화 된 테스트에 제공된 각 인수는 단일 메소드 매개변수에 해당한다
그래서 모든 전달할 인수들을 ArgumentsAccessor 인스턴스로 캡슐화하여 인덱스 및 유형별로 인수를 검색하도록 한다
Let's consider our Person class:
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() 메서드를 테스트하기 위해 firstName , middleName , lastName 및 예상되는 fullName 의 네 가지 인수를 전달할 수 있다
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 인스턴스로 캡슐화한 다음 테스트 메서드 본문에서 전달된 각 인수를 인덱스와 함께 검색한다.
접근자일 뿐 아니라 get* 메서드를 통해 유형 변환이 지원된다
- getString(index) retrieves an element at a specific index and converts it to String — the same is true for primitive types.
- get(index) simply retrieves an element at a specific index as an Object.
- get(index, type) retrieves an element at a specific index and converts it to the given type.
Argument Aggregator
ArgumentsAccessor 추상화를 직접 사용하면 테스트 코드의 가독성이나 재사용 가능성이 낮아진다(하나하나 값 다 넣어줘야하니까).
이러한 문제를 해결하기 위해 사용자 지정 및 재사용 가능한 수집기를 작성할 수 있다.
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 애노테이션을 이용해 참조한다
@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());
}
PersonAggregator는 마지막 세 인수를 가져와서 Person클래스를 인스턴스화시켜준다
Customizing Display Names
By default, the display name for a parameterized test contains an invocation index along with a String representation of all passed arguments
├─ someMonths_Are30DaysLongCsv(Month)
│ │ ├─ [1] APRIL
│ │ ├─ [2] JUNE
│ │ ├─ [3] SEPTEMBER
│ │ └─ [4] NOVEMBERCopy
However, we can customize this display via the name attribute of the @ParameterizedTest annotation:
@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));
}
April is 30 days long surely is a more readable display name:
├─ 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 longCopy
The following placeholders are available when customizing the display name:
- {index} will be replaced with the invocation index. Simply put, the invocation index for the first execution is 1, for the second is 2, and so on.
- {arguments} is a placeholder for the complete, comma-separated list of arguments.
- {0}, {1}, ... are placeholders for individual arguments.
'Web > spring' 카테고리의 다른 글
[Spring] HandlerAdapters in Spring MVC (0) | 2023.04.04 |
---|---|
[Spring Test] JUnit 5 Conditional Test Execution with Annotations (0) | 2023.04.03 |
[Spring Test] Testing in Spring Boot (0) | 2023.04.02 |
[Spring Test] Test a REST API with Java (0) | 2023.04.02 |
[Annotation] Spring Bean Annotations (0) | 2023.04.01 |