Test

[JUnit] @ParameterizedTest 로 경계값 테스트하기

KAispread 2023. 8. 13. 15:00
728x90
반응형

개요

특정 조건에 따라 분기하는 기능이 있을 때, 해당 조건에 대한 경계값 테스트를 작성하여 테스트의 신뢰도를 높일 수 있습니다. 경계값 테스트란, 어떤 조건의 경계에 해당하는 값을 테스트하여 특정 조건일 때 기능이 원하는 대로 동작하는지 확인하기 위해 작성하는 테스트입니다. 예를 들어 어떤 물건을 한 번에 주문할 수 있는 수량이 2개라고 했을 때, 2개를 주문하면 주문에 성공하고 3개를 주문하면 주문에 실패하는 테스트를 작성할 수 있습니다.

이와 같이, 테스트를 작성하다보면 동일한 검증 코드에서 여러 값에 대해 테스트를 수행해야 할 경우가 생깁니다. 이번 포스팅에서는 다음의  문자열 검증 유틸 클래스를 통해 @ParameterizedTest에 대해서 설명해보겠습니다.

public abstract class CustomFormatValidator {

    private CustomFormatValidator() {
        throw new AssertionError();
    }

    private static final Pattern NICKNAME_PATTERN = Pattern.compile("^[가-힣\\s]{2,10}$");

    public static boolean validateNicknameFormat(final String nickname) {
        return NICKNAME_PATTERN.matcher(nickname).matches();
    }
}

본 유틸 클래스의 validateNicknameFormat() 은 닉네임의 형식을 검증합니다. 주어진 닉네임이 2글자 이상 10글자 이하의 한글 텍스트인지 판별하고 형식에 맞다면 true, 형식에 어긋난다면 false를 반환합니다.

 

 @ParameterizedTest 없이 테스트 코드를 작성한다면 다음과 같이 작성하게 됩니다.

class CustomFormatValidatorTest {

    @DisplayName("주어진 닉네임이 2글자 한글 닉네임이면 true를 반환한다.")
    @Test
    void nicknameLength2AndKorean() {
        // given
        final String nickname = "철수";

        // when
        boolean result = CustomFormatValidator.validateNicknameFormat(nickname);

        // then
        assertThat(result).isTrue();
    }
    
    @DisplayName("주어진 닉네임이 띄어 쓰기가 포함된 10글자 한글 닉네임이면 true를 반환한다.")
    @Test
    void nicknameLength10ContainsWhiteSpaceAndKorean() {
        // given
        final String nickname = "김 철 수 철 수 ";

        // when
        boolean result = CustomFormatValidator.validateNicknameFormat(nickname);

        // then
        assertThat(result).isTrue();
    }
    
    // ... true 를 반환하는 여러개의 테스트
    
    @DisplayName("주어진 닉네임이 11글자 한글 닉네임이면 false를 반환한다.")
    @Test
    void nicknameLength11AndKorean() {
        // given
        final String nickname = "열한글자닉네임추천좀요";

        // when
        boolean result = CustomFormatValidator.validateNicknameFormat(nickname);

        // then
        assertThat(result).isFalse();
    }

    @DisplayName("주어진 닉네임이 3글자 영어면 false를 반환한다.")
    @Test
    void nicknameLength3AndEnglish() {
        // given
        final String nickname = "Kai";

        // when
        boolean result = CustomFormatValidator.validateNicknameFormat(nickname);

        // then
        assertThat(result).isFalse();
    }
  }

같은 응답 값에 대해 검증하고자 하는 메서드는 하나인데, 여러 개의 테스트를 작성할 수밖에 없습니다. 물론, 하나의 테스트에 여러 개의 Assertion 구문을 넣어 테스트할 수도 있지만, 코드의 길이가 길어질뿐더러 어떤 검증을 위한 테스트인지 명확해지지 않을 수 있습니다.

이와 같은 문제를 @ParameterizedTest로 해결할 수 있습니다.

 


 

@ParameterizedTest

JUnit5에서 제공하는 @ParameterizedTest는 하나의 테스트에서 여러 인수에 대한 검증을 수행하는 검증 어노테이션입니다. 기본 @Test 대신, @ParameterizedTest 어노테이션을 붙여주면 되며, 테스트에 사용되는 값을 Source로 주입시켜주어야 합니다.

 

