본문 바로가기
공부/Spring

[JUnit] 여러 매개변수를 테스트해야 한다면? @ParameterizedTest

by KimSuyong 2024. 8. 25.

개요

요즘 개발할 때는 의식적으로 테스트 코드를 작성하려고 하고 있습니다. 이 습관을 들이게 된 계기는 조영호 님의 <오브젝트> 라는 책에 내용 중, 테스트 코드 작성을 우선으로 개발하는 습관을 들인다면 데이터가 주도하는 개발이 아닌 객체의 행위가 주도하는 개발을 할 수 있다는 글이 와닿아서 그렇게 하고 있습니다.

 

하지만 테스트를 하다 보면 매개변수를 바꿔가면서 테스트해야 될 때가 많습니다. 예를 들면 특정 메서드에 대해서 경계값을 테스트해야 될 때가 그렇습니다. 이러한 상황에서는 다양한 입력 값에 대해서 테스트가 수행되어야 하는데 이를 일일이 작성하는 것은 비효율적입니다. 이때 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