본문 바로가기

우테코/Level1

[TDD] JUnit - @ParameterizedTest 공식문서 정리하기

 

문제상황

 

우테코 1주차 미션을 정리하다가 테스트 생성에 불편한 점을 느꼈다.

 

불편함을 느낀 코드는 바로 이것이다.

@DisplayName("라운드 테스트 : 예외 범위의 시도 횟수를 입력할 때")
    void validateRound_invalidateInputs() {
        // given
        int[] invalidateInputs = {0, -1, 101, 102};

        // when - then
        for (int invalidateInput : invalidateInputs) {
            assertThrows(IllegalArgumentException.class,
                    () -> RoundValidator.validateRound(invalidateInput));
        }
    }
}

 

라운드를 1-100회 사이로 받아야 할 때,  유효범위 바깥의 비정상 입력 케이스를 테스트하고 있다.

 

불편함을 느낀 지점을 명확히 해보자면 다음 사안들이다.

1. 반복되는 코드를 for문으로 돌리고 있다
2. 직접 유효하지 않은 케이스를  테스트 내에서 생성해주어야 한다.

 

 

만약, test 코드도 인수를 받을 수 있다면, 이 코드가 상당량 줄어들지 않을까

예를 들어 다음처럼 말이다.

 

@DisplayName("라운드 테스트 : 예외 범위의 시도 횟수를 입력할 때")
    void validateRound_invalidateInputs(invalidateInput) {
            assertThrows(IllegalArgumentException.class,
                    () -> RoundValidator.validateRound(invalidateInput));
        }
    }
}

 

이걸 가능하게 해주는게 바로 ParameterizedTest이다.

 


reference)

- https://lannstark.tistory.com/52

-https://junit.org/junit5/docs/current/user-guide/#writing-tests-parameterized-tests

 

>>목차

@ParameterizedTest란?

Source의 종류
- @ValueSource
- @NullAndEmptySource
- @EnumSource
- @MethodSource
- @CsvSource
- @ArgumentSource

[참고]
- 팩토리 메서드 : String to Object
- ArgumentAccessor
- 각 테스트의 이름 짓기

 

 


@ParameterizedTest란?

- 매개변수를 통해 반복되는 코드를 실행할 수 있게 해주는 JUnit의 test기능

- @Test 대신 @ParamㄷtalizedTest를 붙여준다.

- @ValueSource에서 매개변수에 넣어줄 source들을 넣어주면 된다.

 

@ParameterizedTest
@ValueSource(strings = { "racecar", "radar", "able was I ere I saw elba" }) //테스트 케이스
void palindromes(String candidate) {
    assertTrue(StringUtils.isPalindrome(candidate));
}

 

예를 들어 다음 test에서는 candidate라는 매개변수에

ValueSource에 명시되어 있는 strings 배열인

racecar -> radar > able was I ere I saw elba" 가 차례대로 들어간다.

 

@ParamitalizedTest의 좋은 점 중 하나는 각 Parameter  별로 test 결과를 알 수 있다.

 


Source의 종류

 

요약

@ValueSource 매개변수를 하나인 test에 값을 하나씩 전달한다
@NullAndEmptySource Null값과 Empty값을 전달한다
@EnumSource Enum 상수를 하나씩 전달한다
@MethodSource Method의 반환값을 전달한다 (static)
Stream.of(arguments( arg1, arg2, arg3), arguments(...))를 통해 다중 인자 전달 가능
@CsvSource delimiter로 분리된 인자들을 전달한다
@ArugmentSource ArgumentProvider를 구현한 클래스에서 
public Stream<?  extends Argument>provideArugments(ExtensionContext)
의 반환값을 전달한다

 

@ValueSource

  • 매개변수가 하나인 test에 사용
  • 지원 자료형 : short, byte, int, long, float, double, char, boolean, java.lang.String, java.lang.Class

ex)

@ParameterizedTest
@ValueSource(ints = { 1, 2, 3 })
void testWithValueSource(int argument) {
    assertTrue(argument > 0 && argument < 4);
}

 

- argument에 순차적으로 1,2,3이 들어간다.

- 0보다 크고 4보다 작은지 검사한다.

 

Null and Empty Source

@NullSource 파라미터에 null을 전달한다.
@EmptySource 파라미터에 빈값을 전달한다
@NullAndEmptySource 파라미터에 null과 빈값을 전달한다

