テストコードを書く時に困るのが、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 class FieldTest { public String publicString = "Foobar"; private String privateString = "Hello, World!"; }
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 個
当然コンパイルエラーとなる。
java.lang.Classにはクラスに定義されているフィールドを java.lang.reflection.Field インスタンスとして取得可能なメソッドが用意されている。
上の二つのメソッドは、そのクラスおよび継承元のクラスのpublicなフィールドのみを取得できる。
継承元は無視し、そのクラス本体のpublic以外も含めて取得したい場合は次のメソッドを使う。
まずは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)
続いて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インスタンス迄である。実際にそのフィールド値を取得するにはもう少し作業が必要である。
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)
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
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)"というメッセージが表示され、フィールド値の取得に失敗した事が分かる。
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クラスのインスタンスになる点に注意する。
ここまでの実験を踏まえ、実際に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)
上記例で作った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用であり、一般的な業務ロジックには使わないよう注意する。