単体テスト、リフレクション、アノテーション、動的プロキシ

一、単体テスト

1.1 単体テストのクイックスタート

単体テストとは、最小の機能単位に対してテストコードを記述し、正しさを検証することです。

これまではどのようにテストしていましたか?例えば、学生管理システムを作り、学生の追加、変更、削除、検索などの機能をテストするには、mainメソッド内でコードを書いていました。

しかしmainメソッドでのテストには以下の問題があります。

テストをより便利にするため、サードパーティの企業や組織が優れたテストフレームワークを提供しています。ここではJunitテストフレームワークを紹介します。

Junitはサードパーティ製のオープンソースツールで、コードの単体テストに使用されます(IDEAには既にJunitフレームワークが統合されています)。mainメソッドでのテストと比較して以下の利点があります。

単体テストとは何かを理解したところで、実際に使ってみましょう。Junitはサードパーティ製なので、プロジェクトにjarファイルをインポートする必要があります。

まず、以下のようなStringUtilツールクラスを用意します。

public class StringUtil {
    public static void printNumber(String name) {
        System.out.println("名前の長さ:" + name.length());
    }
}

次に、StringUtilのメソッドが正しく動作するかをテストするクラスを作成します。

public class StringUtilTest {
    @Test
    public void testPrintNumber() {
        StringUtil.printNumber("admin");
        StringUtil.printNumber(null);
    }
}

コードを書くと、テストメソッドの左側に緑色の三角形のボタンが表示されます。このボタンをクリックするとテストメソッドが実行されます。

1.2 単体テストのアサーション

次に、単体テストのアサーションメカニズムを学びます。アサーションとは、プログラマがプログラムの実行結果を予測し、実際の結果が期待と一致するかを検査する仕組みです。

StringUtilクラスに新たなメソッドを追加します。

public static int getMaxIndex(String data) {
    if (data == null) {
        return -1;
    }
    return data.length();
}

次に、StringUtilTestクラスにテストメソッドを追加します。

public class StringUtilTest {
    @Test
    public void testGetMaxIndex() {
        int index1 = StringUtil.getMaxIndex(null);
        System.out.println(index1);
        
        int index2 = StringUtil.getMaxIndex("admin");
        System.out.println(index2);
        
        // アサーション:index2の結果を予測
        Assert.assertEquals("メソッドにバグがあります", 4, index2);
    }
}

テストを実行すると、期待値と実際の値が一致しない場合、以下のような結果になります。

1.3 Junitフレームワークのよく使われるアノテーション

これまで@Testアノテーションを学びました。これはメソッドをテストメソッドとしてマークし、テストを実行可能にします。

@Test以外にもいくつかのアノテーションがあり、それぞれがいつ実行されるか、どのようなシナリオで使われるかを把握する必要があります。

それでは他のアノテーションの使い方をデモします。StringUtilTestクラスにテストメソッドを追加します。

public class StringUtilTest {
    @Before
    public void beforeTest() {
        System.out.println("--> beforeTest Before 実行");
    }
    
    @BeforeClass
    public static void beforeClassTest() {
        System.out.println("--> beforeClassTest BeforeClass 実行");
    }
    
    @After
    public void afterTest() {
        System.out.println("--> afterTest After 実行");
    }
    
    @AfterClass
    public static void afterClassTest() {
        System.out.println("--> afterClassTest AfterClass 実行");
    }
}

上記のテストクラスを実行すると、以下の特徴が観察できます。

1. @BeforeClassが付いたメソッドは、すべてのメソッドの前に一度実行される。
2. @AfterClassが付いたメソッドは、すべてのメソッドの後に一度実行される。
3. @Beforeが付いたメソッドは、各@Testメソッドの前に実行される。
4. @Afterが付いたメソッドは、各@Testメソッドの後に実行される。

これらのアノテーションが役立つ場面の例として、各テストメソッドでSocketオブジェクトを使い、使い終わったら閉じたい場合を考えます。以下のような構造でコードを設計できます。

public class StringUtilTest {
    private static Socket socket;
    
    @BeforeClass
    public static void initSocket() {
        socket = new Socket();
    }
    
    @AfterClass
    public static void closeSocket() throws Exception {
        socket.close();
    }
    
    @Test
    public void testSomething() {
        // socketを使用
    }
}

補足:上記のアノテーションはJunit4ベースです。Junit5ではアノテーションが更新されていますが、機能は同じです。

二、リフレクション

リフレクション技術を学びます。リフレクション、動的プロキシ、アノテーションは、実際の開発で頻繁に使うわけではありませんが、フレームワークの理解や開発のために重要です。

