Java Junit5 @ParameterizedTest

테스트 코드나 TDD에 대해서 자세히 알고 싶어서 인프런 이규원님의 TDD 강의 중 Spring Boot TDD – 입문부터 실전까지 정확하게 를 공부하는 중 매개변수화 테스트 내용이 나왔다. 실무에서 많이 사용할 것 같아서 다시 볼 수 있게 글로 남겨보자

  • @Test를 사용하면 보통 하나에 입력값을 검사한다
  • 하지만 같은 로직을 여러 값에 대해 검사한다면 중복 코드 증가한다
  • 이 때 @ParameterizedTest를 사용하면 하나의 테스트 메스드를 다양한 값으로 자동 반복 실행해준다

Email 형식을 검증하는 Email 클래스 있다 가정

문제점: 반복되는 테스트 코드

public class EmailTest {
    @Test
    void email_at_검증 () {
        // given
        String invalidEmail = "invalid-email";
        
        // when & then
        assertThatThrownBy(() -> new Email(invalidEmail))
        .isInstanceOf(IllegalArgumentException.class);
    }

    @Test
    void email_도메인_검증 () {
        // given
        String invalidEmail = "invalid-email@";
        
        // when & then
        assertThatThrownBy(() -> new Email(invalidEmail))
        .isInstanceOf(IllegalArgumentException.class);
    }

    // for문을 활용한 테스트
    @Test
    void email_형식_검증 () {
        // given
        String[] invalidEmails = {
            "invalid-email",
            "invalid-email@",
            "invalid-email@test"
        }
        
        // when & then
        for (String invalidEmail: invalidEmails) {
            assertThatThrownBy(() -> new Email(invalidEmail))
            .isInstanceOf(IllegalArgumentException.class);
        }
    }
}

해결책: @ParameterizedTest

  • 하나의 테스트 메서드를 다양한 값으로 자동 반복 실행해준다

@ValueSource – 단일 값 배열

@ParameterizedTest
@ValueSource(strings = {
    "invalid-email",
    "invalid-email@",
    "invalid-email@test",
    "invalid-email@test.",
    "invalid-email@.com.",
})
void email_형식_검증 (String invalidEmail) {
    // when & then
    assertThatThrownBy(() -> new Email(email))
    .isInstanceOf(IllegalArgumentException.class);
}
  • 사용시기: 단일 타입의 간단한 값들을 테스트 할 때
  • 장점: 가장 간단하고 직관적
  • 단점: 문자열, 숫자 등 기본 타입만 지원, 복잡한 데이터 불가능

@ValueSource가 지원하는 타입들

@ValueSource(ints = {1, 2, 3})
@ValueSource(longs = {100L, 200L, 300L})
@ValueSource(doubles = {1.5, 2.5, 3.5})
@ValueSource(booleans = {true, false})

@MethodSource – 외부 메서드에서 데이터 제공

  • 경로 설정은 package.class#method로 한다
|test
|__java
|__|__com
|__|__|__commerce
|__|__|__|__TestData.class

public class TestData {
    public static String[] invalidEmails() {
        return new String[] {
            null,
            "invalid-email",
            "invalid-email@",
            "invalid-email@test",
            "invalid-email@test.",
            "invalid-email@.com.",
        };
    }
}

@ParameterizedTest
@MethodSource("com.commerce.TestData#invalidEmails")
void email_형식_검증 (String invalidEmail) {
    // when & then
    assertThatThrownBy(() -> new Email(email))
    .isInstanceOf(IllegalArgumentException.class);
}
  • 사용시기: 동일한 데이터셋을 여러 테스트에서 반복 사용할 때
  • 장점
    • 복잡한 객체나 여러 매개변수 지원
    • 데이터 중앙화로 재사용성 높음
    • null 값도 포함 가능
  • 단점
    • 별도 메서드 정의 필요
    • 문자열 기반 메서드 참조의 취약성 (오타 가능)
    • 런타임 의존적 메서드 바인딩 (실행해봐야 에러 발견)

커스텀 어노테이션 – 의미있는 추상화

import org.junit.jupiter.params.provider.MethodSource;

import java.lang.annotation.Retention;

import static java.lang.annotation.RetentionPolicy.RUNTIME;

// 어노테이션 정의
@Retention(RUNTIME) // 어노테이션이 코드가 실행 될 때도 유지되게 Retention을 RUNTIME 설정
@MethodSource("tdd.commerce.TestDataSource#invalidEmails")
public @interface InvalidEmailSource {

}

@ParameterizedTest
@InvalidEmailSource // 의미 명확
void email_형식_검증 (String invalidEmail) {
    // when & then
    assertThatThrownBy(() -> new Email(email))
    .isInstanceOf(IllegalArgumentException.class);
}
  • 사용 시기: 동일한 데이터셋을 여러 테스트에서 반복 사용할 때
  • 장점
    • 의미 전달: @InvalidEmailSource만 봐도 목적 파악 가능
    • 재사용성: 여러 테스트 클래스에서 일관되게 사용 가능
    • 유지보수: 데이터 변경 시 한 곳만 수정
  • 단점: 초기 설정 비용 (어노테이션 정의 필요)

@EnumSource

private enum MemberType {
    BASIC, VIP, PREMIUM
}

@ParameterizedTest
@EnumSource(value = MemberType.class)
void member_enum_type (MemberType memberType) {
    System.out.println("memberType = " + memberType);
    // memberType = BASIC
    // memberType = VIP
    // memberType = PREMIUM

    // 모든 Enum 값에 대한 실행
}

@ParameterizedTest
@EnumSource(MemberType.class)
@EnumSource(value = MemberType.class, names = {"BASIC", "VIP"})
    System.out.println("memberType = " + memberType);
    // memberType = BASIC
    // memberType = VIP

    // BASIC, VPI만 테스트
}

아직 실무에서 사용한 적 없는 여러 Source들

@CsvSources – 여러 매개변수

@CsvFIleSource – 외부 파일

@ParameterizedTest의 이점

  • 코드 중복 제거: 동일한 로직을 여러 값에 대해 간결하게 테스트 가능
  • 명확할 실패 정보: 각 파라미터별로 개별 괄가 제공
  • 유지보수성: 테스트 데이터만 수정하면 됨
  • 가독성: 테스트 의도가 명확히 드러남

@ParameterizedTest는 동일한 로직을 여러 값에 대해 검증할 떄 코드 중복을 없애고 테스트 품질을 높일 수 있다

출처 – Spring Boot TDD – 입문부터 실전까지 정확하게