개요
요즘 개발할 때는 의식적으로 테스트 코드를 작성하려고 하고 있습니다. 이 습관을 들이게 된 계기는 조영호 님의 <오브젝트> 라는 책에 내용 중, 테스트 코드 작성을 우선으로 개발하는 습관을 들인다면 데이터가 주도하는 개발이 아닌 객체의 행위가 주도하는 개발을 할 수 있다는 글이 와닿아서 그렇게 하고 있습니다.
하지만 테스트를 하다 보면 매개변수를 바꿔가면서 테스트해야 될 때가 많습니다. 예를 들면 특정 메서드에 대해서 경계값을 테스트해야 될 때가 그렇습니다. 이러한 상황에서는 다양한 입력 값에 대해서 테스트가 수행되어야 하는데 이를 일일이 작성하는 것은 비효율적입니다. 이때 JUnit5의 @ParameterizedTest가 아주 아주 유용하게 쓰입니다.
테스트 시나리오 설명
이번 포스팅에서는 사용자가 입력한 값들이 올바른 형식인지를 검증하는 테스트를 작성해 보겠습니다. 시나리오는 사용자가 회원가입을 할 때 입력한 데이터를 검사하는 메서드입니다. 조건은 아래와 같게 하겠습니다.
사용자가 회원가입을 할 때
- 이메일 형식인가?
- 비밀번호가 최소 8자 이상인가?
- 이름이 빈 문자열이 아닌가?
@Parameterized를 안 쓴다면?
아래는 사용자 등록 기능의 유효성을 검사하는 UserValidator 클래스의 메서드를 테스트한 것입니다. 위에서 나열한 조건에 따라서 전부 다 테스트해 주어야 합니다. 그리고 그 코드는 아래처럼 나오게 됩니다.
@DisplayName("email 형식이 올바르지 않으면 false를 return한다.")
@Test
void validateEmail() {
String email = "email";
String password = "password";
String username = "validuser";
boolean isValid = UserValidator.isValidUser(email, password, username);
assertThat(isValid)
.as("이메일: %s", email)
.isFalse();
}
@DisplayName("password가 8글자 이하라면 false를 return한다.")
@Test
void validatePassword() {
String email = "user@example.com";
String password = "short";
String username = "validuser";
boolean isValid = UserValidator.isValidUser(email, password, username);
assertThat(isValid)
.as("비밀번호: %s", password)
.isFalse();
}
@DisplayName("username이 빈 문자열이면 false를 return한다.")
@Test
void validateUsername() {
String email = "user@example.com";
String password = "ValidPass123";
String username = "";
boolean isValid = UserValidator.isValidUser(email, password, username);
assertThat(isValid)
.as("사용자 이름: %s", username)
.isFalse();
}
@DisplayName("모든 입력값이 정상적이면 true를 return한다.")
@Test
void validateUserInfo() {
String email = "user@example.com";
String password = "ValidPass123";
String username = "validuser";
boolean isValid = UserValidator.isValidUser(email, password, username);
assertThat(isValid)
.as("모든 입력이 유효함")
.isTrue();
}

결과는 잘 나오지만 언뜻 보기에도 유지보수성이 떨어져 보입니다. 만약에 조건이 추가된다면 그에 대한 개별 테스트를 또 작성해 주어야 하기 때문에 비효율적입니다.
@ParameterizedTest를 써보자
위와 똑같은 기능을 @ParameterizedTest를 쓴다면 어떻게 바뀌는지 한 번 테스트해 보겠습니다.
@ParameterizedTest
@CsvSource({
"email, password, validuser, false", // 잘못된 이메일 형식
"user@example.com, short, validuser, false", // 짧은 비밀번호
"user@example.com, ValidPass123, , false", // 빈 사용자 이름
"user@example.com, ValidPass123, validuser, true" // 모든 값이 유효함
})
@DisplayName("다양하게 테스트 해보자")
void testUserRegistration(String email, String password, String username, boolean expectedIsValid) {
boolean isValid = UserValidator.isValidUser(email, password, username);
assertThat(isValid)
.as("이메일: %s, 비밀번호: %s, 사용자 이름: %s", email, password, username)
.isEqualTo(expectedIsValid);
}

