home ホーム search 検索 -  login ログイン  | reload edit datainfo version cmd icon diff delete  | help ヘルプ

技術/TDD/JavaでUnitTestでprivateメンバにアクセスしたい場合

技術/TDD/JavaでUnitTestでprivateメンバにアクセスしたい場合

技術 / TDD / JavaでUnitTestでprivateメンバにアクセスしたい場合
id: 449 所有者: msakamoto-sf    作成日: 2009-10-03 15:44:05
カテゴリ: Java TDD プログラミング 

テストコードを書く時に困るのが、privateなメンバをテストしたい場面である。
そもそもprivateなメンバをテストコードでテストする必要があるのか、テストしたいのならprivateではなく別のクラスに移すべきではないのか、という意見はひとまずおいておく。
ここでは、下記記事で紹介されている、Javaでprivateなメンバを外部からreflectionを使ってアクセスする手法を例によって抜き書きしてまとめておく。

この記事はJUnitのFAQ "How do I test protected methods?" で触れられている。2003年後半の記事なので随分と昔であり、今更個人のBlogでメモ書きするまでも無いが、自分用のメモとしてまとめておく。
なお以下のJavaコードをコンパイル・実行したのは WinXP(SP2), JavaSDKは 1.6.0_12 を使っている。
最後の方にJUnitが出てくるが、こちらはEclipse3.2に入っていたjunit-4.1.jarを使った。


まずはpublicとprivateメンバを持つクラスを用意しておく。

シンプルなもので良いので、実験対象となるクラスを用意する。

public class FieldTest {
    public String publicString = "Foobar";
    private String privateString = "Hello, World!";
}

Test1.java : まずは普通にprivateメンバにアクセス→当然コンパイルエラー

public class Test1 {
  public static void main(String args[]) {
    System.out.println(new FieldTest().publicString);
    System.out.println(new FieldTest().privateString);
  }
}
DOS> javac Test1.java
Test1.java:4: privateString は FieldTest で private アクセスされます。
    System.out.println(new FieldTest().privateString);
                                      ^
エラー 1 個

当然コンパイルエラーとなる。

Test2.java : ClassオブジェクトのgetField()に挑戦→NoSuchFieldException発生

java.lang.Classにはクラスに定義されているフィールドを java.lang.reflection.Field インスタンスとして取得可能なメソッドが用意されている。

  • getField(String name) : Fieldのインスタンスを返す
  • getFields() : Fieldの配列で返す

上の二つのメソッドは、そのクラスおよび継承元のクラスのpublicなフィールドのみを取得できる。
継承元は無視し、そのクラス本体のpublic以外も含めて取得したい場合は次のメソッドを使う。

  • getDeclaredField(String name) :
  • getDeclaredFields() : Fieldの配列で返す

まずはgetField()を試してみる。getField()の場合はprivateフィールドは取得できない筈である。

import java.lang.reflect.Field;
 
public class Test2 {
    public static void main(String args[]) throws Exception {
        Field f;
 
        f = FieldTest.class.getField("publicString");
        System.out.println("Public Field: " + f);
 
        f = FieldTest.class.getField("privateString");
        System.out.println("Private Field: " + f);
    }
}

コンパイルは成功するが、実行すると"privateString"へのアクセスのところでNoSuchFieldExceptionが発生する。

DOS> java Test2
Public Field: public java.lang.String FieldTest.publicString
Exception in thread "main" java.lang.NoSuchFieldException: privateString
        at java.lang.Class.getField(Class.java:1520)
        at Test2.main(Test2.java:10)

Test3.java, Test4.java : getFields()/getDeclaredFields()に挑戦→ようやくprivateメンバにアクセス出来た

続いてFieldインスタンスの配列を返すメソッドを試してみる。まずgetFields()を試してみるが、これもprivateは取得できない筈である。

import java.lang.reflect.Field;
 
public class Test3 {
    public static void main(String args[]) {
        final Field fields[] = FieldTest.class.getFields();
        for (int i = 0; i < fields.length; i++) {
            System.out.println("Field: " + fields[i]);
        }
    }
}

実行してみるとpublicStringフィールドしか取得できていない事が確認できる。

DOS> java Test3
Field: public java.lang.String FieldTest.publicString

いよいよprivateも取得できるgetDeclaredFields()を使ってみる。

import java.lang.reflect.Field;
 