リフレクションとは、クラスのバイトコードをメモリにロードし、プログラムでそのクラスの構成要素(メンバ変数、メソッド、コンストラクタなど)を解析する技術です。

リフレクションの具体的な使い方:まずクラスを取得します。Javaではクラスもオブジェクトとして扱われ、バイトコードオブジェクトはClassクラスで表されます。バイトコードオブジェクトを取得したら、それを使ってクラスの構成要素を取得します。各構成要素もオブジェクトであり、メンバ変数はFieldクラス、メソッドはMethodクラス、コンストラクタはConstructorクラスで表されます。

2.1 クラスのバイトコードを取得する

リフレクションの第一歩:バイトコードをメモリにロードし、バイトコードオブジェクトを取得します。

public class TestClass {
    public static void main(String[] args) throws Exception {
        Class<?> c1 = Student.class;
        System.out.println(c1.getName());       // 完全修飾クラス名
        System.out.println(c1.getSimpleName()); // シンプルクラス名
        
        Class<?> c2 = Class.forName("com.example.Student");
        System.out.println(c1 == c2); // true
        
        Student s = new Student();
        Class<?> c3 = s.getClass();
        System.out.println(c2 == c3); // true
    }
}

2.2 クラスのコンストラクタを取得する

Classクラスが提供するメソッドを使ってコンストラクタを取得し、それを使ってオブジェクトを生成します。

メソッドの命名規則:

get: 取得
Declared: 付いていると任意の(privateを含む)ものを取得可能。付いていないとpublicのみ。
Constructor: コンストラクタ
s: 複数取得可能。なしは単一。

例としてCatクラスを考えます。

public class Cat {
    private String name;
    private int age;
    
    public Cat() {}
    
    private Cat(String name, int age) {}
}

すべてのコンストラクタを取得するテストコード:

public class ConstructorTest {
    @Test
    public void testGetConstructors() {
        Class<?> clazz = Cat.class;
        Constructor<?>[] constructors = clazz.getDeclaredConstructors();
        for (Constructor<?> cons : constructors) {
            System.out.println(cons.getName() + " ---> パラメータ数: " + cons.getParameterCount());
        }
    }
}

単一のコンストラクタを取得:

@Test
public void testGetConstructor() throws Exception {
    Class<?> clazz = Cat.class;
    // publicな空引数コンストラクタ
    Constructor<?> cons1 = clazz.getConstructor();
    System.out.println(cons1.getName() + " ---> パラメータ数: " + cons1.getParameterCount());
    // privateなString,intのコンストラクタ
    Constructor<?> cons2 = clazz.getDeclaredConstructor(String.class, int.class);
    System.out.println(cons2.getName() + " ---> パラメータ数: " + cons2.getParameterCount());
}

2.3 リフレクションでコンストラクタを使用する

コンストラクタの役割はオブジェクトを初期化して返すことです。Constructorクラスのメソッドを使用します。

// コンストラクタの実行
Constructor<?> cons = clazz.getDeclaredConstructor(String.class, int.class);
cons.setAccessible(true); // privateコンストラクタへのアクセス許可
Object obj = cons.newInstance("タマ", 3);

Catクラスにprint文を追加して確認:

public Cat() {
    System.out.println("空引数コンストラクタ実行");
}
private Cat(String name, int age) {
    System.out.println("パラメータありコンストラクタ実行");
    this.name = name;
    this.age = age;
}

2.4 リフレクションでメンバ変数を取得・使用する

Classクラスが提供するメソッドでメンバ変数を取得。

@Test
public void testGetFields() throws Exception {
    Class<?> clazz = Cat.class;
    Field[] fields = clazz.getDeclaredFields();
    for (Field f : fields) {
        System.out.println(f.getName() + " : " + f.getType());
    }
}

フィールドへの値設定・取得:

@Test
public void testFieldAccess() throws Exception {
    Class<?> clazz = Cat.class;
    Cat cat = new Cat();
    Field nameField = clazz.getDeclaredField("name");
    nameField.setAccessible(true);
    nameField.set(cat, "シロ");
    System.out.println(nameField.get(cat)); // シロ
}

2.5 リフレクションでメソッドを取得・使用する

Catクラスにいくつかのメソッドを追加。

public class Cat {
    private String name;
    private int age;
    
    public Cat() {}
    private Cat(String name, int age) { this.name = name; this.age = age; }
    
    private void run() {
        System.out.println("猫が速く走る");
    }
    public void eat() {
        System.out.println("猫がキャットフードを食べる");
    }
    private String eat(String food) {
        return "猫が好きな食べ物: " + food;
    }
    // getter/setter略
}

