JUnitのテスト起動的なものを試してみる
Junitで使用される@Testアノテーション について
このアノテーション を実装したメソッドはテストメソッドとして起動するが、
どのような原理で動いているか気になったので
その実装内容に関して考察してみる
Junitのテストメソッド起動
そもそもJunitで起動するJavaファイルは特定のディレクトリの配下で指定されたJavaファイルのみである?
例えば「src/test」のような設定フォルダの配下にJavaファイルを置くと
そのsrc/testを起点としてテスト起動する。
つまりsrc/testの配下をエントリーポイントとして設定していると考えられるため、
そのディレクトリ配下のJavaファイル(クラスファイル)をトラバースして
@Testアノテーション が設定されているメソッドのみを実行していると考えている
(Junitの実装を見たわけではないので断言できない)
このようにある特定のフォルダ以下をエントリーポイントとして設定しているのは
クラスパスの設定によってクラスローダーのrootパスから順番にJavaファイルを読み込んで
@Testアノテーション が設定されているメソッドを探して、
もしアノテーション が付与されているなら、対象のクラスをインスタンス化して
メソッドを実行しているのではないかと思った。
このような考察のもと、リフレクションを使用して同じような実装ができないか試してみた。
 
 
アノテーション の作成
@Testアノテーション のようにメソッドに付与するアノテーション を作成する
package anno;
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 TestAnno {
  boolean testFlg() default false;
}
今回はtestFlgでtrue設定されたメソッドのみ
メソッド起動の対象となるように実装した
 
 
起動のエントリーポイントの作成
@TestAnnoが付与されているクラスとメソッドを探しだし、invokeされるようなクラスの作成を行った
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.stream.Stream;
import anno.TestAnno;
public class Entry {
  public static void main(String[] args) throws Exception {
    // *1
    final String resourceName = "test";
    final ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
    final URL testRoot = classLoader.getResource(resourceName);
    final String appRootStr = classLoader.getResource(".").getFile().toString();
  // *2
    try (Stream<Path> paths = Files.walk(Paths.get(testRoot.toURI()))) {
      paths.filter(Files::isRegularFile).forEach(path -> {
        // *3
        String targertAbsolutePath = path.normalize().toFile().getAbsolutePath().toString();
        String targetClassName = targertAbsolutePath.substring(appRootStr.length(), targertAbsolutePath.length()).replace(".class", "").replace("/", ".");
        // 4
        Class<?> testClass;
        Object testClassInstance;
        try {
          testClass = Class.forName(targetClassName);
          testClassInstance = testClass.newInstance();
        } catch (Exception e) {
          throw new RuntimeException(e);
        }
     // *5
        for( Method method : testClass.getMethods() ) {
          TestAnno testAnnotation = method.getAnnotation(TestAnno.class);
          if ( testAnnotation == null ) continue;
          try {
            if ( testAnnotation.testFlg() )
              method.invoke(testClassInstance);
          } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
            e.printStackTrace();
          }
        }
      });
      System.out.println("Test End");
    }
  }
}
*1
 ここの処理では、test駆動するパッケージの指定を行っている
 今回はパッケージ名[test]配下のjavaファイルを対象とする。
 → testRootとする
*2
 testRoot配下のjavaファイルを抽出するためにwalk関数を使って
 ディレクトリトラバースをする。
 通常ファイルであれば(つまりクラスファイルであれば)処理対象とするようにフィルタリングして
 クラスファイルのみStream処理の対象としている
*3
 ここでは、抽出するクラスファイルをパッケージ名込みで文字列として取得し
 *4のClass.forNameでリフレクションを取得できるようにしている
*4
 テスト対象のクラスをリフレクションとして取得し、
 インスタンスを取得しておく
*5
 取得したクラスオブジェクトからメソッドオブジェクトを抽出して、
 testAnnotationが付与されている、かつ、testFlgがtrueのものを
 invokeする。
このような手順で、@testAnnotationが付与されているメソッドを抽出することができた。
テスト起動されるメソッドの実装
ここでは、実際にテスト起動されるメソッドをもつクラスを定義する。
package test;
import anno.TestAnno;
public class Test {
  @TestAnno(testFlg=true)
  public void testMethod() {
    String className = this.getClass().getName();
    String methodName = Thread.currentThread().getStackTrace()[1].getMethodName();
    System.out.println("invoked " + className + ":" + methodName);
  }
  @TestAnno(testFlg=false)
  public void testMethod2() {
    String className = this.getClass().getName();
    String methodName = Thread.currentThread().getStackTrace()[1].getMethodName();
    System.out.println("invoked " + className + ":" + methodName);
  }
  @TestAnno(testFlg=true)
  public void testMethod3() {
    String className = this.getClass().getName();
    String methodName = Thread.currentThread().getStackTrace()[1].getMethodName();
    System.out.println("invoked " + className + ":" + methodName);
  }
}
@TestAnno(testFlg=true)のように設定を行い、
testFlgがtrueのメソッドのみinvokeされるか確認すると
正常に動作しました。
 
 
以上になりますが、
コンテキストクラスローダーの設定や内容などが理解できていないので、
また暇なときに調べてみようと思います。
今回実装していて難しかった点は、パッケージ名を取得するところでした。
あまり参考できるサイトがなかったので、こういった実装は需要がないのかもしれません。
今回はJunitの動きで疑問に思った点の実装内容の考察でしたが、
アノテーション の設定によって起動するクラスやメソッドについては
同じような実装がなされているのかと思います。
例えば、スプリングフレームワークのオートワイヤリングやコンポーネントスキャンなども
同じような考え方ではないのかなと思っています。
また機会があれば、リフレクションの詳細についても調べてみようかと
今回の実装で、クラスローダやリフレクションの動きについて
全然わかってないことに気づいたので
Javaを記事を書く際は、その辺の詳細を調べていこうかと思います。
上記の記載したコードは一部切り取っているものが多いので
興味がある方はGithubを参考ください。
https://github.com/ktkt11122334/qiita/tree/master/test