- @ValueSource와 함께 쓰일 수 있다.

 

ex)

@ParameterizedTest
@NullSource
@EmptySource
@ValueSource(strings = { " ", "   ", "\t", "\n" })
void nullEmptyAndBlankStrings(String text) {
    assertTrue(text == null || text.trim().isEmpty());
}

- 순차적으로 null > " > strings 배열이 들어간다

 

@EnumSource

  • Enum상수들을 순차적으로 받아온다
  • @EnumSource(Enum.class)와 같이 사용한다
  • @EnumSource안에 매개변수를 주지 않고 인수로 넘겨주어도 된다

ex)

/*
TemporalUnit (상위 인터페이스)
       |
    ChronoUnit (구현체)

*/

@ParameterizedTest
@EnumSource(ChronoUnit.class)
void testWithEnumSource(TemporalUnit unit) {
    assertNotNull(unit);
}


//@EnumSource안에 Enum을 넣지 않아도 된다
@ParameterizedTest
@EnumSource
void testWithEnumSourceWithAutoDetection(ChronoUnit unit) {
    assertNotNull(unit);
}

// names 옵션을 통해 Enum 상수 이름을 지정할 수 있다
@ParameterizedTest
@EnumSource(names = { "DAYS", "HOURS" })
void testWithEnumSourceInclude(ChronoUnit unit) {
    assertTrue(EnumSet.of(ChronoUnit.DAYS, ChronoUnit.HOURS).contains(unit));
}

//mode 옵션을 통해 Enum 상수 이름을 기준으로 필터링할 수 있다
//EXCLUDE -> name을 가진 Enum 상수 제외
@ParameterizedTest
@EnumSource(mode = EXCLUDE, names = { "ERAS", "FOREVER" })
void testWithEnumSourceExclude(ChronoUnit unit) {
    assertFalse(EnumSet.of(ChronoUnit.ERAS, ChronoUnit.FOREVER).contains(unit));
}

//EXCLUDE -> name이 정규식을 만족시키는 Enum 상수만 파라미터로 넘김
@ParameterizedTest
@EnumSource(mode = MATCH_ALL, names = "^.*DAYS$")
void testWithEnumSourceRegex(ChronoUnit unit) {
    assertTrue(unit.name().endsWith("DAYS"));
}

 

@MethodSource

- 메소드의 반환값을 source로 삼는다

- 테스트 클래스 내에 있고, @TestInstance(Lifecycle.PRE_CLASS)을 붙인게 아니면 static 메소드여야 한다

- 여러 파라미터를 넣을 수 있다

- 메소드 이름이 test이름과 같다면 @MethodSource안에 지정하지 않아도 괜찮다

 

1. 한개의 파라미터 : Stream 반환

@ParameterizedTest
@MethodSource("stringProvider")
void testWithExplicitLocalMethodSource(String argument) {
    assertNotNull(argument);
}

static Stream<String> stringProvider() {
    return Stream.of("apple", "banana");
}

 

- apple > banna가 순차적으로 들어간다

 

2. 여러개를 넘길때 : arguments / Arguments.of()

@ParameterizedTest
@MethodSource("stringIntAndListProvider")
void testWithMultiArgMethodSource(String str, int num, List<String> list) {
    assertEquals(5, str.length());
    assertTrue(num >=1 && num <=2);
    assertEquals(2, list.size());
}

static Stream<Arguments> stringIntAndListProvider() {
    return Stream.of(
        arguments("apple", 1, Arrays.asList("a", "b")),
        arguments("lemon", 2, Arrays.asList("x", "y"))
    );
}

 

=> 사용자 정의 객체도 다음 방법을 통해 받아 올 수 있다.

 

# 참고

- test와 같은 이름이라면 괄호() 안에 넣지 않아도 괜찮다

 

@ParameterizedTest
@MethodSource //이름이 같아서 넘겨주지 않아도 괜찮음
void testWithDefaultLocalMethodSource(String argument) {
    assertNotNull(argument);
}

static Stream<String> testWithDefaultLocalMethodSource() {
    return Stream.of("apple", "banana");
}

 