メソッドを取得するコード:

@Test
public void testGetMethods() throws Exception {
    Class<?> clazz = Cat.class;
    Method[] methods = clazz.getDeclaredMethods();
    for (Method m : methods) {
        System.out.println(m.getName() + " --> パラメータ数: " + m.getParameterCount() + " --> 戻り値: " + m.getReturnType());
    }
}

特定のメソッドを取得して実行:

@Test
public void testInvokeMethod() throws Exception {
    Class<?> clazz = Cat.class;
    Cat cat = new Cat();
    // private runメソッド
    Method runMethod = clazz.getDeclaredMethod("run");
    runMethod.setAccessible(true);
    Object result = runMethod.invoke(cat);
    System.out.println(result); // null
    
    // private eat(String)メソッド
    Method eatMethod = clazz.getDeclaredMethod("eat", String.class);
    eatMethod.setAccessible(true);
    Object foodResult = eatMethod.invoke(cat, "魚");
    System.out.println(foodResult);
}

2.6 リフレクションの応用例:簡易フレームワーク

任意のオブジェクトのプロパティ名と値をファイルに書き込むフレームワークを作成します。

public class ObjectFrame {
    public static void saveObject(Object obj) throws Exception {
        PrintStream ps = new PrintStream(new FileOutputStream("data.txt", true));
        Class<?> clazz = obj.getClass();
        ps.println("--- " + clazz.getSimpleName() + " ---");
        Field[] fields = clazz.getDeclaredFields();
        for (Field field : fields) {
            field.setAccessible(true);
            String name = field.getName();
            Object value = field.get(obj);
            ps.println(name + " = " + value);
        }
        ps.close();
    }
}

使用例:

public class TestFrame {
    @Test
    public void saveTest() throws Exception {
        Student s = new Student("田中太郎", 20, '男', 175.0, "サッカー");
        Teacher t = new Teacher("佐藤先生", 500000);
        ObjectFrame.saveObject(s);
        ObjectFrame.saveObject(t);
    }
}

三、アノテーション

3.1 アノテーションの認識と定義

アノテーションはコード内の特別なマークです。例:@Override、@Testなどです。他のプログラムがアノテーション情報に基づいて動作を決定できるようにします。

カスタムアノテーションの定義:

public @interface MyTest {
    String aaa();
    boolean bbb() default true;
    String[] ccc();
}

使用例:

@MyTest(aaa = "牛魔王", ccc = {"HTML", "Java"})
public class AnnotationDemo {
    @MyTest(aaa = "鉄扇公主", bbb = false, ccc = {"Python", "前端", "Java"})
    public void testMethod() {}
}

特別な属性名がvalueの場合、使用時に省略可能:

public @interface MyTest2 {
    String value();
    int age() default 10;
}
@MyTest2("孙悟空") // @MyTest2(value="孙悟空") と同等

アノテーションの本質:アノテーションはインターフェースであり、すべてのアノテーションインターフェースはjava.lang.annotation.Annotationを継承しています。属性は抽象メソッドです。@MyTest(aaa="...")は、MyTestインターフェースの実装オブジェクトと似ており、aaa()メソッドを呼び出すことで値を取得できます。

3.2 メタアノテーション

メタアノテーションはアノテーションを修飾するアノテーションです。代表的なもの:@Targetと@Retention。

@Target: アノテーションが使用できる場所を指定(クラス、メソッド、フィールドなど)。

@Retention: アノテーションの保持期間を指定(SOURCE: ソースコードのみ、CLASS: バイトコードまで、RUNTIME: 実行時まで)。

例:

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyTest3 {
}

3.3 アノテーションの解析

リフレクションを使って、クラスやメソッドに付与されたアノテーションを取得し、その属性値を読み取ることを「アノテーションの解析」と呼びます。

@MyTest3
public class Sample {
    @MyTest3
    public void doSomething() {}
}

解析コード:

public class AnnotationParser {
    @Test
    public void parseClassAnnotation() {
        Class<?> clazz = Sample.class;
        if (clazz.isAnnotationPresent(MyTest3.class)) {
            MyTest3 annotation = clazz.getAnnotation(MyTest3.class);
            // 属性があれば取得
        }
    }
    @Test
    public void parseMethodAnnotation() throws Exception {
        Class<?> clazz = Sample.class;
        Method method = clazz.getMethod("doSomething");
        if (method.isAnnotationPresent(MyTest3.class)) {
            MyTest3 annotation = method.getAnnotation(MyTest3.class);
            // 属性を利用
        }
    }
}

