Javaのテスト、リフレクション、アノテーション技術

JUnitユニットテスト

ユニットテストには2つの種類があります:

  • ブラックボックステスト:コードを書かずに入力値のみを与えて、出力が期待値に合致するか確認する方法
  • ホワイトボックステスト:コードを書いて、具体的な実行フローに注目する方法

JUnitはホワイトボックステストに使用されます。

JUnitの使用手順

  1. テストクラスの定義
    • 推奨命名法:テスト対象のクラス名 + Test
    • パッケージ名:XXX.XXX.XX.Test
  2. テストメソッドの定義:独立して実行可能なメソッド
    • 推奨命名法:test + テスト対象のメソッド名
    • 戻り値:void
    • 引数リスト:なし
  3. @Testアノテーションの追加

判定結果

  • 赤色:失敗
  • 緑色:成功

サンプルコード

元のクラス

public class Calculator {
    public int add(int a, int b) {
        return a + b;
    }
}

テストコード

import org.junit.Test;
import static org.junit.Assert.assertEquals;

public class CalculatorTest {
    @Test
    public void testAddition() {
        System.out.println("テストが実行されました");
        Calculator calc = new Calculator();
        int result = calc.add(5, 7);
        assertEquals(12, result);
    }
}

補足アノテーション

  • @Before:テストメソッドの前に自動実行されるメソッドに付与
  • @After:テストメソッドの後に自動実行されるメソッドに付与

リフレクション:フレームワーク設計の魂

リフレクションとは、クラスの各構成要素を他のオブジェクトとしてカプセル化するメカニズムです。

利点:

  1. プログラム実行中にこれらのオブジェクトを操作できる
  2. 結合度を下げ、プログラムの拡張性を高められる

Javaコードのコンピュータにおける3段階

  1. ソースコード段階
      > バイトコードファイル > Employee.java → (javacコンパイル) → Employee.class
  2. クラスローダー段階
      > メンバ変数 Field[] fields > コンストラクタ Constructor[] cons > メンバメソッド Method[] methods
  3. 実行時段階

Classオブジェクトの取得方法

  1. Class.forName("完全修飾クラス名")
      > バイトコードファイルをメモリに読み込み、Classオブジェクトを返す
  2. クラス名.class
      > クラス名のClass属性から取得
  3. オブジェクト.getClass()
      > getClass()メソッドはObjectクラスで定義されている

サンプルクラス

public class Employee {
    private String employeeName;
    private int employeeAge;
    
    public Employee(String employeeName, int employeeAge) {
        this.employeeName = employeeName;
        this.employeeAge = employeeAge;
    }
    
    public Employee() {
    }
    
    public String getEmployeeName() {
        return employeeName;
    }
    
    public void setEmployeeName(String employeeName) {
        this.employeeName = employeeName;
    }
    
    public int getEmployeeAge() {
        return employeeAge;
    }
    
    public void setEmployeeAge(int employeeAge) {
        this.employeeAge = employeeAge;
    }
    
    @Override
    public String toString() {
        return "Employee{" +
                "employeeName='" + employeeName + '\'' +
                ", employeeAge=" + employeeAge +
                '}';
    }
}

Classオブジェクトの機能

メンバ変数の取得と操作

import java.lang.reflect.Field;

public class ReflectionDemo1 {
    public static void main(String[] args) throws Exception {
        Class employeeClass = Employee.class;
        
        // public修飾のメンバ変数をすべて取得
        Field[] fields = employeeClass.getFields();
        for (Field field : fields) {
            System.out.println(field);
        }
        
        System.out.println("------------");
        
        // 指定名のメンバ変数を取得
        Field nameField = employeeClass.getField("publicField");
        Employee emp = new Employee();
        Object value = nameField.get(emp);
        System.out.println(value);
        nameField.set(emp, "新しい値");
        System.out.println(emp);
        
        System.out.println("----------------------------");
        
        // 修飾子に関係なくすべてのメンバ変数を取得
        Field[] declaredFields = employeeClass.getDeclaredFields();
        for (Field declaredField : declaredFields) {
            System.out.println(declaredField);
        }
        
        System.out.println("------------------------");
        
        // privateアクセスの場合、アクセス修飾子のセキュリティチェックを無視
        Field privateField = employeeClass.getDeclaredField("employeeName");
        privateField.setAccessible(true); // 暴力的リフレクション
        Object value2 = privateField.get(emp);
        System.out.println(value2);
    }
}

