LoginSignup
10
16

More than 5 years have passed since last update.

【Java】JUnitでprivateメソッドのテストを行う

Last updated at Posted at 2018-12-18

privateメソッドをテストとして呼び出す

以前Javaを書くときに意識してほしいことにて、privateメソッドはリフレクションで呼び出せるため、テストのためだけにアクセス修飾子を削除するのはやめろと書きました。
なので、その一例を載せておきます。

なお、メソッドではなくフィールドの場合はこんな感じに書きます。
リフレクションでprivateな変数を参照・設定する

2018/12/26 「privateコンストラクタの呼び出し」追加

privateメソッド単位のテストの是非

「publicメソッドから呼び出してprivateメソッドのテストまで一貫して行うべきではないか」という意見があることは、存じ上げております。
ただ、プロジェクトによってはメソッド単位のテストを行う=privateメソッドも含む、そんな場合が多々あります。
ルールです。みんなそれに従ってテストコードを書いてます。破っても書き直せって言われるだけです。
ビジネスロジックでクラス内にpublicメソッドはひとつ、みたいな場合もあります。
そんなときはテストも大変です。privateメソッド単位の処理をテストで保証してから、publicメソッドのテスト書いたほうが楽です。
そんなわけでprivateメソッド単位のテストの是非については範囲外としています。


サンプルコードはJavadocやなんやかんやを省略しています。
コピペで動くように動作確認はしていますが、あくまでサンプルコード、参考です。

テスト対象のクラス

Sample.java
public class Sample {
    private String strValue = null;

    public Sample(String value) {
        this.strValue = (value == null) ? "" : value;
    }

    /** 5.privateコンストラクタの呼び出し */
    private Sample() {
    }

    /** 1.非staticメソッドの呼び出し */
    private boolean equals(String value) {
        return this.strValue.equals(value);
    }

    /** 2.staticメソッドの呼び出し */
    private static boolean isEmpty(String value) {
        return (value == null || "".equals(value)) ? true : false;
    }

    /** 3.戻り値・引数なしの呼び出し */
    private static void dispMessage() {
        System.out.println("Hello world!");
    }

    /** 4.例外の確認 */
    private void setValue(String value) {
        if (value == null || value.isEmpty()) {
            throw new IllegalArgumentException("argument is empty.");
        }
        this.strValue = value;
    }
}

1.非staticメソッドの呼び出し

doPrivateMethodがテスト対象のprivateメソッドを呼び出すためのメソッドです。
typesには、privateメソッドの引数の型を第一引数から順番に配列に格納して渡します。argsは引数そのものを同様に渡します。
テスト対象メソッド名、引数の型と数を指定しているので、リファクタリング等でその辺変更されるとテストは失敗します。(InvocationTargetExceptionが発生します)

SampleTest.java
import static org.junit.Assert.*;

import java.lang.reflect.Method;
import org.junit.Test;

public class SampleTest {
    @Test
    public void test_equals() throws Exception {
        Sample testee = new Sample("test");
        assertTrue((boolean) this.doPrivateMethod(
                testee, "equals", new Class[]{String.class}, new Object[]{"test"}));
    }

    /**
     * 非staticメソッド呼び出し.
     * 
     * @param obj テスト対象オブジェクト
     * @param name テスト対象メソッド名
     * @param types テスト対象メソッドの引数の型
     * @param args テスト対象メソッドの引数
     * @return 非staticメソッドの戻り値
     */
    private Object doPrivateMethod(
        Object obj, String name, Class[] types, Object[] args) throws Exception {
        // テスト対象メソッドの情報を取得
        Method method = obj.getClass().getDeclaredMethod(name, types);
        // テスト対象メソッドへのアクセス制限を解除
        method.setAccessible(true);
        // テスト対象メソッド呼び出し
        return method.invoke(obj, args);
    }
}

2.staticメソッドの呼び出し

doStaticPrivateMethodがテスト対象のprivateメソッドを呼び出すためのメソッドです。
リファクタリングに弱いのは、1.と一緒。
staticメソッドの呼び出しなので、引数にオブジェクトを渡していません。

SampleTest.java
import static org.junit.Assert.*;

import java.lang.reflect.Method;
import org.junit.Test;

public class SampleTest {
    @Test
    public void test_isEmpty() throws Exception {
        assertTrue((boolean) this.doStaticPrivateMethod(
                "isEmpty", new Class[]{String.class}, new Object[]{null}));
    }

    /**
     * staticメソッド呼び出し.
     * 
     * @param name テスト対象メソッド名
     * @param types テスト対象メソッドの引数の型
     * @param args テスト対象メソッドの引数
     * @return staticメソッドの戻り値
     */
    private Object doStaticPrivateMethod(
        String name, Class[] types, Object[] args) throws Exception {
        // テスト対象メソッドの情報を取得
        Method method = Sample.class.getDeclaredMethod(name, types);
        // テスト対象メソッドへのアクセス制限を解除
        method.setAccessible(true);
        // テスト対象メソッド呼び出し
        return method.invoke(null, args);
    }
}

