테스트 코드나 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는 동일한 로직을 여러 값에 대해 검증할 떄 코드 중복을 없애고 테스트 품질을 높일 수 있다