@ParameterizedTest로 작성한 테스트 메서드 하나는 그 위의 테스트 메서드 4개의 기능을 모두 수행하고 있습니다. 결과를 봐도 다른 게 전혀 없다는 걸 알 수 있죠.
@ParameterizedTest의 장점
1. 코드 중복이 감소된다.
위 결과처럼 여러개의 테스트 메서드가 필요 없고, 하나의 메서드만으로 다양한 입력값을 테스트할 수 있습니다.
2. 유지보수성이 향상된다.
만약 비밀번호에 대문자가 1개 이상 포함되어야 한다는 조건이 추가된다면 테스트 메서드를 더 작성할 필요 없이 @CsvSource의 매개변수 안에 테스트할 데이터만 추가해 주면 됩니다.
다양한 Source 어노테이션
@ParameterizedTest는 다양한 어노테이션들을 제공합니다. 포스팅에서는 대표적으로 잘 쓰이는 3가지의 어노테이션만 설명해 보겠습니다. 더 많은 정보는 링크에서 확인할 수 있습니다.
@ValueSource
이것은 단일 타입으로 여러 값을 테스트하고자 할 때 사용합니다. 주로 여러 개의 문자열을 테스트할 때 유용하게 사용합니다.
@ParameterizedTest
@ValueSource(strings = {"", " ", "validuser"})
@DisplayName("username이 빈 문자열 또는 공백일 때 false를 return한다.")
void testUsernameValidation(String username) {
boolean isValid = UserValidator.isValidUsername(username);
assertThat(isValid)
.as("사용자 이름: '%s'", username)
.isFalse();
}
@CsvSource
CSV 형식할 때 그 CSV가 맞습니다. CSV는 Comma-Separated Values의 약자입니다. 즉 , (쉼표)로 구분된 여러 매개변수를 테스트할 때 유용합니다.
@ParameterizedTest
@CsvSource({
"user@example.com, short, false",
"user@example.com, ValidPass123, true"
})
@DisplayName("password 유효성 검사를 수행한다.")
void testPasswordValidation(String email, String password, boolean expectedIsValid) {
boolean isValid = UserValidator.isValidPassword(password);
assertThat(isValid)
.as("이메일: %s, 비밀번호: %s", email, password)
.isEqualTo(expectedIsValid);
}
이때 @CsvSource의 매개변수들을 테스트 메서드의 매개변수 선언 순서에 맞게 대입됩니다. 테스트 메서드의 매개변수 순서가 email, password, expectedIsValid니까 @CsvSource 매개변수들도 이에 맞게 email, password, expectedIsValid의 순서로 매핑됩니다.
@MethodSource
메서드에서 직접 데이터를 만들어서 제공합니다. 복잡한 구조에서 사용하면 좋습니다.
@ParameterizedTest
@MethodSource("provideUserValidationScenarios")
@DisplayName("MethodSource를 사용한 다양한 유효성 검사 케이스 테스트")
void testUserRegistrationWithMethodSource(String email, String password, String username, boolean expectedIsValid) {
boolean isValid = UserValidator.isValidUser(email, password, username);
assertThat(isValid)
.as("이메일: %s, 비밀번호: %s, 사용자 이름: %s", email, password, username)
.isEqualTo(expectedIsValid);
}
static Stream<Arguments> provideUserValidationScenarios() {
return Stream.of(
Arguments.of("invalid-email", "ValidPass123", "validuser", false),
Arguments.of("user@example.com", "short", "validuser", false),
Arguments.of("user@example.com", "ValidPass123", "", false),
Arguments.of("user@example.com", "ValidPass123", "validuser", true)
);
}
@MethodSource의 매개변수로 활용되는 메서드는 반드시 static 접근제어자여야 합니다. 또한 매개변수의 반환타입은 Stream으로 바꿀 수 있는 모든 형은 가능합니다.
참고한 글 + 더 좋은 설명 (하지만 영어)
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
'공부 > Spring' 카테고리의 다른 글
| [Spring]Spring Converter를 사용한 이유: Enum 타입을 소문자로 받기 위하여 (0) | 2024.08.05 |
|---|---|
| [Spring] Spring Security 없이 카카오 로그인 구현하기 (0) | 2024.07.28 |