♦️ Source

Source는 @ParameterizedTest에 사용될 값을 넣어주는 객체입니다. JUnit에서 여러 개의 Source 어노테이션을 제공하며, 본 포스팅에서는 4가지 Source에 대해서만 설명하겠습니다.

 

ValueSource

ValueSource는 가장 기본적인 Source 어노테이션으로, 한 번의 테스트에 하나의 인수를 넣어줄 수 있습니다.

ValueSource를 사용하여 위 테스트 코드를 리팩토링 해보겠습니다.

class CustomFormatValidatorTest {

    @DisplayName("주어진 닉네임이 형식에 맞으면 true를 반환한다.")
    @ValueSource(strings = {"김 철 수 철 수 ", "철수", "철 수 짱", "열글자닉네임추천좀요"})
    @ParameterizedTest
    void validateNicknameFormatWithValidString(String nickname) {
        // when
        boolean result = CustomFormatValidator.validateNicknameFormat(nickname);

        // then
        assertThat(result).isTrue();
    }
    
    @DisplayName("주어진 닉네임이 형식에 맞지 않으면 false를 반환한다.")
    @ValueSource(strings = {"김", "K", "kai", "띄 어 쓰 기 포 함", "열한글자닉네임추천좀요"})
    @ParameterizedTest
    void validateNicknameFormatWithInvalidString(String nickname) {
        // when
        boolean result = CustomFormatValidator.validateNicknameFormat(nickname);

        // then
        assertThat(result).isFalse();
    }
    
 }

다음과 같이, 하나의 테스트 코드에서 여러 값에 대한 테스트를 수행할 수 있습니다. @ValueSource에 정의된 값들이 메서드의 파라미터로 주입되어 한 번씩 테스트가 수행됩니다.

ValueSource에 넣어줄 수 있는 값에 대한 type은 다음과 같습니다.

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

 

위 테스트 코드를 실행시키면 다음과 같이 2개의 테스트 코드에서 실질적으로 9번의 테스트가 발생한 것을 확인할 수 있습니다. 

 

 

@CsvSource

@ValueSource를 사용하여 하나의 테스트 코드에서 여러 값에 대한 테스트를 수행할 수 있었습니다. 하지만 여전히 응답 값 (true, false)을 기대하는 2개의 테스트가 존재합니다. @CsvSource를 사용하면 하나의 테스트로 합칠 수 있습니다.

@ValueSource는 한 번의 테스트에 하나의 인수만 넣어줄 수 있는 반면, @CsvSource는 여러 인수를 넣어줄 수 있습니다.

class CustomFormatValidatorTest {

    @DisplayName("주어진 닉네임의 형식에 맞다면 true, 아니라면 false를 반환한다.")
    @CsvSource({
        "김, false",
        "K, false",
        "kai, false",
        "띄 어 쓰 기 포 함, false",
        "열한글자닉네임추천좀요, false",
        "김 철 수 철 수, true",
        "철수, true"
    })
    @ParameterizedTest
    void validateNicknameFormat(String nickname, boolean expected) {
        // when
        boolean result = CustomFormatValidator.validateNicknameFormat(nickname);

        // then
        assertThat(result).isEqualTo(expected);
    }
    
 }

다음과 같이 입력에 따라 기대하는 값이 다른 테스트를 하나의 테스트 코드에서 모두 검증할 수 있습니다. 각 테스트는 ""로 분리되고 세미콤마 ', '로 각 파라미터를 구분하여 값을 선언해 주면 됩니다. 

 

수행 결과

 

@CsvSource에 여러 인수를 넣다 보면 가독성이 떨어질 수 있습니다. 공식 문서에서는 다음과 같은 형태로도 source를 지정할 수 있다고 안내하고 있습니다.

class CustomFormatValidatorTest {

    @ParameterizedTest
    @CsvSource(delimiter = '|', quoteCharacter = '"', textBlock = """
        #-----------------------------
        #    FRUIT     |     RANK
        #-----------------------------
             apple     |      1
        #-----------------------------
             banana    |      2
        #-----------------------------
          "lemon lime" |     0xF1
        #-----------------------------
           strawberry  |    700_000
        #-----------------------------
        """
    )
    void testWithCsvSource(String fruit, int rank) {
        // ...
    }
    
 }

 

 

