티스토리 뷰

Java & Kotlin

[Java] Private 메소드를 테스트하는 방법과 이를 지양해야 하는 이유

망나니개발자 2022. 4. 9. 10:00
반응형

이번에는 private 메소드를 테스트하는 방법에 대해 알아보도록 하겠습니다. 미리 이 글의 결론을 말씀드리면 private 메소드를 테스트하면 안된다는 것입니다. private 메소드를 테스트하는 코드를 작성하는 것은 뭔가 좋지 못한 신호이므로 private 메소드의 테스트 코드를 작성하기 보다는 현재 코드 구조나 상태 등을 확인해볼 필요가 있습니다.

 

 

 

 

1. Private 메소드를 테스트하는 방법과 이를 지양해야 하는 이유


[ Private 메소드를 테스트하는 방법 ]

문제 코드 소개

예를 들어 이미 정의된 이름 목록이 있을 때, 이를 대문자로 변환하여 해당 입력값과 일치하면 그대로 값을 반환하고 일치하지 않으면 UUID를 생성하여 반환하는 다음과 같은 코드가 있다고 하자. 

@Service
public class PrivateTestClass {

    private final static List<String> PREDEFINED_NAME = Arrays.asList("MangKyu", "Tistory");

    public String getDisplayName(String name) {
        if (isPredefined(name)) {
            return name;
        }
        return UUID.randomUUID().toString();
    }

    private boolean isPredefined(String name) {
        return PREDEFINED_NAME.stream()
                .anyMatch(name::equals);
    }
}

 

 

그리고 여기서 isPredefined를 테스트하고자 하는데, 문제는 해당 코드의 접근 제어자가 private이라 일반적으로는 테스트가 불가능하며 컴파일 에러가 발생한다는 것이다. 이번에는 아래의 private 메소드를 테스트하는 방법을 알아보도록 하자.

 

 

Java Reflection API를 이용한 메소드 호출

가장 먼저 떠오르는 방법은 Java에서 제공하는 리플렉션 API를 이용하는 방법이다. 자바는 클래스와 메소드 자체를 포함해 클래스의 변수, 메소드의 파라미터 타입, 메소드 이름 등의 메타 정보들을 위한 Class, Method 등을 정의해두었다. 리플렉션을 이용하면 정적으로 고정된 메소드의 코드를 메타정보로 추상화된 Method를 얻어낼 수 있으며 직접 호출 또한 가능하다.

그러므로 우리가 테스트하고자 하는 클래스로부터 Method를 추출하여 해당 메소드를 직접 invoke해주면 된다.

@ExtendWith(MockitoExtension.class)
class PrivateTestClassTest {

    @InjectMocks
    private PrivateTestClass target;

    @Test
    void isPredifined가True_ReflectionAPI() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        // given
        String name = "MangKyu";
        Method method = target.getClass().getDeclaredMethod("isPredefined", String.class);
        method.setAccessible(true);

        // when
        boolean result = (boolean)method.invoke(target, name);

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

}

 

 

해당 메소드를 얻어오기 위해서는 메소드의 이름과 타입 클래스를 넘겨주어야 하며, private 메소드는 기본적으로 접근 가능 여부가 false이므로 이를 true로 변경해주어야 한다. 그리고 직접 메소드를 호출해보면 테스트가 성공함을 확인할 수 있다. 하지만 이러한 방식은 상당히 번거롭다. 스프링 프레임워크를 이용중이라면 조금 더 간단히 이를 해결할 수 있다.

 

 

 

Spring의 ReflectionTestUtils를 이용한 메소드 호출

스프링 프레임워크는 내부적으로 리플렉션을 상당히 많이 활용하고 있다. 그래서 직접 자바의 리플렉션 API를 하는 것 보다는 효율적으로 리플렉션을 사용하기 위한 유틸성 클래스인 ReflectionTestUtils를 제공하고 있다. 이는 다음과 같이 사용할 수 있다.

