혹시 테스트 코드를 작성할 때 아래와 같은 중복되는 코드를 작성하고 계신가요?
@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)
);
}
결과
@DisplayName
은 메소드의 이름이고
@ParameterizedTest
의 name은 결과에서의 이름으로 나타납니다. {index}는 1부터 순서대로 표시해주며, {3}은 파라미터에서 4번째 파라미터인 message
를 출력해줍니다. ( 0부터 시작하기 때문에 {3}은 4번째 파라미터를 실행합니다)
References
'Java' 카테고리의 다른 글
자바 리플렉션이란? (0) | 2019.12.28 |
---|---|
[객체지향] OCP (개방 폐쇄의 원칙 : Open Close Principle) (0) | 2019.12.28 |
[객체지향] if-else의 문제점 (0) | 2019.12.28 |
[Java] toString()에 대해서 (0) | 2019.12.28 |
if문 없이 문자열 계산하기 (feat. 전략패턴, Enum) (0) | 2019.05.12 |