コンストラクタの使用

import java.lang.reflect.Constructor;

public class ReflectionDemo2 {
    public static void main(String[] args) throws Exception {
        Class employeeClass = Employee.class;
        
        // パラメータ付きコンストラクタを取得
        Constructor constructor = employeeClass.getConstructor(String.class, int.class);
        System.out.println(constructor);
        
        // オブジェクトを作成
        Object employee = constructor.newInstance("田中", 30);
        System.out.println(employee);
        
        System.out.println("-------------------");
        
        // 空パラメータコンストラクタを使用
        Constructor noArgConstructor = employeeClass.getConstructor();
        Object employee2 = noArgConstructor.newInstance();
        System.out.println(employee2);
    }
}

メソッドの実行

import java.lang.reflect.Method;

public class ReflectionDemo3 {
    public static void main(String[] args) throws Exception {
        Class employeeClass = Employee.class;
        
        // メソッドを取得
        Method workMethod = employeeClass.getMethod("work");
        Employee emp = new Employee();
        // メソッドを実行
        workMethod.invoke(emp);
        
        Method workWithParamsMethod = employeeClass.getMethod("work", String.class);
        workWithParamsMethod.invoke(emp, "プロジェクト");
        
        System.out.println("------------");
        
        // すべてのpublicメソッドを取得
        Method[] methods = employeeClass.getMethods();
        for (Method method : methods) {
            System.out.println(method.getName());
        }
    }
}

フレームワーク開発ケーススタディ

要件:任意のクラスのオブジェクトを作成し、その中のメソッドを実行するフレームワークを開発

実装手順:

  1. 設定ファイルに作成するオブジェクトの完全修飾クラス名と実行するメソッドを定義
  2. プログラムで設定ファイルを読み込む
  3. リフレクション技術でクラスファイルをメモリに読み込む
  4. オブジェクトを作成
  5. メソッドを実行

設定ファイル

className = com.example.Employee
methodName = work

メインクラス

import java.io.InputStream;
import java.util.Properties;
import java.lang.reflect.Method;

public class ReflectionFramework {
    public static void main(String[] args) throws Exception {
        // 設定ファイルを読み込む
        Properties props = new Properties();
        ClassLoader classLoader = ReflectionFramework.class.getClassLoader();
        InputStream is = classLoader.getResourceAsStream("config.properties");
        props.load(is);
        
        // 設定ファイルからデータを取得
        String className = props.getProperty("className");
        String methodName = props.getProperty("methodName");
        
        // クラスをメモリに読み込む
        Class clazz = Class.forName(className);
        
        // オブジェクトを作成
        Object obj = clazz.newInstance();
        
        // メソッドオブジェクトを取得
        Method method = clazz.getMethod(methodName);
        
        // メソッドを実行
        method.invoke(obj);
    }
}

アノテーション

アノテーション(Annotation)は、メタデータとも呼ばれ、コードレベルの説明です。JDK1.5以降で導入された機能で、クラス、インターフェース、列挙と同じレベルにあります。

アノテーションの役割

  1. ドキュメント生成:コードに記述されたアノテーションからドキュメントを生成
  2. コード分析:コードに記述されたメタデータを使ってコードを分析(リフレクションを使用)
  3. コンパイル時チェック:メタデータを使ってコンパイラが基本的なコンパイルチェックを実現(例:@Override)

