정상혁정상혁

private 메소드를 어떻게 테스트해야 할지에 대한 질문은 테스트 코드 작성에 대한 자주 나오는 질문 중에 하나입니다. 이에 대한 답변을 간단히 정리해봅니다.

  • 많은 경우에 private method는 public 메소드에서 extract method 되어서 나온 것이므로 public을 통해서 간접적으로 테스트를 하는 것이 자연스럽습니다.

  • 그래도 private 영역만 따로 테스트를 해야지 더욱 다양한 테스트 케이스를 편하게 작성할 수 있다거나 디버깅이 쉬워진다면,해당 메소드를 package private(default 접근자)나 protected로 바꾸어서 테스트해볼 수도 있습니다. 일반적으로 테스트를 위해서 production 코드의 접근 범위를 넓히는 것은 클래스의 노출 범위를 커지게 하므로 바람직하지 않을 수도 있습니다. 하지만 private method를 public에서 간접적으로 테스트하는 것만으로 충분하지 않고 따로 테스트를 해봐야할 정도가 된다는 것도 설계 개선의 신호일 수 있습니다. private 메소드가 하는 일이 크다는 신호일 수도 있고, 그렇다면 별도의 클래스로 분리하거나, 향후 하위 클래스에서 상속을 해서 대체할 수 있는 가능성을 고려해서 protected로 해두는 것도 고려해 볼만합니다.

  • Reflection을 이용하면 강제적으로 private 메소드를 호출할 수 있습니다. 다만 이렇게 하면 메소드이름 부분이 String값으로 넘겨지게 되므로, compile time에 메소드명의 오타가 검증되지 못하고, refactoring으로 메소드명을 바꾸어도 자동으로 String으로 적힌 부분은 바뀌지 않는 단점이 있습니다. 위의 모든 경우를 다 고려해보고 그대로 필요하다고 생각되는 경우에만 제한적으로 사용하기를 권장드립니다.

(1) PowerMock에서 Whitebox.invokeMethod(..) 메소드를 이용해서도 테스트를 할 수 있습니다.

(2) JUnit Addons에 포함된 PrivateAccessor를 이용하면 테스트 할 수 있습니다.

(3) 아래와 같이 직접 간단한 코드를 만들어서도 비슷한 역할을 할 수 있습니다.

package edu.tdd;

import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

import org.junit.Test;

public class ReflectionCallUtilsTest {

    @Test
    public void testCallPrivate() throws SecurityException, NoSuchMethodException, IllegalAccessException, InvocationTargetException {
        UnderTest ut = new UnderTest();
        String name = "jsh";
        String address = "서울시 마포구";
        invoke(ut, "print",name, address);
        assertThat(ut.isCalled(),is(true));
    }

    @Test
    public void testCallWithPrimitiveType() throws SecurityException, NoSuchMethodException, IllegalAccessException, InvocationTargetException {
        UnderTest ut = new UnderTest();
        String name = "jsh";
        int age = 35;
        boolean printed = (Boolean)invoke(ut, "print", new Class<?>[]\{String.class, int.class}, name,age);
        assertThat(printed,is(true));
        assertThat(ut.isCalled(),is(true));
    }
    private Object invoke(Object ut, String methodName, Class<?>[] argTypes, Object ... args) throws SecurityException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        Method method = ut.getClass().getDeclaredMethod(methodName, argTypes);
        method.setAccessible(true);
        return method.invoke(ut, args);
    }

    private Object invoke(Object ut, String methodName, Object ... args) throws SecurityException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        int argSize = args.length;
        Class<?>[] argTypes = new Class<?>[argSize];
        for(int i=0;i< argSize;i++){
            argTypes[i] = args[i].getClass();
        }
        return invoke(ut,methodName, argTypes, args);
    }

    static class UnderTest {
        private boolean called = false;
        public boolean isCalled(){
            return called;
        }
        private void print(String name, String address) {
            System.out.println(name);
            System.out.println(address);
            called = true;
        }
        private boolean print(String name, int age){
            System.out.println(name);
            System.out.println(age);
            called = true;
            return true;
        }
    }
}
____


필요하다면 test 코드 안에서 쓰이고 있는 invoke메소드를 따로 Util 클래스로 분리할 수도 있습니다.

위의 코드에서 Object .. args 넘어가는 부분이 primitive type이 포함되면 Object[]로 바뀌는 과정에서 Wrapper class로 바뀌는  auto-boxing이 발생하게 됩니다. 그래서 매개변수에 primitive type이 있을 때는 invoke(Object ut, String methodName, Object ... args) 사용하면 NoSuchMethodException이 발생하게 됩니다. 그럴 때는 type을 정확히 명시해 주는 invoke(Object ut, String methodName, Class<?>[] argTypes, Object ... args)을 사용하면 됩니다.

== 참고자료
* http://www.artima.com/suiterunner/private.html[Testing Private Methods with JUnit and SuiteRunner] : 위의 글과 비슷하게 아래 4가지 방법을 제시하고 있습니다.
**  Don't test private methods.
**  Give the methods package access.
**  Use a nested test class.
** Use reflection.
* http://www.yes24.com/24/goods/3908398[채수원 저 테스트 주도개발] 441페이지 : public을 테스트함으로서 간접적으로 테스트하는 방식을 권장하고 굳이 한다면 Reflection을 사용할 수 있다는 점을 언급하고 있으나 부서지기 쉬운 테스트 코드가 되기 쉬움을 경고 하고 있습니다.
* http://xper.org/wiki/xp/TestingPrivateInterfaces : Xper에서 논의된 내용