About the @Test annotation used by Junit The method that implements this annotation is invoked as a test method, I was wondering what kind of principle it works Let's consider the implementation contents
In the first place, is the Java file started by Junit only the Java file specified under a specific directory? For example, if you put a Java file under the setting folder such as "src / test" Start the test starting from that src / test.
In other words, it is thought that the subordinate of src / test is set as the entry point, so Traverse Java files (class files) under that directory I think I'm only executing a method that has the @Test annotation set (I can't say for sure because I haven't seen the Junit implementation)
The reason why the entry point is set under a specific folder like this Depending on the classpath setting, Java files are read in order from the root path of the class loader. Find the method with the @Test annotation and If annotated, instantiate the target class I thought I was executing the method.
Based on this consideration, I tried to see if a similar implementation could be achieved using reflection.
Create an annotation to be added to the method like @Test annotation
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;
}
This time only the methods set to true in testFlg Implemented to be the target of method invocation
I searched for classes and methods with @TestAnno and created a class that would be invited.
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 In the process here, the package to drive test is specified. This time, the java file under the package name [test] is targeted. → testRoot
*2 Use the walk function to extract the java files under testRoot Do a directory traverse. If it is a normal file (that is, if it is a class file), filter it so that it will be processed. Only class files are targeted for Stream processing
*3 Here, get the class file to be extracted as a character string including the package name.
*4 Get the class to be tested as a reflection Get an instance
*5 Extract the method object from the acquired class object and Those with testAnnotation and testFlg of true invoke.
With this procedure, I was able to extract the methods with @testAnnotation.
Here, we define a class that has a method that is actually started as a test.
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);
}
}
Make settings like @TestAnno (testFlg = true) and If you check if testFlg is invited only for methods that are true It worked fine.
That's all, I don't understand the settings and contents of the context class loader, so I will check it again in my spare time.
The difficult part of implementing this time was getting the package name. There may not be much demand for such an implementation, as there weren't many sites to reference.
This time, I was thinking about the implementation contents of the points that I was wondering about the movement of Junit, For classes and methods that are started by setting annotations I wonder if a similar implementation has been made.
For example, auto-wiring of spring frameworks and component scanning. I think it's the same way of thinking.
Also, if I have a chance, I'd like to find out more about reflection.
With this implementation, about the movement of class loader and reflection I realized I didn't understand at all When writing an article about Java, I'll look into the details around it.
Many of the above codes have been cut out. If you are interested, please refer to Github. https://github.com/ktkt11122334/qiita/tree/master/test
Recommended Posts