728x90
최근 클린코드 8기 과정을 들으면서 필요하다고 생각했던 내용을 번역한 글입니다
@ParameterizedTest
- 메소드는 private, static이 되어서는 안된다
- 아래 목록 중 하나는 꼭 있어야 한다
- ArgumentProvider
- @ValueSource
- @CsvSource
- provider는
Arguments
의Stream
을 제공해야 한다 - DEFAULT_DISPLAY_NAME
"[{index}] {arguments}"
{0}
: 0번째 argument
정리
@ParameterizedTest
@ValuSource
@NullSource
@EmptySource
@NullAndEmptySource
@EnumSource(Month.class)
@EnumSource(
value = Month.class,
names= {"APRIL",..},
mode = EnumSource.Mode.EXCLUDE
)
@CsvSource({"input,expected"})
@CsvSource(
value = {"test:test", "tEst:test", "Java:java"},
delimiter = ':'
)
private Stream<Arguments> provide() {
return Stream.of(Argument.of(),...)
@ParameterizedTest
@ValueSource(ints = {1, 3, 5, -3, 15, Integer.MAX_VALUE})
method(int number) { }
- 하나의 테스트를 다른 parameter로 여러번 실행 가능
Argument Sources
같은 테스트를 다른 argument로 여러번 실행
1. Simple Values
- short (with the shorts attribute)
- byte (with the bytes attribute)
- int (with the ints attribute)
- long (with the longs attribute)
- float (with the floats attribute)
- double (with the doubles attribute)
- char (with the chars attribute)
- java.lang.String* (with the *strings attribute)
- java.lang.Class (with the classes attribute)
2. Null & Empty Values
@EmptySource
@NullSource
@NullAndEmptySource
@EmptySource
- Strings
- Collections
- arrays
@ParameterizedTest
@NullAndEmptySource
@ValueSource(strings = {" ", "\t", "\n"})
void isBlank_ShouldReturnTrueForAllTypesOfBlankStrings(String input) {
assertTrue(Strings.isBlank(input));
}
Enum
@EnumSource(Month.class)
@EnumSource(value = Month.class, names = {"APRIL", "JUNE", "SEPTEMBER", "NOVEMBER"})
@EnumSource(value = Month.class,
names = {"APRIL", "JUNE", "SEPTEMBER", "NOVEMBER", "FEBRUARY"},
mode = EnumSource.Mode.EXCLUDE)
@ParameterizedTest
@EnumSource(Month.class) // passing all 12 months
void getValueForAMonth_IsAlwaysBetweenOneAndTwelve(Month month) {
int monthNumber = month.getValue();
assertThat(monthNumber >= 1 && monthNumber <= 12);
}
@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));
}
EnumSource.Mode.EXCLUDE
- names에 해당하는 아이템을 Enum에서 지외
@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));
}
EnumSet
- Enum들의 Set
EnumSource.Mode.MATCH_ANY
- name에 Regex가 있을 때
CSV Literals
아래 시나리오의 경우
- Input 값과 expected 값을 주입
- 위의 input값으로 actual 결과를 계산
- actual 값을 expected 값과 비교
@ParameterizedTest
@CsvSource({"test,TEST", "tEst,TEST", "Java,JAVA"})
void toUpperCase_ShouldGenerateTheExpectedUppercaseValue(String input, String expected) {
String actualValue = input.toUpperCase();
assertEquals(expected, actualValue);
}
@ParameterizedTest
@CsvSource(value = {"test:test", "tEst:test", "Java:java"}, delimiter = ':')
void toLowerCase_ShouldGenerateTheExpectedLowercaseValue(String input, String expected) {
String actualValue = input.toLowerCase();
assertEquals(expected, actualValue);
}
Method
method를 argument source로 사용할 경우
- method 재활용 할 때 좋겠다
@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)
);
}
@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, "", " ");
}
}
외부 클래스의 메소드를 가져올 수도 있다
- 패키지.클래스#메소드
Custom Argument Provider
ArgumentsProvider 인터페이스를 상속받은 Custom 클래스의 arguments를 사용
class BlankStringsArgumentsProvider implements ArgumentsProvider {
@Override
public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
return Stream.of(
Arguments.of((String) null),
Arguments.of(""),
Arguments.of(" ")
);
}
}
@ParameterizedTest
@ArgumentsSource(BlankStringsArgumentsProvider.class)
void isBlank_ShouldReturnTrueForNullOrBlankStringsArgProvider(String input) {
assertTrue(Strings.isBlank(input));
}
🔥Custom Annotation
- static 변수에서 test argument 제공
- 하지만, 실제로 Junit 5는 제공하지 않는다
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));
}
하지만, 아래와 같은 방법을 사용하면 가능하다
- Annotation 생성
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@ArgumentsSource(VariableArgumentsProvider.class)
public @interface VariableSource {
/**
* The name of the static variable
*/
String value();
}
Then we need to somehow consume the annotation details and provide test argument
- JUnit5는 2개의 abstraction을 제공한다
- AnnotationConsumer — consume the annotation detail
- ArgumentProvider — provide test argument
- VariableArgumentsProvider 생성
we next need to make the VariableArgumentsProvider class read from the specified static variable and return its value as test arguments:
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;
}
}
5. Argument Conversion
5-1. Implicit Conversion
@ParameterizedTest
@CsvSource({"APRIL", "JUNE", "SEPTEMBER", "NOVEMBER"}) // Pssing strings
void someMonths_Are30DaysLongCsv(Month month) {
final boolean isALeapYear = false;
assertEquals(30, month.length(isALeapYear));
}
- UUID
- Locale
- LocalDate, LocalTime, LocalDateTime, Year, Month, etc.
- File and Path
- URL and URI
- Enum subclasses
5-1. Explicit Conversion
- *"yyyy/mm/dd"* → LocalDate
- ArgumentConverter interface
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);
}
}
}
@ParameterizedTest
@CsvSource({"2018/12/25,2018", "2019/02/11,2019"})
void getYear_ShouldWorkAsExpected(
@ConvertWith(SlashyDateConverter.class) LocalDate date, int expected) {
assertEquals(expected, date.getYear());
}
6. Argument Accessor
- 기본적으로 각 arugmente들은 parameterized test에 제공된다
- 순차적으로 argument source를 통해 argument가 제공된다
- 이 때, test method는 매우 더러워 진다
- 해결법은 모든 argument를 캡슐화하여 ArgumentsAccessor에 제공한다
- 그리고 argument를 index와 type으로 반환한다
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() 을 테스트하기 위해 , 4개의 argument를 전달한다
- firstName
- middleName
- lastName
- expected fullName
- test argument를 반환받기 위해 ArgumentsAccessor를 사용한다
- 이제 method parameter로 선언하지 않아도 된다
@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());
}
- 모든 arguments를 ArgumentsAccessor로 캡슐화 하였다
- 테스트 메소드에서 각 argument들을 index와 함께 반환한다
- 타입 conversion은 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
7. Argument Aggregator
- ArgumentsAccessor abstraction을 직접 사용하는 것은 테스트 코드를 재사용과 가독성을 저해 할 수도 있다
- 이 문제를 해결하기 위해, custom 그리고 재사용 가능한 aggregator를 만들 수 있다
- 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));
}
}
- @AggregationWith으로 참조한다
@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는 마지막 3개의 argument들을 받는다
- 3개의 argument로 Person 인스턴스를 생성한다
8. Customizing Display Names
- {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
728x90