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

y001·2025년 1월 27일
post-thumbnail

1. 단일 책임을 가진 테스트 작성하기

테스트 코드는 단순히 코드가 예상대로 동작하는지 확인하는 것을 넘어서, 코드의 의도와 동작을 명확하게 설명해야 합니다. 이를 위해 각 테스트는 하나의 목적만 가져야 하며, 여러 개의 목적을 포함하면 테스트가 복잡해지고 유지보수성이 낮아집니다.


2. 잘못된 테스트 코드 예제: 조건문과 반복문 사용

아래 코드는 반복문과 조건문을 사용하여 여러 테스트 케이스를 한 테스트 메서드에서 처리하고 있습니다. 하지만 이렇게 작성하면 테스트가 복잡해지고, 특정 케이스에서 실패할 경우 원인을 빠르게 파악하기 어렵습니다.

@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();
        } else {
            // when
            boolean result = UserRole.isAdmin(role);

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

이 코드는 여러 역할(Role)을 한 번에 검증하려다 보니 테스트 목적이 불분명해졌습니다. 실패 시 어떤 조건에서 문제가 발생했는지 빠르게 알아내기 어렵습니다.


3. 개선된 테스트 코드: 개별 테스트로 분리

각 조건을 개별 테스트로 분리하면 가독성과 유지보수성이 향상됩니다.

@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();
}

이처럼 각 테스트를 분리하면 실패 시 원인을 빠르게 찾을 수 있고, 테스트 목적이 더 명확해집니다.


4. @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);
}

@ParameterizedTest를 사용하면 반복문 없이도 여러 테스트 케이스를 간결하게 표현할 수 있습니다.


5. @TestFactory를 활용한 동적 테스트

여러 시나리오를 검증할 때 @TestFactory를 활용하면 동적으로 테스트를 생성하고 실행할 수 있습니다.

@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);
        })
    );
}

@TestFactory는 여러 개의 테스트를 한 번에 정의하고 동적으로 실행할 때 유용합니다.


6. 결론

  • 각 테스트는 하나의 목적만 가져야 한다.
  • 반복문과 조건문을 제거하여 테스트를 더 명확하게 만든다.
  • @ParameterizedTest@TestFactory를 활용하여 테스트 중복을 줄이고 가독성을 높인다.

이러한 원칙을 따르면 테스트 코드가 단순해지고 유지보수성이 높아집니다.


📌 이 글은 TDD 강의를 학습한 내용을 바탕으로 재구성하였습니다. 문제가 되는 부분이 있다면 수정하겠습니다.

0개의 댓글