public class Test4 {
    public static void main(String args[]) {
        final Field fields[] = FieldTest.class.getDeclaredFields();
        for (int i = 0; i < fields.length; i++) {
            System.out.println("Field: " + fields[i]);
        }
    }
}

実行してみると、privateフィールドも取得できている。

DOS> java Test4
Field: public java.lang.String FieldTest.publicString
Field: private java.lang.String FieldTest.privateString

しかし、ここで取得できたのはあくまでもFieldインスタンス迄である。実際にそのフィールド値を取得するにはもう少し作業が必要である。

Test5.java : フィールド値を取得しようとするとIllegalAccessException発生

Fieldクラスには get(Object o) メソッドが用意されており、実際にインスタンスからフィールド値を取り出す時に使う。

import java.lang.reflect.Field;
 
public class Test5 {
    public static void main(String args[]) throws Exception {
        final Field fields[] = FieldTest.class.getDeclaredFields();
        for (int i = 0; i < fields.length; i++) {
            if ("privateString".equals(fields[i].getName())) {
                System.out.println(fields[i].get(new FieldTest()));
                break;
            }
        }
    }
}

しかし、実行してみるとIllegalAccessExceptionが発生してしまう。

DOS> java Test5
Exception in thread "main" java.lang.IllegalAccessException: \
Class Test5 can not access a member of class FieldTest with modifiers "private"

        at sun.reflect.Reflection.ensureMemberAccess(Reflection.java:65)
        at java.lang.reflect.Field.doSecurityCheck(Field.java:960)
        at java.lang.reflect.Field.getFieldAccessor(Field.java:896)
        at java.lang.reflect.Field.get(Field.java:358)
        at Test5.main(Test5.java:8)

Test6.java, Test7.java : AccessibleObjectのsetAccessible()を使ってようやく取得成功

Test5.javaでIllegalAccessExceptionが発生したのは、Java言語のアクセス制御チェックがprivateなのでアクセス不可能と判定したからである。
java.lang.reflectパッケージには Field の他に Constructor, Method というクラスが用意されているが、これらは java.lang.reflect.AccessibleObject というクラスを基底クラスとしている。
そして AccessibleObject クラスには、対象Field/Constructor/Methodに対するアクセス制御チェックの有効/無効を設定する setAccessible(boolean) というメソッドがある。
setAccessible(true)とすることで、アクセス制御チェックを抑制し、privateなメンバも外部からアクセスできるようになる。

import java.lang.reflect.Field;
 
public class Test6 {
    public static void main(String args[]) throws Exception {
        final Field fields[] = FieldTest.class.getDeclaredFields();
        for (int i = 0; i < fields.length; i++) {
            if ("privateString".equals(fields[i].getName())) {
                fields[i].setAccessible(true);
                System.out.println(fields[i].get(new FieldTest()));
                break;
            }
        }
    }
}

ようやくFieldのget()メソッドで値を取得できるようになった。

DOS> java Test6
Hello, World!

続いてFieldクラスのset()メソッドを使って、privateフィールドの値を更新してみる。

import java.lang.reflect.Field;
 
public class Test7 {
    public static void main(String args[]) throws Exception {
        final Field fields[] = FieldTest.class.getDeclaredFields();
        for (int i = 0; i < fields.length; i++) {
            if ("privateString".equals(fields[i].getName())) {
                FieldTest fieldTest = new FieldTest();
                Field f = fields[i];
 
                f.setAccessible(true);
                System.out.println(f.get(fieldTest));
 
                f.set(fieldTest, "Modified Field"); // 値を更新
                System.out.println(f.get(fieldTest));
                break;
            }
        }
    }
}

実行してみると、ちゃんと値が更新された事が確認できた。

DOS> java Test7
Hello, World!
Modified Field

Test8.java : AppletなどJavaの実行環境によっては使えない場合も。

AppletなどJavaの実行環境によってはセキュリティマネージャが有効になり、AccessibleObjectのsetAccessible()が使えなくなっている場合もある。

import java.lang.reflect.Field;
import java.applet.Applet;
import java.awt.Label;
 
