Private 메소드를 테스트하는 방법
예를 들어 id를 만들때 해당 id의 길이가 2에서 10 사이가 아니라면 UUID를 생성하여 반환하는 다음과 같은 코드가 있다고 하자.
@Service
public class PrivateTestClass {
public String makeID(String id) {
if (isCollectID(id)) {
return id;
}
return UUID.randomUUID().toString().substring(0,10);
}
private boolean isCollectID(String id) {
return 2 <= id.length() && id.length() <= 10;
}
}
그리고 isCollectID를 테스트하고자 하는데, 문제는 해당 코드의 접근 제어자가 private이라 일반적으로는 테스트가 불가능하며 컴파일 에러가 발생한다.
Java Reflection API를 이용한 메소드 호출
Java에서 제공하는 리플렉션 API를 이용하는 방법을 사용할 수 있다. 자바는 클래스와 메소드 자체를 포함해 클래스의 변수, 메소드의 파라미터 타입, 메소드 이름 등의 메타 정보들을 위한 Class, Method 등을 정의해 두었다. 리플렉션을 이용하면 정적으로 고정된 메소드의 코드를 메타정보로 추상화된 Method를 얻어낼 수 있으며 직접 호출 또한 가능하다.
그러므로 우리가 테스트하고자 하는 클래스로부터 Method를 추출하여 해당 메소드를 직접 invoke 해주면 된다.
@ExtendWith(MockitoExtension.class)
class PrivateTestClassTest {
@InjectMocks
private PrivateTestClass target;
@Test
void isCollectID_ReflectionAPI() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
//given
String id = "abcd";
Method method = target.getClass().getDeclaredMethod("isCollectID", String.class);
method.setAccessible(true);
//when
boolean result = (boolean) method.invoke(target, id);
//then
assertThat(result).isTrue();
}
}
해당 메소드를 얻어오기 위해서는 메소드의 이름과 타입 클래스를 넘겨주어야 하며, private 메소드는 기본적으로 접근 가능 여부가 false이므로 이를 true로 변경해주어야 한다. 하지만 이러한 방식은 상당히 번거롭다. 스프링 프레임워크를 이용 중이라면 조금 더 간단히 이를 해결할 수 있다.
Spring의 ReflectionTestUtils를 이용한 메소드 호출
스프링 프레임워크는 내부적으로 리플렉션을 상당히 많이 활용하고 있다. 그래서 직접 자바의 리플렉션 API를 하는 것보다는 효율적으로 리플렉션을 사용하기 위한 유틸성 클래스인 ReflectionTestUtils를 제공하고 있다. 이는 다음과 같이 사용할 수 있다.
@ExtendWith(MockitoExtension.class)
class PrivateTestClassTest {
@InjectMocks
private PrivateTestClass target;
@Test
void isCollectID_ReflectionTestUtils() throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
//given
String id = "abcd";
//when
boolean result = ReflectionTestUtils.invokeMethod(target, "isCollectID", id);
//then
assertThat(result).isTrue();
}
}
private 메소드를 호출하기 위한 ReflectionTestUtils의 invokeMethod는 파라미터로 타깃 객체, 메소드 이름, 파라미터 목록(가변 변수)을 받고 있다.
private 메소드 테스트를 지양해야 하는 이유
리플렉션은 런타임에 동작하는 기술로, 클래스와 메소드의 메타정보를 사용해서 애플리케이션을 동적으로 유연하게 만들 수 있다. 그래서 리플렉션을 사용하면 private 메소드를 invoke 할 수 있다. 그런데 문제는 이렇게 작성한 private 메소드에 대한 테스트는 깨지기 쉬운 테스트가 된다는 것이다. private 메소드는 내부를 감추어 클라이언트와의 결합도를 낮춰주는데, 클라이언트인 테스트 클래스가 내부 메소드를 알고 있으니 결합도가 높아진다. 그리고 이는 유지보수할 때 테스트에 대한 비용을 증가시키는 요인이 될 수 있는데, 메소드 이름이나 파라미터 등을 변경할 때 실패하게 된다. 또한 리플렉션 자체 역시 컴파일 에러를 유발하지 못하므로 최대한 사용을 자제해야 한다.
물론 private 테스트가 정말 필요한 상황도 있다. 리팩토링을 위해 혹은 기능의 동작 검증을 위해 임시로 작성해도 괜찮고, 남겨두어도 괜찮다. 하지만 나중에 내부 구현이 바뀌어 테스트가 깨지는 상황이 된다면, 그때는 반드시 제거해주어야 한다.
private 메소드를 올바르게 테스트하는 방법
우선 테스트 대상은 private 메소드가 아닌 public 메소드인 makeID가 되어야 한다. 그리고 실패하는 케이스와 성공하는 케이스를 나누어 private 메소드까지 커버할 수 있는 여러 개의 파라미터로 돌리면 된다. Junit5에서는 @ParameterizedTest를 이용하면 동일한 테스트를 여러 개의 파라미터로 실행할 수 있다.
@ExtendWith(MockitoExtension.class)
class PrivateTestClassTest {
@InjectMocks
private PrivateTestClass target;
@ParameterizedTest
@ValueSource(strings = {"collect", "Hello"})
void isCollectID_true(String name) {
String result = target.makeID(name);
assertThat(result.equals(name)).isTrue();
}
@ParameterizedTest
@ValueSource(strings = {"b","asdasdasdwqd"})
void isCollectID_false(String name) {
String result = target.makeID(name);
assertThat(result.equals(name)).isFalse();
}
}
private 메소드의 테스트는 테스트 클래스가 내부 메소드를 알고 있으니 결합도가 높아지고 깨지기 쉽다. 그리고 이는 유지보수할 때 테스트에 대한 비용을 증가시키는 요인이 될 수 있다. 그러므로 private 메소드를 테스트해야 하는 상황이라면 무언가 책임이 이상하거나 설계가 잘못되었다는 신호로 받아들이고 점검을 해볼 필요가 있다. 이는 리팩토링의 신호이다.
'JAVA' 카테고리의 다른 글
[JAVA] 불변 객체를 사용해야 하는 이유 (2) | 2023.05.17 |
---|---|
다양한 가비지 컬렉션(Garbage Collection) 알고리즘 (0) | 2023.05.15 |
[JAVA] 가비지 컬렉션(Garbage Collection)의 개념 및 동작 원리 (0) | 2023.05.14 |
[JAVA] JVM 동작원리 및 기본 개념 (1) | 2023.05.14 |
[JAVA] Optional 잘 사용하는 방법 (0) | 2023.05.12 |