JUnitでprivateメソッドのテストをする場合に注意すること

JUnitでどうしてもprivateメソッドをテストしなければならない時のために。
実装方法を定期的に忘れるので、一回しっかり記事で書けば忘れないだろう。という動機で書き残しておきます。

一般的なケース

以下のようなプライベートメソッドを持つクラスをテスト対象に考えます。

public class MyClass {
    private String introduceMyself(String name){
        return "my name is " + name + ".";
    }
}

MyClassのprivateなintroduceMyselfメソッドに対するテストは、以下のようにリフレクションを使うことで行えます。

import com.example.MyClass;
import org.junit.jupiter.api.Test;
import java.lang.reflect.Method;
import static org.assertj.core.api.Assertions.assertThat;

public class MyClassTest {
    @Test
    void testIntroduceMyself01() throws Exception {
        MyClass targetClass = new MyClass();
        // テスト対象メソッドの情報を取得する
        Method method = MyClass.class.getDeclaredMethod("introduceMyself", String.class);
        // メソッドをアクセス制限を解除
        method.setAccessible(true);
        // メソッド呼び出し
        String selfIntroduction = (String) method.invoke(targetClass,"John");
        // 結果をアサーション
        assertThat(selfIntroduction).isEqualTo("my name is John.");
    }
}

privateメソッドがスローする例外のアサーション

正常系のテストケースは上記で問題ないのですが、privateメソッドから例外がスローされるケースは注意が必要です。

Method#invoke内で発生した例外は、InvocationTargetExceptionクラスにラップされてしまいます。その為、原因例外を取り出してからアサーションする必要があります。

public class MyClass {
    private String introduceMyself(String name){
        // 引数が空文字の場合は例外をスローする
        if(name.isEmpty()){
            throw new IllegalArgumentException("argument must not be empty.");
        }
        return "my name is " + name + ".";
    }
}
public class MyClassTest {
    @Test
    void testIntroduceMyself02() throws Exception {
        MyClass targetClass = new MyClass();
        Method method = MyClass.class.getDeclaredMethod("introduceMyself", String.class);
        method.setAccessible(true);

        try {
            method.invoke(targetClass, "");
            fail("this code should not has been reached.");
        }catch (InvocationTargetException e) {
            // getCauseで原因例外を取り出してからアサーション
            assertThat(e.getCause()).isInstanceOf(IllegalArgumentException.class);
        }
    }
}

privateメソッドの引数にnullを渡す

リフレクションで呼び出すメソッドの引数としてnullを渡したい場合は、nullを要素として持つObject配列を渡す必要がある為、こちらも注意が必要です。

method.invoke(targetClass, new Object[]{null});
// これではダメ
method.invoke(targetClass, null);

そもそもprivateメソッドを単独でテストすべきではない

ここまで書いといてあれですが、privateメソッドは原則としてユニットテストすべきではありません。

privateメソッドは、言ってみればpublic(protected)メソッドから切り出された内部処理です。内部処理を強引にリフレクションでテストすることで、テストコードが内部実装に依存することになり、リファクタリングや機能追加の際に障壁となります。

適切に設計されたprivateメソッドであれば、呼び出し元のメソッド経由で網羅性はテスト可能です。
仮にprivateメソッドを個別で呼び出さなければ通らない分岐があるとしたら、それは実質的にデッドコードですから、実装を見直したほうが良いでしょう。

まとめ

  • privateメソッドを呼び出すにはリフレクションが必要
  • 例外のテストは、InvocationTargetExceptionからgetCauseする事
  • invokeにnullを渡す場合は、nullを要素として持つObject配列を渡す
  • privateメソッドをテストする必要が出たら、設計を見直してみること
  • privateメソッドは、呼び出し元のメソッド経由でテストするのが原則
Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.