본문 바로가기
개발/Java

[번역] JUnit5 Parameterized Test 가이드

by mixxeo 2024. 10. 27.

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