@CsvSource

  • 쉼표로 구분된 값( Comma-Separated Values)을 파라미터로 받는다
  • delimiterString을 통해 구분자를 바꿀 수 있다. (default는 ,)
  • ignoreLeadingAndTrailingWhitespace : 앞뒤로의 공백 무시 옵션

출처 :&nbsp;https://junit.org/junit5/docs/current/user-guide/#writing-tests-parameterized-tests

 

@ParameterizedTest
@CsvSource({
    "apple,         1",
    "banana,        2",
    "'lemon, lime', 0xF1",
    "strawberry,    700_000"
})
void testWithCsvSource(String fruit, int rank) {
    assertNotNull(fruit);
    assertNotEquals(0, rank);
}

 

@ArgumentSource

  • ArgumentsProvider를 구현한 클래스를 파라미터로 받는다.
  • 클래스 내에는 public <Stream ? extends Arguments> provideArugments(ExtensionContext)이 구현되어 있ㅇ야 한다.
  •  
@ParameterizedTest
@ArgumentsSource(MyArgumentsProvider.class)
void testWithArgumentsSource(String argument) {
    assertNotNull(argument);
}
public class MyArgumentsProvider implements ArgumentsProvider {

    @Override
    public Stream<? extends Arguments> provideArguments(ExtensionContext context) {
        return Stream.of("apple", "banana").map(Arguments::of);
    }
}

 

팩토리 메서드를 활용한 String-to-Object 형변환

- 파라미터로 사용자 정의 객체가 주어지면 팩토리 생성자가 호출된다.

 

예를 들어 다음 Book 객체가 있다고 생각해보자

public class Book {

    private final String title;

    private Book(String title) {
        this.title = title;
    }

    public static Book fromTitle(String title) {
        return new Book(title);
    }

    public String getTitle() {
        return this.title;
    }
}

 

여기서 String이 넘거지면 자동으로 Book.fromTitle(String) 팩토리 메서드가 호출된다.

@ParameterizedTest
@ValueSource(strings = "42 Cats")
void testWithImplicitFallbackArgumentConversion(Book book) {
    assertEquals("42 Cats", book.getTitle());
}

 

ArgumentAccessor

  • 파라미터 원시 데이터에 접근할 수 있게 된다
  • accessor.getString(index) : index에 있는 파라미터를 문자로 가져온다
  • accessor.get(index, Class) : index에 있는 파라미터를 지정한 Class 타입으로 가져온다

ex)

@ParameterizedTest
@CsvSource({
    "Jane, Doe, F, 1990-05-20",
    "John, Doe, M, 1990-10-22"
})
void testWithArgumentsAccessor(ArgumentsAccessor arguments) {
    Person person = new Person(arguments.getString(0),
                               arguments.getString(1),
                               arguments.get(2, Gender.class),
                               arguments.get(3, LocalDate.class));

    if (person.getFirstName().equals("Jane")) {
        assertEquals(Gender.F, person.getGender());
    }
    else {
        assertEquals(Gender.M, person.getGender());
    }
    assertEquals("Doe", person.getLastName());
    assertEquals(1990, person.getDateOfBirth().getYear());
}

각 테스트의 이름 짓기(Customizing Display Names)

  • Display name의 형식을 정할 수 있는 기능

ex)

@DisplayName("Display name of container")
@ParameterizedTest(name = "{index} ==> the rank of ''{0}'' is {1}")
@CsvSource({ "apple, 1", "banana, 2", "'lemon, lime', 3" })
void testWithCustomDisplayNames(String fruit, int rank) {
}

 

여기서 ParameterizedTest의 괄호 안에 name 옵션으로 정하고 이름을 정하고 있다.

name= {index} ==> the rank of ''{0}'' is {1}''

 

파이썬을 할 줄 아는 사람들은 쉽게 이해할 수 있을 텐데 문자열 formatting 과 비슷한 원리이다.

실제로 이 test를 실행해보면 다음 결과가 나온다.

Display name of container ✔
├─ 1 ==> the rank of 'apple' is 1 ✔
├─ 2 ==> the rank of 'banana' is 2 ✔
└─ 3 ==> the rank of 'lemon, lime' is 3 ✔

 

즉 index는 1부터 시작하는 순서를, 0과 1은 각각 파라미터 index를 의미한다

 

다른 옵션들도 존재한다.

출처 :&nbsp;https://junit.org/junit5/docs/current/user-guide/#writing-tests-parameterized-tests