One of the issues when I write unit test is having to create TestSuite after TestSuite at package level to run all the test in a package or run all tests in a project. And I really dislike the repetitiveness of it.
And sometimes I just like to test all classes in a package and its sub packages rather than running all the tests in a project due to the amount of time it takes to setup all the necessary resources.
Finally I decided to do something about it and started digging into JUnit to figure out a way to automate testing classes in a package without having to add test classes to a TestSuite.
The initial version of Suite I developed added all classes from the package a class was marked with RunWith annotation as well as all classes from sub-packages recursively.
It was all great. I could just mark a class with @RunWith(DynamicSuite.class) annotation at the root of a project’s root package and it would
run all the tests for the project. Or I could mark a class in a package with @RunWith(DynamicSuite.class) annotation and I could test just the classes in the package and its sub-packages.All sweet.
But later in some cases I only wanted to run classes in a package and not all classes in its sub-packages as well due to the speed of test as some tests in sub-packages were resource intensive. So I had to come up with a finer-grained test Suite mechanism.
To tackle that issue, I decided to add an Annotation -@NonRecursive – that marked a Test entry point ( class marked with @RunWith(DynamicSuite.class) annotation as being non-recursive.
I wanted to be able to test just a package when I wanted to without testing sub-packages, but I also wanted a over-riding mechanism to be able to run all tests in a project or a package and its sub-package such that if the entry point class is not marked as @NonRecursive , all the @NonRecursive annotaitons get ignored in subsequent sub-packages and all classes in the package and below get executed.
Here’s the DynamicSuite Class code.
/**
*TestSuite execute all test classes from the package of <br>
* classes that are annotated to run with this class<br>
* It loads all classes in the same package as the setup class and
* loads classes from sub packages recursively.
* If a sub-package contains a class that's marked to be run with this class<br>
* it only loads that class so that the marked class can in turn load and execute<br>
* test classes in its package and its subpackages.<br><br>
* To cut it short: This class loads and executes all test classes in its package and its
* subpackages recursively.<br<br>
* Example usage:Just create an empty class and add <code>@RunWith</code> annotation<br>
*
* <code>@RunWith(DynamicSuite.class)<br>
* public class BatchTest {<br>
* //no methods or body needed.<br>
* }</code>
* <br><br>
* To test only classes in a package and ignore test classes in subpackages mark the class with @NonRecursive.
* To test all test classes in a project mark a class at a project's root test package with <code>@RunWith(DynamicSuite.class)</code>
* and it will run all test classes in the project.
* The only catch so far is that all the methods of all test classes gets run under
* this the setup class and the results are all grouped under that class , at least from what I can see in Netbeans.
*
*
* @author anjan
*/
public class DynamicSuite extends Suite {
static boolean isEntrySuite=true;
public DynamicSuite(Class<?> setupClass) throws InitializationError {
super(setupClass, getTestClasses(setupClass,setupClass.isAnnotationPresent(NonRecursive.class)));
//this will run after the initial setup has been done;
//If a Class is marked as NonRecursive and its the entry point of tests
// it won't load test classes from subpackages.
//but if the Class is marked as NonRecursive and its not the entry point
// of tests the annotation will be ignored and all classes from subpackages of the containing
//package will be loaded
isEntrySuite=false;
}
public static Class<?>[] getTestClasses(Class setupClass ,boolean nonRecursive) {
List<Class> classes = null;
String packageName = setupClass.getPackage().getName();
String path = setupClass.getProtectionDomain().getCodeSource().getLocation().getFile();
path = path + packageName.replace('.', File.separatorChar);
File dir = new File(path);
try {
classes = findTestClasses(dir, setupClass,nonRecursive);
} catch (Exception ex) {
throw new RuntimeException("Problem loading classes from package[" + packageName + "]", ex);
}
return classes.toArray(new Class[classes.size()]);
}
/**
* recursively finds Classes in test package <br>
* if it finds a class annotated with @RubWith(DynamicSuite.class)
* if loads that class and ignores all classes and sub-packages in that package
* because the loaded class is responsible for loading classes under ite hierarchy
* @param dir The root directory
* @return The classes
* @throws ClassNotFoundException
*/
private static List<Class> findTestClasses(File dir, Class setupClass, boolean nonRecursive) throws ClassNotFoundException {
List<Class> classes = new ArrayList<Class>();
if (!dir.exists()) {
return classes;
}
File[] files = dir.listFiles();
for (File file : files) {
if (file.isDirectory()) {
//only load classes in subpackage if the setup class isn't marked as NonRecursive
// or its marked as NonRecursive but its not the entry point class
if(!nonRecursive ||(nonRecursive && !isEntrySuite))
classes.addAll(findTestClasses(file, setupClass,nonRecursive));
}
else if (file.getName().endsWith(".class")) {
String className = file.getName().substring(0, file.getName().length() - 6);
if (!className.equals(DynamicSuite.class.getSimpleName())) {
String packageDirPath = file.getPath();
packageDirPath = packageDirPath.substring(packageDirPath.indexOf("classes"));
String pkg = packageDirPath.replace(File.separatorChar, '.').substring(8).replace("." + file.getName(), "");
Class cls = Class.forName(pkg + '.' + className);
if (cls.isAnnotationPresent(RunWith.class)) {
RunWith ann = (RunWith) cls.getAnnotation(RunWith.class);
if (ann.value().equals(DynamicSuite.class)) {
//ignore the class being run, otherwise the program will crash with StackOverflowError
if (!cls.equals(setupClass)) {
classes = new ArrayList<Class>();
classes.add(cls);
break;
}
} else {
classes.add(cls);
}
} else {
classes.add(cls);
}
}
}
}
return classes;
}
}
The Annotation class:
/**
*Used to Mark DynamicSuite as NonRecursive so that:<br>
* If a Suite that's being run is marked as NonRecursive, it will only run classes<br>
* within the same package.<br>
* But If the Suite being run is not marked as NonRecursive,<br>
* it ignores all NonRecursive annotations in Suites contained in subpackages of the package the<br>
* Suite class belongs to.<br>
* The rationale is to be able to test only Tests in a package when desired at the same time<br>
* being able to test all tests in a project or in a package and subpackages when running when the Suite being run is<br>
* not marked as NonRecursive.<br>
* @author anjan
*/
@Retention(RetentionPolicy.RUNTIME)
public @interface NonRecursive {
}
Sample code:
Running all test classes recursively
@RunWith(DynamicSuite.class)
public class EntryPoint {
}
Sample code:
Running only test classes in a package
@NonRecursive
@RunWith(DynamicSuite.class)
public class EntryPoint {
}