public class Test8 extends Applet {
    public Test8() {
        super();
        String s = "Field not found";
        try {
            final Field fields[] = FieldTest.class.getDeclaredFields();
            for (int i = 0; i < fields.length; ++i) {
                if ("privateString".equals(fields[i].getName())) {
                    fields[i].setAccessible(true);
                    s = (String)fields[i].get(new FieldTest());
                    break;
                }
            }
        } catch (Exception ex) {
            s = ex.getMessage();
        }
        add(new Label(s));
    }
}

test8.html :

<html>
  <body>
    <applet width="100" height="50" code="Test8.class">
    </applet>
  </body>
</html>
DOS> appletviewer test8.html

→ "access denied (java.lang.reflect.ReflectPermission suppressAccessChecks)"というメッセージが表示され、フィールド値の取得に失敗した事が分かる。

Test9.java : コンストラクタ・メンバに対しても同様の手法が可能

java.lang.reflect.{Filed|Method|Constructor} はいずれもAccessibleObjectから派生している。よってMethod/Constructorについても同様にsetAccessible(true)を使う事が可能である。
Test9.javaでprivateメソッドを外部から呼び出す例を示す。

import java.lang.reflect.Method;
 
class MethodTest {
    private final String sayHello(final String name) {
        return "Hello, " + name;
    }
}
 
public class Test9 {
    public static void main(String args[]) throws Exception {
        MethodTest test = new MethodTest();
        final Method[] methods = test.getClass().getDeclaredMethods();
        for (int i = 0; i < methods.length; i++) {
            if (methods[i].getName().equals("sayHello")) {
                final Object params[] = {"Ross"};
                methods[i].setAccessible(true);
                Object ret = methods[i].invoke(test, params);
                System.out.println(ret);
            }
        }
    }
}
DOS> java Test9
Hello, Ross

Methodの場合は上記例のように invoke() で実行できる。なおメソッドの引数と戻り値がint/floatなどのプリミティブ型で宣言されていた場合、invoke()で指定する引数およびその戻り値は、対応するInteger/Floatクラスのインスタンスになる点に注意する。

Test10.java, Test11.java : JUnitのテストコードで使ってみる

ここまでの実験を踏まえ、実際にJUnitのテストコードでprivateフィールドにアクセスしてassertしてみる。

まずは普通にpublicフィールドのみをassertするTestCaseを作ってみる(Test10.java)。

import java.lang.reflect.Field;
import junit.framework.TestCase;
 
public class Test10 extends TestCase {
    public Test10(final String name) {
        super(name);
    }
 
    public void test_reflectin() throws Exception {
        FieldTest f = new FieldTest();
        assertEquals(f.publicString, "Foobar");
    }
 
    public static void main(String args[]) {
        junit.textui.TestRunner.run(Test10.class);
    }
}
DOS> java -cp .;junit-4.1.jar Test10
.
Time: 0.015

OK (1 test)

続いてprivateフィールドにアクセスするTestCaseを作ってみるが、その前に毎回Fieldクラスの取得やsetAccessible(true)を呼ぶのは手間なので、ラップするクラスを作ってみる。
PrivateAccessor.java :

import java.lang.reflect.Field;
import junit.framework.Assert;
 
public class PrivateAccessor {
    public static Object getPrivateField(Object o, String fieldName) {
        Assert.assertNotNull(o);
        Assert.assertNotNull(fieldName);
        final Field fields[] = o.getClass().getDeclaredFields();
        for (int i = 0; i < fields.length; i++) {
            if (fieldName.equals(fields[i].getName())) {
                try {
                    fields[i].setAccessible(true);
                    return fields[i].get(o);
                } catch (IllegalAccessException ex) {
                    Assert.fail("IllegalAccessException accessing " + fieldName);
                }
            }
        }
        Assert.fail("Field '" + fieldName + "' not found");
        return null;
    }
}

これを使ってTest11.javaでprivateフィールドをassertしてみる。

import java.lang.reflect.Field;
import junit.framework.TestCase;
 
public class Test11 extends TestCase {
    public Test11(final String name) {
        super(name);
    }
 
    public void test_reflection() throws Exception {
        FieldTest f = new FieldTest();
        assertNotNull(f); // 一応not null確認
        assertEquals(f.publicString, "Foobar"); // publicフィールド確認
 
        String s = (String)PrivateAccessor.getPrivateField(f, "privateString");
        assertEquals(s, "Hello, World!");
    }
 
    public static void main(String args[]) {
        junit.textui.TestRunner.run(Test11.class);
    }
}
DOS> java -cp .;junit-4.1.jar Test11
.
Time: 0

