1.はじめに
リフレクションとは
・実行時にクラスの構造(フィールド、メソッドなど)を動的に調査・操作できるJavaの機能
・通常はアクセスできないprivateなフィールドやメソッドにもアクセス可能
本記事の対象
・Java1.7をベースに開設
・Junit4を使用
・Apache Commons LangのFieldUtilsやMethodUtilsを活用
・標準リフレクションAPIとの比較
2.リフレクションの基礎
基本的なAPI
リフレクションでは以下のような基本APIを使用
// フィールドの取得
Field field = TestClass.class.getDeclaredField("fieldName");
// メソッドの取得
Method method = TestClass.class.getDeclaredMethod("methodName", String.class);
setAccessible(true)の役割
setAccessible(true)は、privateやprotectedなメンバーへのアクセス制限を解除するために使用
public class TargetClass {
private String privateValue = "テスト";
}
public class SampleTest {
@Test
public void test() throws Exception {
TargetClass obj = new TargetClass();
// プライベートフィールドにアクセス
Field field = TargetClass.class.getDeclaredField("privateValue");
field.setAccessible(true);
// 値を取得
String value = (String) field.get(obj);
}
3.フィールドの書き換え
3.1 インスタンス変数の書き換え
標準リフレクションでの実装
TargetClass target = new TargetClass();
// すべてのフィールドを取得して対象フィールドを探す
Field[] allFields = target.getClass().getDeclaredFields();
for (Field field : allFields) {
if ("privateField".equals(field.getName())) {
field.setAccessible(true);
field.set(target, "新しい値");
break;
}
}
または、フィールド名が分かっている場合:
Field field = target.getClass().getDeclaredField("privateField");
field.setAccessible(true);
field.set(target, "新しい値");
FieldUtilsを使った実装
Apache Commons LangのFieldUtilsを使うと、よりシンプルに書けます。
import org.apache.commons.lang.reflect.FieldUtils;
TargetClass target = new TargetClass();
// 第4引数のtrueはprivateフィールドへのアクセスを許可
FieldUtils.writeField(target, "privateField", "新しい値", true);
3.2 static変数の書き換え
標準リフレクションでの実装
Field staticField = TargetClass.class.getDeclaredField("staticField");
staticField.setAccessible(true);
staticField.set(null, "新しい値"); // staticフィールドなのでnullを指定
FieldUtilsを使った実装
// staticフィールドの場合、インスタンスの代わりにnullを渡す
FieldUtils.writeStaticField(TargetClass.class, "staticField", "新しい値", true);
4.メソッド呼び出しと例外ハンドリング
4.1 標準リフレクションでのメソッド呼び出し
getDeclaredMethod()とinvoke()
TargetClass target = new TargetClass();
// メソッドを取得(メソッド名と引数の型を指定)
Method method = target.getClass().getDeclaredMethod("methodName", String.class);
method.setAccessible(true);
// メソッドを実行
Object result = method.invoke(target, "引数の値");
private/protectedメソッドへのアクセス
privateやprotectedメソッドもsetAccessible(true)で呼び出し可能
// 引数なしのprivateメソッド
Method privateMethod = target.getClass().getDeclaredMethod("privateMethod");
privateMethod.setAccessible(true);
Object result = privateMethod.invoke(target);
// 複数引数のprotectedメソッド
Method protectedMethod = target.getClass()
.getDeclaredMethod("protectedMethod", String.class, int.class);
protectedMethod.setAccessible(true);
Object result = protectedMethod.invoke(target, "test", 123);
4.2 MethodUtilsを使ったメソッド呼び出し
publicメソッド専用という制約
MethodUtilsはpublicメソッドのみを対象
import org.apache.commons.lang.reflect.MethodUtils;
TargetClass target = new TargetClass();
// publicメソッドの呼び出し
Object result = MethodUtils.invokeMethod(target, "publicMethod", "引数の値");
標準リフレクションとの比較
// ❌ MethodUtilsではprivateメソッドを呼び出せない
try {
MethodUtils.invokeMethod(target, "privateMethod", "test");
} catch (NoSuchMethodException e) {
// privateメソッドは見つからない
}
// ✅ 標準リフレクションならprivateメソッドも呼び出せる
Method method = target.getClass().getDeclaredMethod("privateMethod", String.class);
method.setAccessible(true);
method.invoke(target, "test");
4.3 MethodUtilsの落とし穴(null引数問題)
引数にnullを渡すとNoSuchMethodExceptionが発生
MethodUtilsは引数の実際の型からシグニチャ(引数の型や引数の数のこと)を推論
→nullを渡すと型が特定できず、例外が発生
TargetClass target = new TargetClass();
// ❌ これはNoSuchMethodExceptionが発生
try {
MethodUtils.invokeMethod(target, "processValue", null);
} catch (NoSuchMethodException e) {
// nullの型が判定できないためメソッドが見つからない
}
異常系テストでは標準リフレクションが必要
nullを引数として渡す異常系テストでは、標準リフレクションを使う必要がある
// ✅ 標準リフレクションなら型を明示的に指定できる
Method method = target.getClass()
.getDeclaredMethod("processValue", String.class);
method.setAccessible(true);
// nullを渡してもOK(引数の型は既に指定済み)
try {
method.invoke(target, (Object) null);
fail("NullPointerExceptionが発生するはず");
} catch (Exception e) {
}
5.例外処理の注意点
5.1 InvocationTargetExceptionの扱い
メソッド内で発生した例外がラップされる
リフレクションで呼び出したメソッド内で例外が発生すると、
その例外はInvocationTargetExceptionに内包される
public class TargetClass {
private void validateData(String data) {
if (data == null) {
throw new IllegalArgumentException("データがnullです");
}
// 正常処理
System.out.println("データ: " + data);
}
}
public class TestClass {
@Test
public void test() throws Exception {
TargetClass obj = new TargetClass();
Method method = TargetClass.class.getDeclaredMethod("validateData", String.class);
method.setAccessible(true);
try {
// nullを渡して例外を発生させる
method.invoke(obj, (String) null);
} catch (IllegalArgumentException e) {
// ❌ ここではキャッチできない
}
}
正しいコード
public class TestClass {
@Test
public void test() throws Exception {
TargetClass obj = new TargetClass();
Method method = TargetClass.class.getDeclaredMethod("validateData", String.class);
method.setAccessible(true);
try {
// nullを渡して例外を発生させる
method.invoke(obj, (String) null);
} catch (InvocationTargetException e) {
// ✅ リフレクション実行時はInvocationTargetExceptionに内包
// 元の例外を取得
Throwable cause = e.getCause();
// 元の例外が IllegalArgumentException かどうか検証
assertTrue(cause instanceof IllegalArgumentException);
assertEquals("データがnullです", cause.getMessage());
}
}
6.テストでの実践パターン
6.1 テストクラスでのoverride
テスト対象クラスを継承し、特定のメソッドで例外を発生させるテストパターン
テスト対象クラスを継承
public class TargetClass {
/**
* メイン処理:現在日時を返却
*/
public String processWithCurrentDate() {
Date currentDate = getCurrentDate();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String dateStr = sdf.format(currentDate);
return dateStr;
}
/**
* 現在日時を取得
*/
protected Date getCurrentDate() {
return new Date();
}
}
public class TestClass {
// テスト用のインナークラスを定義
private static class TestInstance extends TargetClass {
private boolean exceptionFlg = false;
public void setExceptionFlg(boolean flg) {
this.exceptionFlg = flg;
}
@Override
protected Date getCurrentDate() {
// TargetClassのgetCurrentDateは実行されずに、このメソッドが実行される
if (exceptionFlg) {
throw new RuntimeException("テスト用例外です");
}
// flgが無効のときは元のメソッドを実行
return super.getCurrentDate();
}
}
@Test
public void test() throws Exception {
TestInstance testInstance = new TestInstance();
// 例外フラグを有効に設定
testInstance.setExceptionFlg(true);
try{
// テスト実行
MethodUtils.invokeMethod(testInstance, "processWithCurrentDate");
} catch (InvocationTargetException e) {
Throwable cause = e.getCause();
// 元の例外が IllegalArgumentException かどうか検証
assertTrue(cause instanceof RuntimeException);
assertEquals("テスト用例外です", cause.getMessage());
}
}
最後に
本記事は、今年参画したプロジェクトでの経験をもとにまとめました。
約1000件のテストメソッドを設計・実装するという、
キャリアの中でも過去最大規模の単体テストフェーズでした。
プロジェクトではJUnit 4を使用していたため、
本記事で紹介したような標準リフレクションやApache Commons Langのユーティリティを活用する必要がありました。
正直なところ、JUnit 5であれば、もっとスマートな実現方法がいくつもあったはずで、当時はもやもやしていました。
とはいえ、この経験を通じてリフレクションAPIの深い部分まで理解でき、
標準APIとユーティリティライブラリの使い分けや、それぞれの制約・落とし穴についても実践的な知見を得ることができました。
いつか、JUnit 5についての内容もまとめたいと思います。