@ExtendWith(MockitoExtension.class)
class PrivateTestClassTest {

    @InjectMocks
    private PrivateTestClass target;

    @Test
    void isPredifined가True_ReflectionTestUtils() {
        // given
        String name = "MangKyu";

        // when
        boolean result = ReflectionTestUtils.invokeMethod(target, "isPredefined", name);

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

}

 

 

private 메소드를 호출하기 위한 ReflectionTestUtils의 invokeMethod는 파라미터로 타깃 객체, 메소드 이름, 파라미터 목록(가변 변수)를 받고 있다. 위의 코드를 실행하면 테스트가 성공함을 확인할 수 있다.

 

 

 

private 메소드 테스트를 지양해야 하는 이유

이 글에서 가장 중요한 부분이며, 이 글을 작성한 이유이기도 하다.

리플렉션은 런타임에 동작하는 기술로, 클래스와 메소드의 메타정보를 사용해서 애플리케이션을 동적으로 유연하게 만들 수 있다. 그래서 리플렉션을 사용하면 private 메소드를 invoke 할 수 있다. 그런데 문제는 이렇게 작성한 private 메소드에 대한 테스트는 깨지 쉬운 테스트가 된다는 것이다. private 메소드는 내부를 감추어 클라이언트와의 결합도를 낮춰주는데, 클라이언트인 테스트 클래스가 내부 메소드를 알고 있으니 결합도가 높아진다. 그리고 이는 유지보수할 테스트에 대한 비용을 증가시키는 요인이 될 수 있는데, 메소드 이름이나 파라미터 등을 변경할 때 실패하게 된다. 또한 리플렉션 자체 역시 컴파일 에러를 유발하지 못하므로 최대한 사용을 자제해야 한다.

물론 private 테스트가 정말 필요한 상황도 있다. 리팩토링을 위해 혹은 기능의 동작 검증을 위해 임시로 작성해도 괜찮고, 남겨두어도 괜찮다. 다만 나중에 내부 구현이 바뀌어 테스트가 깨지는 상황이 되었다면, 그때는 반드시 제거해주도록 하자.

 

 

 

 

그렇다면 어떻게 테스트 해야하는가?

그렇다면 위의 경우는 어떻게 해야하는가? 우선 테스트 대상은 private 메소드가 아닌 public 메소드인 getDisplayName이 되어야 한다. 그리고 실패하는 케이스와 성공하는 케이스를 나누어 private 메소드까지 커버할 수 있는 여러 개의 파라미터로 돌리면 된다. Junit5에서는 @ParameterizedTest를 이용하면 동일한 테스트를 여러 개의 파라미터로 실행할 수 있다. 이를 코드로 작성하면 다음과 같다.

@ExtendWith(MockitoExtension.class)
class PrivateTestClassTest {

    @InjectMocks
    private PrivateTestClass target;

    @ParameterizedTest
    @ValueSource(strings = {"MangKyu", "Tistory"})
    void isPredifined가True(String name) {
        String result = target.getDisplayName(name);

        assertThat(result.equals(name)).isTrue();
    }

    @ParameterizedTest
    @ValueSource(strings = {"banana", "taxi", "mangkyu", "tistory"})
    void isPredifined가False(String name) {
        String result = target.getDisplayName(name);

        assertThat(result.equals(name)).isFalse();
    }
}

 

 

 

 

 

private 메소드의 테스트는 테스트 클래스가 내부 메소드를 알고 있으니 결합도가 높아지고 깨지기 쉽다. 그리고 이는 유지보수할  테스트에 대한 비용을 증가시키는 요인이 될 수 있다. 그러므로 private 메소드를 테스트해야 하는 상황이라면 무언가 책임이 이상하거나 설계가 잘못되었다는 신호로 받아들이고 점검을 해볼 필요가 있다. 이는 리팩토링의 신호이다.

 

 

 

 

 

반응형
댓글
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG more
«   2024/04   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함