Java のプライベート・メソッドをテストする

Javaで書かれたレガシーコードを改修するお仕事では、リフレクションを使ってのテストは避けられないと思います。
リフレクションをそのまま使うとテストコードが煩雑になりすぎるので、簡単なラッパーを作成しました。

どの現場でも毎度同じようなコードを書いてる気がしますが、意外とだれも用意してくれないんですよね。(^-^;

(Github にもコミットしています。レポジトリ

なぜか配列の[]が記事から消えるので、適当に補完してください!編集画面では表示されてるのに。。。

 

MethodInvoker.java

package util;

import java.lang.reflect.Method;

/**
 * 可視性を意識せずにメソッドを実行するためのユーティリティ・クラスです.
 * @author naga
 */
public class MethodInvoker {

    private Class clazz;
    private Object targetInstance;
    private String methodName;
    private Object argValues;
    private Class argTypes;

    /**
     * メソッド実行対象クラスのインスタンスと実行メソッド名を引数とするコンストラクタ.
     * @param instance メソッド実行対象クラスのインスタンス nullの場合の挙動は保証しません
     * @param methodName 実行メソッド名 nullの場合の挙動は保証しません
     */
    public MethodInvoker(Object instance, String methodName) {
        this.targetInstance = instance;
        this.clazz = instance.getClass();
        this.methodName = methodName;
    }

    /**
     * 実行対象メソッドが複数の引数を必要とする場合に使用します.
     * @param values 実行時の引数の実値 実行対象メソッドのシグニチャに定義されている順番で配列にセットしてください
     * @param types 実行時の引数の型 リフレクションにより対象メソッドを特定するために指定が必要です。
     * 引数の実値に対応した順番で配列にセットしてください。
     */
    public void args(Object values, Class types) {
        if (values == null || types == null
                || values.length != types.length) {
            throw new IllegalArgumentException();
        }

        this.argValues = values;
        this.argTypes = types;
    }

    /**
     * 実行対象メソッドが引数を必要とする場合に使用します.
     * @param value 実行時の引数の実値
     * @param type 実行時の引数の型 リフレクションにより対象メソッドを特定するために指定が必要です。
     */
    public void arg(Object value, Class type) {
        if (type == null) {
            throw new IllegalArgumentException();
        }

        this.argValues = new Object {value};
        this.argTypes = new Class { type };
    }

    /**
     * メソッドを実行します.
     * @return メソッドに戻り値がある場合、{@code Object}型で返却します.
     * 戻り値の型を指定したい場合、{@link #invokeAndReturn(Class)}を使用してください。
     * @throws RuntimeException 例外はすべて{@link RuntimeException}にラップして投げます
     */
    @SuppressWarnings("unchecked")
    public Object invoke() {
        try {
            if (hasArgs()) {
                Method method = clazz.getDeclaredMethod(methodName, argTypes);
                method.setAccessible(true);
                return method.invoke(targetInstance, argValues);
            } else {
                Method method = clazz.getDeclaredMethod(methodName);
                method.setAccessible(true);
                return method.invoke(targetInstance);
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * メソッド実行後、戻り値を指定した型にキャストし返却します.
     * @param type 戻り値の型
     * @param <E> 型パラメータ
     * @return 指定した型にキャストした戻り値
     * @throws RuntimeException 例外はすべて{@link RuntimeException}にラップして投げます
     */
    @SuppressWarnings("unchecked")
    public <E> E invokeAndReturn(Class<E> type) {
        return (E) this.invoke();
    }

    private boolean hasArgs() {
        return argValues != null;
    }
}

使用方法はテストクラスを参考にしてください。 

MethodInvokerTest.java (一部抜粋)

@RunWith(Suite.class)
@Suite.SuiteClasses({
        MethodInvokerTest.正常系テスト.class,
        MethodInvokerTest.異常系テスト.class
})
public class MethodInvokerTest {

    public static class 正常系テスト {

        @Test public void 引数なしメソッドの実行() {
            Hoge sut = new Hoge();
            MethodInvoker method = new MethodInvoker(sut, "greeting");
            String actual = method.invokeAndReturn(String.class);
            assertThat(actual, is("Hi !!"));
        }

        @Test public void 引数ありメソッドの実行() {
            Hoge sut = new Hoge();
            MethodInvoker method = new MethodInvoker(sut, "greeting");
            method.arg("Tom", String.class);
            String actual = method.invokeAndReturn(String.class);
            assertThat(actual, is("Hi, Tom !!"));
        }

        @Test public void 複数引数ありメソッドの実行() {
            Hoge sut = new Hoge();
            MethodInvoker method = new MethodInvoker(sut, "greeting");
            method.args(new Object {"Tom", 3}, new Class {String.class, int.class});
            String actual = method.invokeAndReturn(String.class);
            assertThat(actual, is("Hi, Tom3 !!"));
        }
    }

    public static class 異常系テスト {

        @Test(expected = Exception.class)
        public void テスト対象メソッドが存在しない場合は例外を投げる() {
            Hoge sut = new Hoge();
            MethodInvoker method = new MethodInvoker(sut, "non Exists Method");
            method.invoke();
        }
    }
}

本当はこんなもの使わなくて済むのが一番良いのですが、ね。