@MethodSource

@MethodSource는 Stream 형태의 반환 타입을 갖는 메서드에 source를 지정해 놓고 사용할 수 있도록 합니다. 다음과 같이 사용할 수 있습니다.

 class CustomFormatValidatorTest {
    
    public static Stream<Arguments> provideNicknameForValidateFormat() {
        return Stream.of(
            Arguments.of("김", false),
            Arguments.of("K", false),
            Arguments.of("kai", false),
            Arguments.of("띄 어 쓰 기 포 함", false),
            Arguments.of("열한글자닉네임추천좀요", false),
            Arguments.of("김 철 수 철 수", true),
            Arguments.of("철수", true)
        );
    }

    @DisplayName("주어진 닉네임의 형식에 맞다면 true, 아니라면 false를 반환한다.")
    @MethodSource("provideNicknameForValidateFormat")
    @ParameterizedTest
    void validateNicknameFormat(String nickname, boolean expected) {
        // when
        boolean result = CustomFormatValidator.validateNicknameFormat(nickname);

        // then
        assertThat(result).isEqualTo(expected);
    }
    
 }

Stream <Arguments>를 반환하는 메서드의 이름을 @MethodSource 의 인자로 넣어주면 됩니다.

메서드의 반환 타입은 Stream으로 변환할 수 있는 어떤 타입이든 상관없으며 (IntStream, Collection, Iterator, Iterable..), Arguments는 Object [] 형과 단일 값만 가능합니다. 단, 메서드는 반드시 static 이여야 합니다. 해당 메서드는 외부 클래스에 정의하여 사용할 수도 있습니다.

 

 

@EnumSource

@EnumSource는 말 그대로 Enum을 source에 넣어주는 어노테이션입니다. 지원하는 mode에 따라 테스트에 포함시킬 상수를 지정할 수 있습니다.

class CustomFormatValidatorTest {

    @DisplayName("모든 enum 상수를 사용한다.")
    @EnumSource
    @ParameterizedTest
    void enumSource(AcceptStatus status) {
        boolean contains = EnumSet.allOf(AcceptStatus.class).contains(status);

        assertThat(contains).isTrue();
    }

    @DisplayName("모든 enum 상수에서 특정 상수를 제외한다.")
    @EnumSource(mode = EXCLUDE, names = {"ACCEPT", "REJECT"})
    @ParameterizedTest
    void enumSourceExclude(AcceptStatus status) {
        boolean contains = EnumSet.of(AcceptStatus.EXPIRED, AcceptStatus.PENDING).contains(status);

        assertThat(contains).isTrue();
    }

    @DisplayName("특정 enum 상수만 사용한다.")
    @EnumSource(mode = INCLUDE, names = {"ACCEPT", "REJECT"})
    @ParameterizedTest
    void enumSourceInclude(AcceptStatus status) {
        boolean contains = EnumSet.of(AcceptStatus.ACCEPT, AcceptStatus.REJECT).contains(status);

        assertThat(contains).isTrue();
    }

    @DisplayName("정규식에 맞는 enum 상수만 사용한다.")
    @EnumSource(mode = MATCH_ALL, names = "^.*JECT$")
    @ParameterizedTest
    void enumSourceMatchAll(AcceptStatus status) {
        assertThat(status).isEqualTo(AcceptStatus.REJECT);
    }

}

 

 


정리

🔸 @ParameterizedTest:  하나의 테스트 코드에서 여러 값에 대한 검증 가능

🔹 @ValueSource: 한 번의 테스트에 하나의 인수 사용
🔹 @CsvSource: 한 번의 테스트에 여러 개의 인수 사용
🔹 @MethodSource: 넣고자 하는 인수를 Method로 분리할 때 사용
🔹 @EnumSource: Enum 타입의 인수를 넣고 싶을 때 사용

 

더 자세한 내용은 공식문서에서 확인하실 수 있습니다.

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

 

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

 

728x90
반응형