기록

[테스트전략] 각 테스트는 하나의 목적만 가진다 본문

교육/강의

[테스트전략] 각 테스트는 하나의 목적만 가진다

youngyin 2025. 2. 9. 00:00

각 테스트는 하나의 목적만 가진다

테스트 코드는 단순히 코드가 잘 작동하는지 확인하는 데서 끝나는 것이 아니라, 코드의 의도와 동작 방식을 명확히 보여줄 수 있어야 합니다.
이를 위해 하나의 테스트는 하나의 목적만 가져야 하며, 여러 목적을 포함하면 테스트가 복잡해지고 의도를 이해하기 어려워집니다.

잘못된 테스트 코드 예제 : 반복문과 조건문을 포함한 예제

아래는 반복문과 조건문을 포함한 잘못된 테스트 코드 예제입니다. 이 코드는 여러 케이스를 하나의 테스트에 담아 작성되었기 때문에 가독성과 유지보수성이 떨어집니다.

@DisplayName("사용자 권한이 특정 권한에 해당하는지 확인한다.")
@Test
void checkUserRole() {
    // given
    UserRole[] roles = UserRole.values();

    for (UserRole role : roles) {
        if (role == UserRole.ADMIN) {
            // when
            boolean result = UserRole.isAdmin(role);

            // then
            assertThat(result).isTrue();
        }

        if (role == UserRole.USER || role == UserRole.GUEST) {
            // when
            boolean result = UserRole.isAdmin(role);

            // then
            assertThat(result).isFalse();
        }
    }
}

이 코드는 반복문과 조건문을 사용해 여러 케이스를 처리하고 있습니다. 하지만 이렇게 작성하면 테스트의 목적이 명확하지 않고, 실패했을 때 어떤 케이스에서 문제가 발생했는지 파악하기 어렵습니다.

개선된 테스트 : 독립적인 테스트로 분리

각 조건을 독립적인 테스트로 분리하면 테스트 목적이 더 명확해지고 가독성과 유지보수성이 좋아집니다.

@DisplayName("ADMIN 권한인지 확인한다.")
@Test
void isAdminRole() {
    // given
    UserRole role = UserRole.ADMIN;

    // when
    boolean result = UserRole.isAdmin(role);

    // then
    assertThat(result).isTrue();
}

@DisplayName("USER 권한이 ADMIN인지 확인한다.")
@Test
void isNotAdminForUserRole() {
    // given
    UserRole role = UserRole.USER;

    // when
    boolean result = UserRole.isAdmin(role);

    // then
    assertThat(result).isFalse();
}

@DisplayName("GUEST 권한이 ADMIN인지 확인한다.")
@Test
void isNotAdminForGuestRole() {
    // given
    UserRole role = UserRole.GUEST;

    // when
    boolean result = UserRole.isAdmin(role);

    // then
    assertThat(result).isFalse();
}

이처럼 각 테스트를 분리하면 실패 시 원인을 빠르게 파악할 수 있고, 테스트 목적도 더 명확히 드러납니다. 따라서 코드의 문서 역할을 제대로 수행할 수 있습니다.

ParameterizedTest와 DynamicTest 활용

JUnit에서 제공하는 @ParameterizedTest와 DynamicTest를 활용하면 반복문 없이도 여러 테스트 케이스를 효율적으로 작성할 수 있습니다. 이를 통해 코드 중복을 줄이고 테스트를 더 깔끔하게 구성할 수 있습니다.

ParameterizedTest : 여러 데이터를 테스트

테스트 내에서는 가능한 한 if문이나 반복문을 사용하지 않는 것이 좋습니다. 만약 테스트 환경이나 데이터를 바꿔가며 테스트해야 한다면 @ParameterizedTest를 사용하는 것이 좋습니다.

private static Stream<Arguments> provideRolesForAdminCheck() {
    return Stream.of(
        Arguments.of(UserRole.ADMIN, true),
        Arguments.of(UserRole.USER, false),
        Arguments.of(UserRole.GUEST, false)
    );
}

@MethodSource("provideRolesForAdminCheck")
@ParameterizedTest
@DisplayName("권한이 ADMIN인지 확인한다.")
void checkAdminRoleParameterized(UserRole role, boolean expected) {
    // when
    boolean result = UserRole.isAdmin(role);

    // then
    assertThat(result).isEqualTo(expected);
}

DynamicTest : 시나리오 기반 테스트

테스트 간 강결합을 방지하기 위해 공유 변수를 사용하는 것은 지양해야 합니다. 여러 시나리오를 검증해야 할 경우 @DynamicTest를 활용해 순서대로 실행할 수 있습니다.

@DisplayName("권한 변경 시나리오")
@TestFactory
Collection<DynamicTest> roleChangeScenario() {
    // given
    User user = new User("John", UserRole.USER);

    return List.of(
        DynamicTest.dynamicTest("USER 권한을 ADMIN으로 변경할 수 있다.", () -> {
            // when
            user.changeRole(UserRole.ADMIN);

            // then
            assertThat(user.getRole()).isEqualTo(UserRole.ADMIN);
        }),
        DynamicTest.dynamicTest("ADMIN 권한을 GUEST로 변경할 수 있다.", () -> {
            // when
            user.changeRole(UserRole.GUEST);

            // then
            assertThat(user.getRole()).isEqualTo(UserRole.GUEST);
        }),
        DynamicTest.dynamicTest("GUEST 권한을 USER로 변경할 수 있다.", () -> {
            // when
            user.changeRole(UserRole.USER);

            // then
            assertThat(user.getRole()).isEqualTo(UserRole.USER);
        })
    );
}
 
Comments