핵심 정리
@ParameterizedTest
@ValueSource(ints = {0, 1, 2, 10})
void isNonNegative_returnsTrue(int value) {
assertTrue(value >= 0);
}기본 흐름
어떤 테스트 반복 방식을 먼저 떠올리면 되나
| 상황 | 먼저 떠올릴 선택 |
|---|---|
| 같은 규칙을 여러 입력으로 검증 | @ParameterizedTest |
| 간단한 값 목록 전달 | @ValueSource |
| 여러 열의 입력 전달 | @CsvSource |
| 복잡한 테스트 데이터 생성 | @MethodSource |
| 테스트마다 공통 준비 | @BeforeEach |
| 클래스 전체 공통 준비 | @BeforeAll |
parameterized test: 같은 규칙을 여러 입력으로 검증
같은 검증 구조를 여러 입력에 적용해야 할 때, 테스트를 복붙하는 대신 @ParameterizedTest로 입력만 바꿔 반복 실행할 수 있습니다. 입력 표가 있는 규칙 검증, 경계값 테스트에 특히 잘 맞습니다.
// ❌ 같은 구조를 복붙 — 유지보수 어려움
@Test void isValid_withEmptyString_returnsFalse() { assertFalse(validator.isValid("")); }
@Test void isValid_withNull_returnsFalse() { assertFalse(validator.isValid(null)); }
@Test void isValid_withSpaces_returnsFalse() { assertFalse(validator.isValid(" ")); }
// ✅ @ParameterizedTest로 통합
@ParameterizedTest
@NullAndEmptySource
@ValueSource(strings = {" ", "\t", "\n"})
void isValid_withBlankInput_returnsFalse(String input) {
assertFalse(validator.isValid(input));
}@ValueSource / @CsvSource / @MethodSource
입력 데이터를 어디서 가져올지에 따라 source annotation을 고릅니다. 단순 값은 @ValueSource, 여러 컬럼이 있는 경우는 @CsvSource, 복잡한 객체나 큰 데이터는 @MethodSource를 씁니다.
// @CsvSource — 입력과 기대값을 함께 표현
@ParameterizedTest
@CsvSource({
"2, 3, 5",
"0, 0, 0",
"-1, 1, 0"
})
void add_returnsExpectedSum(int a, int b, int expected) {
assertEquals(expected, calculator.add(a, b));
}
// @MethodSource — 복잡한 테스트 데이터
@ParameterizedTest
@MethodSource("provideUsers")
void isEligible_returnsTrue(User user) { assertTrue(service.isEligible(user)); }
static Stream<User> provideUsers() {
return Stream.of(new User("admin", 30), new User("editor", 25));
}ValueSource vs CsvSource vs MethodSource
입력이 한 칸짜리 단순 값이면 @ValueSource, 입력과 기대값처럼 열이 여러 개면 @CsvSource, 객체 생성이나 조합 로직이 들어가면 @MethodSource가 보통 가장 읽기 쉽습니다.
// 단일 값
@ParameterizedTest
@ValueSource(strings = {"A", "B", "C"})
void isUppercase(String input) { ... }
// 입력 + 기대값
@ParameterizedTest
@CsvSource({"2,3,5", "3,4,7"})
void add_returnsSum(int a, int b, int expected) { ... }lifecycle: @BeforeEach / @BeforeAll
@BeforeEach는 각 테스트 메서드 전에 실행됩니다. @BeforeAll은 클래스 전체에서 한 번만 실행됩니다. DB 연결처럼 비용이 큰 초기화는 @BeforeAll로, 테스트마다 새 상태가 필요한 경우는 @BeforeEach로 분리합니다.
class UserServiceTest {
static DataSource dataSource; // 한 번만 초기화
UserService service;
@BeforeAll
static void setupDataSource() {
dataSource = createTestDataSource(); // DB 연결 — 한 번만
}
@BeforeEach
void setup() {
service = new UserService(dataSource); // 매 테스트마다 새 인스턴스
}
@AfterEach
void cleanup() {
service.clearTestData();
}
}체크포인트
| 상황 | 적합한 선택 |
|---|---|
| 같은 규칙, 여러 입력 검증 | @ParameterizedTest |
| 단순 값 목록 | @ValueSource |
| 입력-기대값 쌍 | @CsvSource |
| 복잡한 객체 데이터 | @MethodSource |
| 비용이 큰 초기화 (DB, 서버) | @BeforeAll |
| 테스트마다 새 상태 필요 | @BeforeEach |
주의할 점
입력 구조가 크게 다른 케이스를 하나의 parameterized test에 몰아넣으면 실패 원인이 흐려집니다. 관련 있는 케이스만 묶으세요.
// ❌ 성격이 다른 케이스를 하나의 테스트에 혼합
@ParameterizedTest
@CsvSource({
"valid@email.com, true",
"invalid-email, false",
", false", // null 케이스 — 성격이 다름
"a@b@c.com, false"
})
void validate_email(String email, boolean expected) { ... }
// ✅ null/blank는 별도 테스트로 분리
@ParameterizedTest
@NullAndEmptySource
void validate_email_withNullOrEmpty_returnsFalse(String email) { ... }
@ParameterizedTest
@CsvSource({"valid@email.com, true", "a@b@c.com, false"})
void validate_email_withFormatCases(String email, boolean expected) { ... }무거운 초기화를 @BeforeEach에 넣으면 테스트 수만큼 반복 실행돼 전체 테스트가 불필요하게 느려집니다.
// ❌ 매 테스트마다 DB 컨테이너 생성
@BeforeEach
void setup() {
startDatabaseContainer();
}
// ✅ 한 번만 준비하고 각 테스트는 필요한 상태만 초기화
@BeforeAll
static void setupDatabase() {
startDatabaseContainer();
}참고 링크
2 sources