OK (1 test)

Test12.java : PrivilegedAccessorを使った例

上記例で作ったPrivateAccessorクラスだが、これにMethodとConstructorに対応させた"PrivilegedAccessor"というライブラリがSourceForge上で公開されている。

これを使って、private/protectedなフィールド・メソッド・コンストラクタを一通り呼び出してみる。まずテスト対象となるクラスを用意する。
HiddenClass.java :

public class HiddenClass {
    private String privateHello;
    protected String protectedHello;
 
    private HiddenClass(String s) {
        this.privateHello = "private: " + s;
        this.protectedHello = "protected: " + s;
    }
 
    private String getPrivateHello() {
        return this.privateHello;
    }
    private void setPrivateHello(String s) {
        this.privateHello = s;
    }
 
    protected String getProtectedHello() {
        return this.protectedHello;
    }
    protected void setProtectedHello(String s) {
        this.protectedHello = s;
    }
}

テストケースを作成する。

import junit.extensions.PA;
import junit.framework.TestCase;
 
public class Test12 extends TestCase {
    public static void main(String args[]) {
        junit.textui.TestRunner.run(Test12.class);
    }
    public Test12(final String name) { super(name); }
 
    public void test_reflection() throws Exception {
 
        // call private constructor
        HiddenClass o = (HiddenClass)PA.instantiate(
                HiddenClass.class,
                new Object[] { "Hello, World" }
                );
        assertNotNull(o);
        assertTrue(o instanceof HiddenClass);
 
        // get access private/protected fields
        assertEquals("private: Hello, World",
                (String)PA.getValue(o, "privateHello"));
        assertEquals("protected: Hello, World",
                (String)PA.getValue(o, "protectedHello"));
 
        // set access private/protected fields
        String new_private = "new Private";
        String new_protected = "new Protected";
        PA.setValue(o, "privateHello", new_private);
        PA.setValue(o, "protectedHello", new_protected);
        assertEquals(new_private, (String)PA.getValue(o, "privateHello"));
        assertEquals(new_protected, (String)PA.getValue(o, "protectedHello"));
 
        // call private/protected methods (no args)
        assertEquals(new_private,
                (String)PA.invokeMethod(o, "getPrivateHello()"));
        assertEquals(new_protected,
                (String)PA.invokeMethod(o, "getProtectedHello()"));
 
        // call private/protected methods (with args)
        new_private = "new Private(2)";
        new_protected = "new Protected(2)";
        PA.invokeMethod(o, "setPrivateHello(java.lang.String)", 
                new Object[]{new_private});
        PA.invokeMethod(o, "setProtectedHello(java.lang.String)", 
                new Object[]{new_protected});
        assertEquals(new_private,
                (String)PA.invokeMethod(o, "getPrivateHello()"));
        assertEquals(new_protected,
                (String)PA.invokeMethod(o, "getProtectedHello()"));
    }
}

コンパイルし、実行する。今回は"privilegedAccessor5.0_1.0.2.jar"を使用した。

> javac -classpath .;junit-4.1.jar;privilegedAccessor5.0_1.0.2.jar Test12.java
> java -classpath .;junit-4.1.jar;privilegedAccessor5.0_1.0.2.jar Test12
.
Time: 0.016

OK (1 test)

このようにprotected/privateなField/Method/Constructorを一通り呼び出す事が出来る。
ただしメソッドの場合、invokeMethod()で指定するメソッド名に"(引数の型1, 型2, ...)"と付けなければならないのが独特で注意が必要である。

まとめ

テストコードでprivateなメンバにアクセスする場合、java.lang.reflect.Field/Method/Constructorを用いる事が出来る。その場合、setAccessible(true)でアクセス制御チェックを抑制しておく事。
実際にテストコードを作成する場合は、PrivilegedAccessor"を用いる事で簡潔に表記する事が可能である。
ただしこの手法はあくまでもUnitTest用であり、一般的な業務ロジックには使わないよう注意する。


プレーンテキスト形式でダウンロード
現在のバージョン : 1
更新者: msakamoto-sf
更新日: 2009-10-03 15:50:26
md5:b4aee528e5e9fec973ee57e1fb0f2501
sha1:f467757563dcbec9af7aa756ddc621d973e08c95
コメント
コメントを投稿するにはログインして下さい。