본문 바로가기

JUnit 5 Parameterized Tests 사용하기

혹시 테스트 코드를 작성할 때 아래와 같은 중복되는 코드를 작성하고 계신가요?

@Test
@DisplayName("User 생성 name 2자 미만 예외처리")
void createUserException01() {
      IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () -> new User(VALID_EMAIL, "q", password));
      assertThat(e.getMessage()).isEqualTo(NAME_NOT_MATCH_MESSAGE);
}

@Test
@DisplayName("User 생성 name 10자 초과 예외처리")
void createUserException02() {
      IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () -> new User(VALID_EMAIL, "qwerasdfzxcv", password));
      assertThat(e.getMessage()).isEqualTo(NAME_NOT_MATCH_MESSAGE);
}

@Test
@DisplayName("User 생성 name 숫자 포함 예외처리")
void createUserException03() {
      IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () -> new User(VALID_EMAIL, "qq23", password));
      assertThat(e.getMessage()).isEqualTo(NAME_NOT_MATCH_MESSAGE);
}

중복되는 부분이 많은데 이렇게 여러 개의 테스트 메소드를 만드는 것은 관리하기도 가독성에도 좋지 않습니다. 물론 템플릿으로 빼는 방법도 있겠지만 오늘은 JUnit5에서 제공하는 @Parameterized를 사용해서 해결하는 방법을 소개해드리겠습니다.

일단 먼저 사용한 결과물을 보여드리겠습니다.

@ParameterizedTest
@ValueSource(strings = {"q", "qwerasdfzxcv", "qq23"})
void createUserException(String name){
      IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () -> new User(VALID_EMAIL, name, password));
      assertThat(e.getMessage()).isEqualTo(NAME_NOT_MATCH_MESSAGE);
}

@ParameterizedTest 를 사용하면 하나의 테스트 메소드로 여러 개의 파라미터에 대해서 테스트할 수 있습니다. 훨씬 깔끔하지 않나요? 그러면 사용 방법에 대해서 보겠습니다.

사용 방법

1. 의존성 추가

Gradle

testImplementation 'org.junit.jupiter:junit-jupiter-params:5.4.2'

Maven

<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-params</artifactId>
    <version>5.4.2</version>
    <scope>test</scope>
</dependency>

2. @ParameterizedTest 사용

2.1 @ValueSource

class UserTest {
    ...
    @ParameterizedTest
    @ValueSource(strings = {"q", "qwerasdfzxcv", "qq23"})
    void createUserException(String name){
        IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () -> new User(VALID_EMAIL, name, password));
        assertThat(e.getMessage()).isEqualTo(NAME_NOT_MATCH_MESSAGE);
    }
}

@ValueSource는 리터럴 값의 단일 배열을 지정할 수 있으며 매개 변수화 된 테스트 호출마다 단일 인수를 제공하는 데만 사용할 수 있습니다.
@ValueSource는 다음 유형의 리터럴 값을 지원합니다.

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

@ValueSource 안에 ints, strings 등 원하는 타입을 적어준 뒤 리터럴 값을 넣어주면 됩니다.

int 사용 예

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

2.2 Null and Empty Sources

@NullSource @EmptySource를 사용하면 파라미터 값으로 null과 empty를 넣어줍니다.

@ParameterizedTest
@NullSource
@EmptySource
void nullEmptyStrings(String text) {
    assertTrue(text == null || text.trim().isEmpty());
}

@NullAndEmptySource 를 사용하면 null과 empty를 함께 제공해주며 @ValueSource 와 함께 사용이 가능합니다.

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

2.3 @EnumSource

@ParameterizedTest
@EnumSource(value = TimeUnit.class, names = { "DAYS", "HOURS" })
void testWithEnumSourceInclude(TimeUnit timeUnit) {
    assertTrue(EnumSet.of(TimeUnit.DAYS, TimeUnit.HOURS).contains(timeUnit));
}

enum도 지원합니다.

2.4 @MethodSource

지금까지 살펴본 방식은 단순하고 한 가지 파라미터만 제공합니다. 복잡한 오브젝트를 사용하여 전달하는 것은 어렵거나 불가능합니다. 보다 복잡한 인수를 제공하는 방법으로 @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 의 value에는 메소드명을 넣어줘야 합니다.
저는 @MethodSource를 User 도메인 유효성 검사할 때 유용하게 활용했습니다.

코드

@ParameterizedTest(name = "{index}: {3}")
@MethodSource("invalidParameters")
@DisplayName("User 생성 유효성 검사")
void invalidCreate(String name, String email, String password, String message, String exceptionMessage) {
    IllegalArgumentException e = assertThrows(IllegalArgumentException.class, () -> new User(email, name, password));
    assertThat(e.getMessage()).isEqualTo(exceptionMessage);
}

static Stream<Arguments> invalidParameters() throws Throwable {
	return Stream.of(
    	Arguments.of("a", VALID_EMAIL, VALID_PASSWORD, "name 2자 미만", NAME_NOT_MATCH_MESSAGE),
        Arguments.of("qwertasdfzpl", VALID_EMAIL, VALID_PASSWORD, "name 10자 초과", NAME_NOT_MATCH_MESSAGE),
        Arguments.of("1232ad", VALID_EMAIL, VALID_PASSWORD, "name 숫자 포함", NAME_NOT_MATCH_MESSAGE),
        Arguments.of(VALID_NAME, VALID_EMAIL, "Passw0rd", "password 특수문자 제외", PASSWORD_NOT_MATCH_MESSAGE),
        Arguments.of(VALID_NAME, VALID_EMAIL, "P@ssword", "password 숫자 제외", PASSWORD_NOT_MATCH_MESSAGE),
        Arguments.of(VALID_NAME, VALID_EMAIL, "p@ssw0rd", "password 대문자 제외", PASSWORD_NOT_MATCH_MESSAGE),
        Arguments.of(VALID_NAME, VALID_EMAIL, "P@SSW0RD", "password 소문자 제외", PASSWORD_NOT_MATCH_MESSAGE),
        Arguments.of(VALID_NAME, "asdfasd", VALID_PASSWORD, "email 양식 틀림", EMAIL_NOT_MATCH_MESSAGE)
    );
}

결과

스크린샷 2019-12-04 오후 11.59.26.png

@DisplayName 은 메소드의 이름이고

@ParameterizedTest 의 name은 결과에서의 이름으로 나타납니다. {index}는 1부터 순서대로 표시해주며, {3}은 파라미터에서 4번째 파라미터인 message 를 출력해줍니다. ( 0부터 시작하기 때문에 {3}은 4번째 파라미터를 실행합니다)


References