assertTrue((boolean) this.doStaticPrivateMethod("isEmpty", new Class[]{String.class}, new Object[]{null}));

引数があるメソッドでnullを渡したい場合、配列に入れてください。
引数が1つでも配列が必要です。そのまま渡すと失敗します。

3.戻り値・引数なしの呼び出し

テスト対象メソッドがvoidの場合、戻り値はありません。(拾おうとしてもnull)
引数がないメソッドの呼び出しの時は、nullを渡します。2.と違って配列じゃなくていいです。
※doStaticPrivateMethodは2.と同じなので省略しています。

SampleTest.java
import static org.junit.Assert.*;

import java.lang.reflect.Method;
import org.junit.Test;

public class SampleTest {
    @Test
    public void test_dispMessage() throws Exception {
        this.doStaticPrivateMethod("dispMessage", null, null);
    }
}

コメントより追記

this.doStaticPrivateMethod("dispMessage", null, null);

アサートかけてないのは呼び出し方法のサンプルだからです。
voidのメソッドのテストでも、処理結果に対しての確認は必須です。例えばDBやファイルを処理したならその結果にアサートかけます。
Sampleクラスの場合だとコンソールに出力された文字列が正しいかどうかを確認します。例えばこんな感じ。

// 標準出力結果のリダイレクト
ByteArrayOutputStream out = new ByteArrayOutputStream();
System.setOut(new PrintStream(out));
// テスト対象呼出し
this.doStaticPrivateMethod("dispMessage", null, null);
// アサート
assertEquals("Hello world!" + System.lineSeparator(),  out.toString());

サンプルを動かしたいときは、ByteArrayOutputStreamとPrintStreamをimportしてくださいね。

4.例外の確認

invoke実行時のエラーは、すべてInvocationTargetExceptionでラップされてスローされます。
なので、テスト対象でスローされた例外のチェックは、InvocationTargetExceptionでラップされた中身を取り出して行います。
また、例外がスローされることを確認するテストの場合、例外がスローしなかった場合、意図する例外以外の例外が発生した場合にJUnit上でテスト失敗とするため、明示的にfail();を呼び出します。
※doPrivateMethodは1.と同じなので省略しています。

SampleTest.java
import static org.junit.Assert.*;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import org.junit.Test;

public class SampleTest {
    @Test
    public void test_setValue() throws Exception {
        try {
            Sample testee = new Sample("test");
            this.doPrivateMethod(
                    testee, "setValue", new Class[]{String.class}, new Object[]{""});
            // テスト失敗
            fail();
        } catch (InvocationTargetException e) {
            // 例外の型チェック
            assertTrue(e.getCause() instanceof IllegalArgumentException);
            // 例外のメッセージチェック
            assertEquals("argument is empty.", e.getCause().getMessage());
        } catch (Exception e) {
            // テスト失敗
            fail(e.getMessage());
        }
    }
}

5.privateコンストラクタの呼び出し

staticクラスで外から呼び出されないようにするために引数なしのprivateコンストラクタ書いたりするじゃないですか。
あれのテスト方法です。「カバレッジ100%」が条件のプロジェクトとかで使ったりします。
コメントに「クラス名は完全修飾名で記述する」ってありますけど、今回のSampleクラスは、パッケージ切ってないのでクラス名だけ記述してます。
普通はパッケージあると思うので、完全修飾名じゃないと動かないです。

SampleTest.java
import static org.hamcrest.CoreMatchers.instanceOf;
import static org.junit.Assert.*;

import java.lang.reflect.Constructor;
import org.junit.Test;

public class SampleTest {
    @Test
    public void test_private_constructor() {
        try {
            // クラス名は完全修飾名で記述する
            Class<?> clazz = Class.forName("Sample");
            Constructor<?>[] constructors = clazz.getDeclaredConstructors();
            constructors[1].setAccessible(true);
            Object obj = constructors[1].newInstance();
            assertNotNull(obj);
            assertThat(obj, instanceOf(clazz));
        } catch (Exception e) {
            fail(e.getMessage());
        }
    }
}

constructors[1].setAccessible(true);

ここにはSampleクラスに記載した順番を0オリジンで記述します。
今回の場合、テストしたいprivateコンストラクタはSampleクラスのコンストラクタのうち、2番目に記述しているので、「1」になります。
テスト対象のコンストラクタしかない場合は、「0」でいいです。


それではよいJavaライフを!
また何かあったら追記とか別記事で書きます。

10
16
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
16