@ParameterizedTest를 사용하여 테스트 효율성을 높여보자
📌 서론
이번 포스트에서는 @ParameterizedTest를 사용해서 다양한 입력 값에 대한 테스트를 진행하는 방법을 살펴볼 것이다.이 방법으로 테스트 코드를 작성하는 것은 실제로 유효성 검사 같은 것들을 할 때 매우 유용하다. 지금부터 @ParameterizedTest를 사용하지 않을때와 사용할때를 비교해 보면서 왜 이 어노테이션을 통해 입력 값에 대한 테스트를 하는게 좋은지 비교해 보자
1. 테스트하려는 도메인의 비즈니스 로직 이해하기
- 먼저, 우리가 테스트할 도메인의 비즈니스 로직은 외부에서 validateBasicInfo() 메서드를 호출하면 내부에 담겨있는 변수들을 검증하는 로직이다. 이 메서드는 각 필드가 null이나 빈 문자열이 아닌지 확인하는데, 만약 조건을 만족하지 못하면 커스텀 에러 메시지와 함께 예외를 발생시킨다.
public void validateBasicInfo() {
if (this.memberId == null) {
throw new RecipeApplicationException(ErrorCode.REQUIRED_MEMBER_ID);
}
if (this.recipeName == null || this.recipeName.trim().isEmpty()) {
throw new RecipeApplicationException(ErrorCode.REQUIRED_RECIPE_NAME);
}
if (this.recipeDesc == null || this.recipeDesc.trim().isEmpty()) {
throw new RecipeApplicationException(ErrorCode.REQUIRED_RECIPE_DESCRIPTION);
}
if (this.nickname == null || this.nickname.trim().isEmpty()) {
throw new RecipeApplicationException(ErrorCode.REQUIRED_MEMBER_NICKNAME);
}
if (this.delYn == null || this.delYn.trim().isEmpty()) {
throw new RecipeApplicationException(ErrorCode.REQUIRED_RECIPE_DELETE_YN);
}
}
2. @Parameterize 사용 전 테스트 코드 알아보기
@Parameterize를 사용하지 않는 테스트 코드
- @ParameterizedTest 어노테이션을 사용하지 않고 각각의 테스트 메서드를 작성하는 경우, 각 조건에 대해 별도의 테스트 메서드를 만들어야 한다. 예를 들면, 'Recipe' 클래스의 'validateBasicInfo' 메서드를 테스트한다고 가정할 때, 다음과 같이 각각의 조건에 대해 별도의 테스트 메서드를 작성하게 된다.
class RecipeTest {
@Test
@DisplayName("회원 ID가 null일 때 예외 발생")
void shouldThrowExceptionWhenMemberIdIsNull() {
//given
Recipe recipe = Recipe.createTest(null, "레시피 이름", "레시피 설명", "닉네임", "N");
//when & then
assertThrows(RecipeApplicationException.class, recipe::validateBasicInfo);
}
@Test
@DisplayName("레시피 이름이 null일 때 예외 발생")
void shouldThrowExceptionWhenRecipeNameIsNull() {
//given
Recipe recipe = Recipe.createTest(1L, null, "레시피 설명", "닉네임", "N");
//when & then
assertThrows(RecipeApplicationException.class, recipe::validateBasicInfo);
}
@Test
@DisplayName("레시피 설명이 null일 때 예외 발생")
void shouldThrowExceptionWhenRecipeDescIsNull() {
//given
Recipe recipe = Recipe.createTest(1L, "레시피 이름", null, "닉네임", "N");
//when & then
assertThrows(RecipeApplicationException.class, recipe::validateBasicInfo);
}
private Recipe createRecipe(Long memberId, String recipeName, String recipeDesc, String nickname, String delYn) {
return Recipe.createTest(
memberId,
recipeName,
recipeDesc,
nickname,
delYn
);
}
// 기타 매게변수 조건 검사 테스트 작성
}
테스트 결과
- 각각의 테스트는 모두 성공한다.
📌 요약
이렇게 테스트 코드를 작성하는 방법은 각 테스트 케이스마다 독립적인 테스트 메서드를 작성해야 하기 때문에, 코드 중복이 많아지고 관리가 어려워진다. 반면, @ParameterizedTest를 사용하면 이런 중복을 획기적으로 줄일 수 있어서 테스트 코드를 더 깔끔하게 관리가 된다.
3. @Parameterize 테스트 코드 작성 및 테스트 진행
테스트 코드 작성
- 새롭게 작성한 테스트 코드를 보자. @ParameterizedTest와 @MethodSource 어노테이션을 사용해서 다양한 잘못된 입력 값들에 대해 예외가 발생하는지 테스트한다.
- 예를 들어, memberId가 null이면 '회원 ID는 필수 항목입니다.'라는 메시지를 가진 RecipeApplicationException이 발생해야 한다. 이렇게 여러 케이스를 한 번에 테스트할 수 있어서 코드 중복을 줄이고, 새로운 테스트 케이스를 추가하기도 쉬워진다.
@DisplayName("[bad] - 레시피를 등록할때 잘못된 입력값이 들어오면 예외가 발생하고 커스텀 ERROR 메시지를 보여준다.")
@ParameterizedTest
@MethodSource("invalidRecipeProvider")
void shouldThrowExceptionForInvalidInputs(Long memberId, String recipeName, String recipeDesc, String nickname, String delYn,
Class<?> expectedException, String expectedMessage) {
//given
Recipe recipe = Recipe.createTest(memberId, recipeName, recipeDesc, nickname, delYn);
//when & then
RecipeApplicationException exception = assertThrows((Class<RecipeApplicationException>) expectedException, recipe::validateBasicInfo);
assertEquals(expectedMessage, exception.getMessage());
}
private static Stream<Arguments> invalidRecipeProvider() {
return Stream.of(
Arguments.of(null, "맛도리 닭갈비", "맛도리 닭갈비", "진안", "N", RecipeApplicationException.class, "회원 ID는 필수 항목입니다."),
Arguments.of(1L, null, "맛도리 닭갈비", "진안", "N", RecipeApplicationException.class, "레시피 이름은 필수 항목입니다."),
Arguments.of(1L, "맛도리 닭갈비", null, "진안", "N", RecipeApplicationException.class, "레시피 설명은 필수 항목입니다."),
Arguments.of(1L, "맛도리 닭갈비", "맛도리 닭갈비", null, "N", RecipeApplicationException.class, "닉네임은 필수 항목입니다."),
Arguments.of(1L, "맛도리 닭갈비", "맛도리 닭갈비", "진안", null, RecipeApplicationException.class, "삭제 여부는 필수 항목입니다.")
// ... [기타 테스트 케이스들] ...
);
}
private Recipe createRecipe(Long memberId, String recipeName, String recipeDesc, String nickname, String delYn) {
return Recipe.createTest(
memberId,
recipeName,
recipeDesc,
nickname,
delYn
);
}
테스트 코드 분석
- 이 코드는 @Parameterized를 활용해 여러 입력값에 대한 테스트를 수행하는 예시다. Parameterized 테스트는 한 테스트 메소드를 다양한 매개변수로 여러 번 실행할 수 있게 해 준다. 이런 방식은 같은 로직이 여러 파라미터를 가진 data에 대해서 올바르게 동작하는지 효율적으로 검증하고자 할 때 유용하다.
- 아래의 코드에서 shouldThrowExceptionForInvalidInputs 메소드는 잘못된 레시피 정보가 주어졌을 때 적절한 예외가 발생하는지 테스트한다. 여기서는 내가 직접 정의한 RecipeApplicationException이라는 예외가 발생하는지 확인한다.
- @ParameterizedTest 어노테이션은 이 메소드가 파라미터화된 테스트임을 나타내며, @MethodSource("invalidRecipeProvider") 어노테이션은 이 테스트에 필요한 매개변수를 제공하는 메서드를 지정한다.
@DisplayName("[bad] - 레시피를 등록할때 잘못된 입력값이 들어오면 예외가 발생하고 커스텀 ERROR 메시지를 보여준다.")
@ParameterizedTest
@MethodSource("invalidRecipeProvider")
void shouldThrowExceptionForInvalidInputs(Long memberId, String recipeName, String recipeDesc, String nickname, String delYn,
Class<?> expectedException, String expectedMessage) {
//given
Recipe recipe = Recipe.createTest(memberId, recipeName, recipeDesc, nickname, delYn);
//when & then
RecipeApplicationException exception = assertThrows((Class<RecipeApplicationException>) expectedException, recipe::validateBasicInfo);
assertEquals(expectedMessage, exception.getMessage());
}
- 여기서 invalidRecipeProvider 메서드는 잘못된 입력값의 여러 조합을 반환한다. 이 입력값들은 Arguments.of를 통해 정의되며, 각각 다른 유효하지 않은 시나리오를 나타낸다.
private static Stream<Arguments> invalidRecipeProvider() {
return Stream.of(
Arguments.of(null, "맛도리 닭갈비", "맛도리 닭갈비", "진안", "N", RecipeApplicationException.class, "회원 ID는 필수 항목입니다."),
Arguments.of(1L, null, "맛도리 닭갈비", "진안", "N", RecipeApplicationException.class, "레시피 이름은 필수 항목입니다."),
Arguments.of(1L, "맛도리 닭갈비", null, "진안", "N", RecipeApplicationException.class, "레시피 설명은 필수 항목입니다."),
Arguments.of(1L, "맛도리 닭갈비", "맛도리 닭갈비", null, "N", RecipeApplicationException.class, "닉네임은 필수 항목입니다."),
Arguments.of(1L, "맛도리 닭갈비", "맛도리 닭갈비", "진안", null, RecipeApplicationException.class, "삭제 여부는 필수 항목입니다.")
// ... [기타 테스트 케이스들] ...
);
}
테스트 동작 설명
- 테스트 실행 시, JUnit은 invalidRecipeProvider에서 제공하는 각각의 매개변수 data를 shouldThrowExceptionForInvalidInputs 메소드에 전달한다. 메서드는 이 매개변수들을 사용해 Recipe 객체를 생성하고, 이 객체의 유효성을 검사하게 된다.
- 유효성 검사에서 실패하면 정의된 사용자 정의 예외가 발생하며, 테스트는 이 예외가 발생하는지 그리고 발생했을 때 예상된 에러 메시지를 반환하는지 확인한다.
- 결과적으로, 이 테스트 코드는 다양한 잘못된 입력 조건에 대해 Recipe 클래스의 유효성 검증 로직이 올바르게 예외를 처리하는지 검증하는 코드다.
테스트 결과
- 모든 파라미터를 검증하는 테스트가 성공한다.
📌 요약
ParameterizedTest를 사용하면 다양한 입력 조건에 대해 하나의 테스트 메서드에서 테스트를 수행할 수 있다. 이 방식은 코드의 중복을 줄이고, 테스트 케이스를 추가하는 것을 간소화한다.
반응형
'Spring > 테스트 코드' 카테고리의 다른 글
[Spring] 예외 테스트의 중요성: 바인딩 오류 (5) | 2024.02.03 |
---|---|
Spring 통합테스트: Validation 문제 해결과 깊은 고민 (30) | 2024.01.10 |
[Spring] 테스트 코드: @MockBean/@SpyBean 사용방법 (66) | 2023.12.24 |
[Spring] 테스트 코드: @SpyBean으로 @EventListener 검증하기 (40) | 2023.12.23 |
[Spring] 테스트 코드: @MockBean으로 @EventListener 검증하기 (4) | 2023.12.23 |