IT/Spring

Private Method Test 하기

봉즙 2023. 9. 14. 13:29

Test 코드를 작성하다보면 private method를 테스트해야할 경우가 생긴다.

이러한 경우 Reflection을 사용하면 테스트가 가능하다.

여기서는 Spring의 ReflectionTestUtils와 Java의 Reflection을 사용하는 방법에 대해 작성하였다.

data class Human(
    val age: Int
) {

    private fun isAdult(): Boolean {
        return age > 19
    }
}

import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test
import org.springframework.test.util.ReflectionTestUtils
import java.lang.reflect.Method

class HumanTest {
    @Test
    fun springTest() {
        val adult = Human(20)
        val child = Human(10)
        assertTrue(ReflectionTestUtils.invokeMethod<Boolean>(adult, "isAdult")!!)
        assertFalse(ReflectionTestUtils.invokeMethod<Boolean>(child, "isAdult")!!)
    }

    @Test
    fun test() {
        val adult = Human(20)
        val child = Human(10)

        val method: Method = Human::class.java.getDeclaredMethod("isAdult")
        method.setAccessible(true)
        val adultResult = method.invoke(adult) as Boolean
        val childResult = method.invoke(child) as Boolean

        assertTrue(adultResult)
        assertFalse(childResult)
    }
}

springTest()는 Spring의 ReflectionTestUtils를 사용한 방법이다.

아래의 메서드가 ReflectionTestUtils의 invokeMethod 메서드이다.

targetObject 혹은 targetClass를 넘겨주고, 메서드 이름과 인자를 넘겨주면 해당 메서드를 실행시켜주는 방식으로 구현되어있다.

public class ReflectionTestUtils {

    @Nullable
    public static <T> T invokeMethod(Class<?> targetClass, String name, Object... args) {
        Assert.notNull(targetClass, "Target class must not be null");
        return invokeMethod(null, targetClass, name, args);
    }

    @Nullable
    public static <T> T invokeMethod(@Nullable Object targetObject, @Nullable Class<?> targetClass, String name,
                                     Object... args) {

        Assert.isTrue(targetObject != null || targetClass != null,
                "Either 'targetObject' or 'targetClass' for the method must be specified");
        Assert.hasText(name, "Method name must not be empty");

        try {
            MethodInvoker methodInvoker = new MethodInvoker();
            methodInvoker.setTargetObject(targetObject);
            if (targetClass != null) {
                methodInvoker.setTargetClass(targetClass);
            }
            methodInvoker.setTargetMethod(name);
            methodInvoker.setArguments(args);
            methodInvoker.prepare();

            if (logger.isDebugEnabled()) {
                logger.debug(String.format("Invoking method '%s' on %s or %s with arguments %s", name,
                        safeToString(targetObject), safeToString(targetClass), ObjectUtils.nullSafeToString(args)));
            }

            return (T) methodInvoker.invoke();
        }
        catch (Exception ex) {
            ReflectionUtils.handleReflectionException(ex);
            throw new IllegalStateException("Should never get here");
        }
    }
}

test()는 java의 Reflection을 사용한 방법이다.

public class Class {
    @CallerSensitive
    public Method getDeclaredMethod(String name, Class<?>... parameterTypes)
            throws NoSuchMethodException, SecurityException {
        Objects.requireNonNull(name);
        @SuppressWarnings("removal")
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            checkMemberAccess(sm, Member.DECLARED, Reflection.getCallerClass(), true);
        }
        Method method = searchMethods(privateGetDeclaredMethods(false), name, parameterTypes);
        if (method == null) {
            throw new NoSuchMethodException(methodToString(name, parameterTypes));
        }
        return getReflectionFactory().copyMethod(method);
    }
}

이름을 통해 메서드를 찾은 다음 복사하여 메서드를 반환하는 방식으로 구현되어있다.

이후 테스트 코드에서 method.setAccessible(true) 접근이 가능하도록 수정한 후 private 메서드를 동작시킨다.

| Ref. |

https://dev.gmarket.com/38