private 메소드를 어떻게 테스트해야 할지에 대한 질문은 테스트 코드 작성에 대한 자주 나오는 질문 중에 하나입니다. 이에 대한 답변을 간단히 정리해봅니다.
-
많은 경우에 private method는 public 메소드에서 extract method 되어서 나온 것이므로 public을 통해서 간접적으로 테스트를 하는 것이 자연스럽습니다.
-
그래도 private 영역만 따로 테스트를 해야지 더욱 다양한 테스트 케이스를 편하게 작성할 수 있다거나 디버깅이 쉬워진다면 설계를 개선하라는 신호로 해석할만만합니다. private 메소드가 하는 일이 크다는 신호로 해석하고 별도의 클래스로 분리하거나, 하위 클래스에서 상속을 해서 대체할 수 있는 가능성을 고려해서 protected로 해두는 것도 고려해 볼만합니다.
그럼에도 불구하고 private 메서드를 테스트해야한다면
설계 개선 등을 바로 할 수 상황이라 당장 private 메서드를 테스트해야할때 쓸수 있는 방법을 이어서 소개합니다.
-
테스트만을 해당 메소드를 package private(default 접근자)나 protected로 바꾸어서 테스트해볼 수도 있습니다. 일반적으로 테스트를 위해서 production 코드의 접근 범위를 넓히는 것은 클래스의 노출 범위를 커지게 하므로 바람직하지 않을 수도 있습니다.
-
Reflection을 이용하면 강제적으로 private 메소드를 호출할 수 있습니다. 다만 이렇게 하면 메소드이름 부분이 String값으로 넘겨지게 되므로, compile time에 메소드명의 오타가 검증되지 못하고, refactoring으로 메소드명을 바꾸어도 자동으로 String으로 적힌 부분은 바뀌지 않는 단점이 있습니다. 부작용을 감수하고서라도 쓰겠다고 각오가 된 곳에 제한적으로 사용하기를 권장드립니다.
Reflection으로 private 메서드를 호출하는 방법들은 아래와 같습니다.
(1) PowerMock에서 Whitebox.invokeMethod(..) 메소드 활용
(2) JUnit Addons에 포함된 PrivateAccessor
(3) 직접 Reflection 활용
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)
을 사용하면 됩니다.
참고자료
-
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.
-
-
채수원 저 테스트 주도개발 441페이지 : public을 테스트함으로서 간접적으로 테스트하는 방식을 권장하고 굳이 한다면 Reflection을 사용할 수 있다는 점을 언급하고 있으나 부서지기 쉬운 테스트 코드가 되기 쉬움을 경고 하고 있습니다.
-
http://xper.org/wiki/xp/TestingPrivateInterfaces : Xper에서 논의된 내용
Twitter
Google+
Facebook
Reddit
LinkedIn
StumbleUpon
Email