JUnit5 Parameterized Test 가이드

yesjuhee·2024년 10월 26일

Java 공부

목록 보기
13/17

아래 사이트에 게시된 글을 정리/번역해 작성한 글입니다. 작성 중에 생략된 내용이 있을 수 있습니다.
https://www.baeldung.com/parameterized-tests-junit-5

1. Overview

JUnit5의 새로운 기능 중 하나가 바로 Parameterized test이다. 이 기능은 하나의 테스트 메서드를 다른 파라미터들로 여러번 실행시킬 수 있다.

2. Dependencies

testCompile("org.junit.jupiter:junit-jupiter-params:5.10.0")

3. First Impression

아주 간단한 예시를 통해 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를 실행시킬 수 있다.
  • @ValueSourceints 배열은 6개의 숫자를 가지고 있다. 테스트 함수가 6번 실행되면서 각각의 값을 number 파라미터로 넘겨준다.

4. Argument Sources

Parameterized test는 다른 인자를 전달하면서 같은 테스트를 여러개 실행시킨다. 어떤 인자 값들을 전달할 수 있는지 알아보자.

4.1 Simple Values

@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는 아래의 값들을 지원한다.

  • short
  • byte
  • int
  • long
  • foat
  • double
  • char
  • java.lang.String
  • java.lang.Class

또한, @ValueSource는 테스트 메서드에 한번에 하나의 인자만 전달할 수 있다. 그리고 @ValueSource에는 null값을 이용할 수 없다는 것 또한 알아두자.

4.2 Null and Empty Values

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));
}

아래와 같이 @NullAndEmptySourcenull 과 빈 값을 모두 넘겨줄 수 있다.

@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));
}

4.3 Enum

@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));
}

modeEXCLUDE로 설정할 경우 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));
}

4.4 CSV Literals

@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);
}

4.5 CSV Files

실제로 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이다.

4.6 Method

보다 복잡한 값을 넘겨주기 위해 @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, "", "  ");
    }
}

4.7 Field

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());
}

4.8 Custom Argument Provider

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));
}

4.9 Custom Annotation

https://www.baeldung.com/parameterized-tests-junit-5#9-custom-annotation

5. Repeatable Argument Source Annotations

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*

6. Argument Conversion

Argument converter는 primitive argument를 더 복잡한 데이터 구조에 매핑할 수 있도록 한다.

6.1 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로 값을 넘겨주고 있는데 테스트 메서드의 파라미터는 Enum 값을 받고 있다. 오류가 발생할 것같지만 위의 코드는 정상적으로 작동한다.

JUnit5는 built-in implicit type converter를 통해 암시적 형변환을 제공한다. String 인스턴스를 다음과 같은 타입으로 변환할 수 있다.

  • UUID
  • Locale
  • LocalDateLocalTimeLocalDateTimeYearMonth, etc.
  • File and Path
  • URL and URI
  • Enum subclasses

6.2 Explicit Conversion

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());
}

7. Argument Accessor

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());
}

8. Argument Aggregator

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());
}

9. Customizing Display Names

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 커스터마이징을 위해 사용할 수 있는 형식은 다음과 같다.

  • {displayName} will be replaced with the display name of the method. In case the @Display annotation is provided, then the value is provided.
  • {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’ values.
  • {argumentsWithName} is a placeholder for named argument. These arguments are built with the structurearguments(named(NAME, ARG), …). This prints the given name and the actual parameter name.
  • {argumentSetName} is a placeholder for the first parameter (name of the set) provided in the factory method argumentSet.
  • {argumentSetNameOrArgumentsWithName} is a placeholder for the first parameter provided in the factory method argumentSet.
  • {0}, {1}, … are placeholders for individual arguments.

https://www.baeldung.com/parameterized-tests-junit-5#display-name

10. Conclusion

이 글에서는 JUnit 5에서 매개변수화된 테스트의 핵심을 살펴보았다.

매개변수화된 테스트는 두 가지 측면에서 일반 테스트와 다르다는 것을 배웠다. @ParameterizedTest로 주석을 달고, 선언된 인수의 소스가 필요하다.

또한 JUnit이 인수를 사용자 정의 대상 유형으로 변환하거나 테스트 이름을 사용자 정의하는 몇 가지 기능을 제공한다.

profile
https://yesjuhee.tistory.com/

0개의 댓글