JDKで定義された主なアノテーション

  • @Override:アノテーションが付与されたメソッドが親クラス(インターフェース)から継承されたものかを検出
  • @Deprecated:アノテーションが付与された内容が非推奨であることを示す
  • @SuppressWarnings:警告を抑制する

カスタムアノテーションの作成

形式:

メタアノテーション
public @interface アノテーション名 {}

本質:アノテーションは本质上はインターフェースで、このインターフェースはAnnotationインターフェースを継承しています。

プロパティ(インターフェースの抽象メソッド)

要件:

  1. プロパティの戻り値の型
      > 基本データ型 > String > 列挙型 > アノテーション > 上記型の配列
  2. プロパティを定義した後、値を設定する必要がある
      > プロパティを定義する際にdefaultキーワードを使ってプロパティを初期化する場合、アノテーション使用時にプロパティの値を設定する必要はない > 値を設定するプロパティが1つだけで、プロパティ名がvalueの場合、valueは省略可能 > 配列に値を設定する場合、値は{}で囲む。配列に値が1つしかない場合は{}を省略可能

メタアノテーション

  • @Target:アノテーションが適用できる位置を記述
      > TYPE:クラスに適用 > METHOD:メソッドに適用 > FIELD:メンバ変数に適用
  • @Retention:アノテーションが保持される段階を記述
      > @Retention(RetentionPolicy.RUNTIME):記述されたアノテーションがクラスバイトコードファイルに保持され、JVMによって読み取られる
  • @Documented:アノテーションがAPIドキュメントに抽出されるかどうかを記述
  • @Inherited:アノテーションが子クラスによって継承されるかどうかを記述

アノテーションの解析

手順:

  1. アノテーションが定義されている位置のオブジェクトを取得(Class、Method、Field)
  2. 指定されたアノテーションを取得 getAnnotation(Class)
  3. アノテーションで定義された抽象メソッドを呼び出して、設定されたプロパティ値を取得

テストフレームワークの実装

要件:メインメソッド実行後、検証対象のすべてのメソッド(アノテーションが付与されたもの)を自動実行し、例外があるか判定してファイルに記録するフレームワーク

検証アノテーション

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface Validate {
}

計算機クラス

public class MathCalculator {
    @Validate
    public void add() {
        System.out.println("1 + 0 = " + (1 + 0));
    }
    
    @Validate
    public void subtract() {
        System.out.println("1 - 0 = " + (1 - 0));
    }
    
    @Validate
    public void multiply() {
        System.out.println("1 * 0 = " + (1 * 0));
    }
    
    @Validate
    public void divide() {
        System.out.println("1 / 0 = " + (1 / 0));
    }
    
    public void show() {
        System.out.println("バグなし");
    }
}

テストフレームワーククラス

import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.lang.reflect.Method;

public class TestFramework {
    public static void main(String[] args) throws IOException {
        // オブジェクトを作成
        MathCalculator calc = new MathCalculator();
        
        // バイトコードファイルオブジェクトを取得
        Class clazz = calc.getClass();
        
        // 例外発生回数
        int errorCount = 0;
        BufferedWriter bw = new BufferedWriter(new FileWriter("bug_report.txt"));
        
        // すべてのメソッドを取得
        Method[] methods = clazz.getMethods();
        for (Method method : methods) {
            if (method.isAnnotationPresent(Validate.class)) {
                try {
                    method.invoke(calc);
                } catch (Exception e) {
                    errorCount++;
                    bw.write(method.getName() + " メソッドで例外が発生");
                    bw.newLine();
                    bw.write("例外名: " + e.getCause().getClass().getSimpleName());
                    bw.newLine();
                    bw.write("例外原因: " + e.getCause().getMessage());
                    bw.newLine();
                    bw.write("-------------------------------------");
                    bw.newLine();
                }
            }
        }
        
        bw.write("今回のテストで合計 " + errorCount + " 個の例外が発生しました");
        bw.flush();
        bw.close();
    }
}

タグ: Java JUnit リフレクション アノテーション テストフレームワーク

7月12日 23:12 投稿