private変数を参照・設定する
以前、JUnitでprivateメソッドのテストを行う という記事を書きました。
要は、**privateメソッドもJUnitから直接テストできるぜ!だからお願いだからJUnitののためにアクセス修飾子を何でもpublicにするとかそもそも付けないとか愚かなことはやめてくれ!**っていう主張のためなんですけど、それの派生みたいなものです。
飾りじゃないのよアクセス修飾子はハッハーン。
意味を考えて付けてよホッホー。
アクセス修飾子の意図がテスト用としか思えないのはクソコードだし、害悪でしかない。
そういうプロジェクトのコードと設計書は信用度が低いと判断してすべてを疑ってかかる必要性が出てきて、大体上の見積もりより実際の工数が増えるんだ。(経験談)
モックライブラリ使えばリフレクションなんて使わずにもっと楽に書ける(MockitoのWhiteboxとか)んですけど、モックライブラリの導入が許されないプロジェクトにぶち当たると軽率にアクセス修飾子を外す人が存在するので、この記事を書きました。
やめろ!やめてくれ!テストのために誤ったクソコードを書くのはやめるんだ!まじで!!
サンプルコードと実例
テスト対象のクラスが持つprivate変数を別のJUnitクラスから設定・参照します。
サンプルコードはJavadocやなんやかんやを省略しています。
コピペで動くように動作確認はしていますが、あくまでサンプルコード、参考です。
テスト対象のクラス
コンストラクタは省略しています。
基本的にprivate変数とprivate変数を参照するgetterを持つだけ。
package test.variable;
import java.util.Objects;
public class Sample {
/** 1.privateインスタンス変数 */
private String strValue = null;
/** 2.privateクラス変数 */
private static String strValueStatic = null;
private InnerDynamic innerDynamic = null;
private static InnerStatic innerStatic = new InnerStatic();
public String getStrValue() {
return this.strValue;
}
public static String getStrValueStatic() {
return strValueStatic;
}
public String getDynamicInnerPrivate() {
if (Objects.isNull(this.innerDynamic)) {
this.innerDynamic = new InnerDynamic();
}
return this.innerDynamic.getIStrValue();
}
public static String getStaticInnerPrivate() {
return innerStatic.getIStrValue();
}
private class InnerDynamic {
/** 3.動的privateクラスのインスタンス変数 */
private String iStrValue = null;
public String getIStrValue() {
return this.iStrValue;
}
}
private static class InnerStatic {
/** 4.静的privateクラスのインスタンス変数 */
private String iStrValue = null;
public String getIStrValue() {
return this.iStrValue;
}
}
}
1.privateインスタンス変数の設定・参照
staticではないので、インスタンスが必要なタイプ。
特定のインスタンスのprivate変数を参照・設定できるようにします。
package test.variable;
import static org.junit.Assert.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import org.junit.Test;
public class SampleTest {
/** 1.privateインスタンス変数 */
@Test
public void test_private_field() throws Exception {
String testValue = "test";
Sample testee = new Sample();
// 初期値の確認
assertNull(testee.getStrValue());
// private変数のフィールドを取得
Field field = testee.getClass().getDeclaredField("strValue");
// private変数へのアクセス制限を解除
field.setAccessible(true);
// private変数に値を設定
field.set(testee, testValue);
// private変数を確認
String value = String.valueOf(field.get(testee));
assertEquals(testValue, value);
assertEquals(value, testee.getStrValue());
}
}
2.privateクラス変数の設定・参照
staticの場合、特定インスタンスの中身を変えるわけではないので、クラスオブジェクトをインスタンスから取得しないとか、値の設定時に第一引数をnullにしたりします。
package test.variable;
import static org.junit.Assert.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import org.junit.Test;
public class SampleTest {
/** 2.privateクラス変数 */
@Test
public void test_private_static_field() throws Exception {
String testValue = "test";
// 初期値の確認
assertNull(Sample.getStrValueStatic());
// private変数のフィールドを取得
Field field = Sample.class.getDeclaredField("strValueStatic");
// private変数へのアクセス制限を解除
field.setAccessible(true);
// private変数に値を設定
field.set(null, testValue);
// private変数を確認
String value = String.valueOf(field.get(null));
assertEquals(testValue, value);
assertEquals(value, Sample.getStrValueStatic());
}
}
3.動的privateクラスのインスタンス変数の設定・参照
インナークラスの場合、外のクラスからはそれ自体が不可視なので、クラスローダー経由でコンストラクタを取得します。
取得するときは、完全修飾名の外側のクラス$インナークラス
を指定してください。
インナークラスのオブジェクトを取得した後は、前述と同様にフィールドを取得し、フィールドのアクセス制限を解除します。
package test.variable;
import static org.junit.Assert.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import org.junit.Test;
public class SampleTest {
/** 3.動的privateクラスのインスタンス変数 */
@Test
public void test_dynamic_inner_class_private_field() throws Exception {
String testValue = "test";
Sample testee = new Sample();
// 初期値の確認
assertNull(testee.getDynamicInnerPrivate());
// インナークラスのオブジェクトを取得
Class innerClazz = Class.forName("test.variable.Sample$InnerDynamic");
Constructor constructor = innerClazz.getDeclaredConstructor(new Class[]{Sample.class});
constructor.setAccessible(true);
Object inner = constructor.newInstance(testee);
// インナークラスのprivate変数のフィールドを取得
Field field = inner.getClass().getDeclaredField("iStrValue");
field.setAccessible(true);
field.set(inner, testValue);
// 外側のクラスのインナークラスを保持するフィールドを取得
Field outerFld = testee.getClass().getDeclaredField("innerDynamic");
outerFld.setAccessible(true);
outerFld.set(testee, inner);
// private変数を確認
String value = String.valueOf(field.get(inner));
assertEquals(testValue, value);
assertEquals(value, testee.getDynamicInnerPrivate());
}
コンストラクタが引数を取る場合
インナークラスのコンストラクタに引数がある場合、第一引数は外側のクラスの方やオブジェクトになるので、第二引数以降がインナークラスのコンストラクタの引数となります。
// コンストラクタの取得
Constructor constructor =
innerClazz.getDeclaredConstructor(new Class[]{Sample.class, String.class});
// オブジェクトの取得
Object inner = constructor.newInstance(testee, "constructor");
4.静的privateクラスのインスタンス変数の設定・参照
動的インナークラスと同様に、まずはコンストラクタを取得します。
取得するときは、完全修飾名の外側のクラス$インナークラス
を指定してください。
静的と動的のインナークラスで違うのは、取得時に渡す第一引数に外側のクラスを指定しているところです。
package test.variable;
import static org.junit.Assert.*;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import org.junit.Test;
public class SampleTest {
/** 4.静的privateクラスのインスタンス変数 */
@Test
public void test_static_inner_class_private_field() throws Exception {
String testValue = "test";
// 初期値の確認
assertNull(Sample.getStaticInnerPrivate());
// インナークラスのオブジェクトを取得
Class innerClazz = Class.forName("test.variable.Sample$InnerStatic");
Constructor constructor = innerClazz.getDeclaredConstructor();
constructor.setAccessible(true);
Object inner = constructor.newInstance();
// インナークラスのprivate変数のフィールドを取得
Field field = inner.getClass().getDeclaredField("iStrValue");
field.setAccessible(true);
field.set(inner, testValue);
// 外側のクラスのインナークラスを保持するフィールドを取得
Field outerFld = Sample.class.getDeclaredField("innerStatic");
outerFld.setAccessible(true);
outerFld.set(null, inner);
// private変数を確認
String value = String.valueOf(field.get(inner));
assertEquals(testValue, value);
assertEquals(value, Sample.getStaticInnerPrivate());
}
}
コンストラクタが引数を取る場合
例では引数のないコンストラクタを呼んでいますが、InnerStatic(String str)
みたいなコンストラクタの場合、取得時に引数の型と値を指定します。
// コンストラクタの取得
Constructor constructor = innerClazz.getDeclaredConstructor(new Class[]{String.class});
// オブジェクトの取得
Object inner = constructor.newInstance("constructor");
最後に
まあ、テストだからこんな風にリフレクション使って不可視のフィールドやらメソッド呼んでますけど、呼べるからと言ってテスト対象の方でリフレクション使うのはどうよ、と思ってます。
プログラム本体のソースでリフレクション使うことってあまりないことだと思うので、もし使うようなら設計から見直したほうがいい場合かも。(フレームワーク自体を作ってるとかなら別ですが)
よくわからないなら、周りの詳しそうな人に相談してみてください。
まともにコードレビューされることのない環境の方は特に。
おまけ
そういや転職活動中なのですが、(新人に技術指導しながらでも)自分でコード書ける職場を探しています。
マネジメント向いてないタイプの人間なので、コード書かせてくれるいい職場があったら連絡ください。