3.4 アノテーションの応用例:簡易テストフレームワーク

@MyTestが付いたメソッドだけを実行するフレームワークを作成。

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyTest {} 
public class TestRunner {
    @MyTest
    public void test1() {
        System.out.println("test1実行");
    }
    @MyTest
    public void test2() {
        System.out.println("test2実行");
    }
    public void test3() {
        System.out.println("test3(実行されない)");
    }
    public static void main(String[] args) throws Exception {
        TestRunner runner = new TestRunner();
        Class<?> clazz = runner.getClass();
        for (Method m : clazz.getDeclaredMethods()) {
            if (m.isAnnotationPresent(MyTest.class)) {
                m.invoke(runner);
            }
        }
    }
}

四、動的プロキシ

4.1 動的プロキシの紹介と準備

動的プロキシは、あるインターフェースを実装したクラスの代わりに振る舞うオブジェクトを実行時に生成する技術です。

例:歌手インターフェースとその実装クラスを用意します。

public interface Singer {
    String sing(String song);
    void dance();
}

public class SuperStar implements Singer {
    private String name;
    public SuperStar(String name) { this.name = name; }
    @Override
    public String sing(String song) {
        System.out.println(name + "が歌う: " + song);
        return "拍手";
    }
    @Override
    public void dance() {
        System.out.println(name + "が踊る");
    }
}

4.2 動的プロキシオブジェクトの生成

java.lang.reflect.ProxyクラスのnewProxyInstanceメソッドを使用してプロキシを生成します。

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class ProxyFactory {
    public static Singer createProxy(SuperStar target) {
        return (Singer) Proxy.newProxyInstance(
            ProxyFactory.class.getClassLoader(),
            new Class[]{Singer.class},
            new InvocationHandler() {
                @Override
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                    // 前処理
                    if (method.getName().equals("sing")) {
                        System.out.println("マイク準備、20万円受領");
                    } else if (method.getName().equals("dance")) {
                        System.out.println("会場準備、1000万円受領");
                    }
                    // 実際のメソッド呼び出し
                    return method.invoke(target, args);
                }
            }
        );
    }
}

使用例:

public class Main {
    public static void main(String[] args) {
        SuperStar star = new SuperStar("楊超越");
        Singer proxy = ProxyFactory.createProxy(star);
        String result = proxy.sing("良い日");
        System.out.println(result);
        proxy.dance();
    }
}

4.3 動的プロキシの応用:メソッド実行時間計測

UserServiceインターフェースとその実装クラスに対し、プロキシを使って各メソッドの実行時間を自動計測します。

public interface UserService {
    void login(String loginName, String passWord) throws Exception;
    void deleteUsers() throws Exception;
    String[] selectUsers() throws Exception;
}

public class UserServiceImpl implements UserService {
    @Override
    public void login(String loginName, String passWord) throws Exception {
        if ("admin".equals(loginName) && "123456".equals(passWord)) {
            System.out.println("ログイン成功");
        } else {
            System.out.println("ログイン失敗");
        }
        Thread.sleep(1000);
    }
    @Override
    public void deleteUsers() throws Exception {
        System.out.println("1万人のユーザーを削除");
        Thread.sleep(1500);
    }
    @Override
    public String[] selectUsers() throws Exception {
        System.out.println("3人のユーザーを検索");
        Thread.sleep(500);
        return new String[]{"田中", "鈴木", "佐藤"};
    }
}

プロキシ生成ユーティリティ:

public class UserServiceProxyFactory {
    public static UserService createProxy(UserService target) {
        return (UserService) Proxy.newProxyInstance(
            UserServiceProxyFactory.class.getClassLoader(),
            new Class[]{UserService.class},
            new InvocationHandler() {
                @Override
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                    long start = System.currentTimeMillis();
                    Object result = method.invoke(target, args);
                    long end = System.currentTimeMillis();
                    System.out.println(method.getName() + " 実行時間: " + (end - start) + "ms");
                    return result;
                }
            }
        );
    }
}

テストコード:

public class TestApp {
    public static void main(String[] args) throws Exception {
        UserService service = UserServiceProxyFactory.createProxy(new UserServiceImpl());
        service.login("admin", "123456");
        service.deleteUsers();
        String[] users = service.selectUsers();
        System.out.println("ユーザー: " + Arrays.toString(users));
    }
}

動的プロキシの動作:プロキシオブジェクトのメソッドが呼ばれるたびに、InvocationHandlerのinvokeメソッドが実行され、そこで前処理・後処理を行うことができます。

タグ: JUnit リフレクション Javaアノテーション 動的プロキシ proxy

8月8日 08:01 投稿