A Selective JUnit Test Runner

Like all "good" solutions, this one begins with a problem. Specifically, a problem that I had executing my unit tests within Eclipse.

Usually, running JUnit tests is quite easy in Eclipse. If you have a JUnit test class, you can simply right click on it and Run As » JUnit Test. If you have an entire package full of test classes or indeed, an entire folder full of test packages, just do the same: Run As » JUnit Test.

The trouble starts when you have packages that contain a mix of test and non-test classes and further, a src/test/java folder that contains a mix of test and non-test packages. There is literally no way in Eclipse to selectively choose the test classes to execute! If you find one, let me know and I'll happily retract the rest of this post :-)

When using Ant, it is trivial to pick and choose which tests to run by passing a suitably constructed fileset to the junit task's batchtest property.

Consider:

<junit showoutput="true" fork="yes">

    <classpath refid="test.run.classpath" />
    <formatter type="brief" />

    <batchtest todir="${testOutput}">
        <fileset dir="${src}/test/java">
            <include name="**/cache/objects/*.java" />
            <include name="**/config/*.java" />
            <include name="**/util/*.java" />
            <include name="**/test/TestFoo.java" />
            <include name="**/test/TestBar.java" />
            <include name="**/test/TestBaz.java" />
        </fileset>
    </batchtest>
</junit>

Now I could launch the corresponding Ant test task from within Eclipse but it doesn't integrate well (i.e., not at all!) with Eclipse's JUnit view. Instead, what I want is to be able to select my test classes like above and then launch & monitor them as I normally do in Eclipse.

So then, here are the requirements:

  1. Be able to specify test classes to run.
  2. Be able to launch using Run As » JUnit Test.

If you look at the junit ant task above, you'd notice that the way in which we declared the set of test classes is quite nice. It allows use of wildcards which provides great flexibility in selecting resources. Wouldn't it be nice to reuse the same syntax in our new JUnit runner? It would be and that's what we'll do! But first, let's focus on building the list of test classes.

JUnit provides a Suite class that acts as a test Runner for a specified set of classes. All we have to do is pass it a list of classes we want executed as tests and it'll do the rest. Let's look at some code:

/**
 * Discovers all JUnit tests as per config and runs them in a suite.
 */
@RunWith(AllTests.class)
public class AllTests extends Suite  {

    public AllTests(final Class<?> clazz) throws InitializationError {
        super(clazz, findTestClasses());
    }

    private static Class<?>[] findTestClasses() throws InitializationError {

        // The following Resource Locations are configured using Ant fileset syntax
        String[] resourceLocations = new String[] {
                "classpath*:**/cache/objects/*.class",
                "classpath*:**/config/*.class",
                "classpath*:**/util/*.class",
                "classpath*:**/test/TestFoo.class",
                "classpath*:**/test/TestBar.class",
                "classpath*:**/test/TestBaz.class"
        };

        try {
            // Restrict search to this base package
            String basePackage = "net.antrix";

            TestClassesPatternResolver resolver = new TestClassesPatternResolver(
                    basePackage, Arrays.asList(resourceLocations));

            List<Class<?>> classes = resolver.getClasses();

            return classes.toArray(new Class[classes.size()]);

        } catch (Exception e) {
            List<Throwable> errors = new ArrayList<Throwable>();
            errors.add(e);
            throw new InitializationError(errors);
        }
    }
}

The AllTests class is quite simple. We extend the JUnit Suite class and pass our list of classes to the Suite constructor. The list of classes is constructed using a list of strings passed to a TestClassesPatternResolver. Clearly, this TestClassesPatternResolver is taking the list of Strings in our "wildcard" format and returning the actual list of classes that match the specified patterns. Let's look at the implementation of this resolver class.

class TestClassesPatternResolver {

    private final List<Class<?>> _classes = new ArrayList<Class<?>>();
    private final String _basePackage;
    private final ResourcePatternResolver _resourcePatternResolver = new PathMatchingResourcePatternResolver();

    public TestClassesPatternResolver(String basePackage, List<String> resourceLocations) 
                throws IOException, ClassNotFoundException {

        this._basePackage = basePackage;

        for (String resourceLocation : resourceLocations) {
            _classes.addAll(resolveClassesFromResourcePatterns(resourceLocation));
        }

    }

    public List<Class<?>> getClasses() {
        return _classes;
    }

    private Collection<Class<?>> resolveClassesFromResourcePatterns(String pattern) 
                throws IOException, ClassNotFoundException {

        Collection<Class<?>> classes = new ArrayList<Class<?>>();

        if (ResourcePatternUtils.isUrl(pattern)) {

            Resource[] resources = _resourcePatternResolver.getResources(pattern);

            for (Resource resource : resources) {

                URL url = resource.getURL();

                System.out.println("Resolving Class Name for URL: " + url.toString());

                String className = resolveClassFromPath(url.toString());

                System.out.println("Finding methods in Class Name: " + className);

                if (className != null && className.length() > 0) {
                    Class<?> klass = Class.forName(className);
                    for (Method method : klass.getDeclaredMethods()) {
                        // If at least one method is annotated with the JUnit
                        // @Test annotation, we include the class as a Test Class.
                        if (method.getAnnotation(org.junit.Test.class) != null) {
                            System.out.println("Found @Test method " + method.getName() + " in class " + klass.getName());
                            classes.add(klass);
                            break;
                        }
                    }
                }
            }
        }

        return classes;
    }

    private String resolveClassFromPath(String url) {
        String classUrl = ClassUtils.convertResourcePathToClassName(url);
        int begin = classUrl.indexOf(this._basePackage);
        if (begin < 0) return null;
        return classUrl.substring(begin, classUrl.indexOf(".class"));
    }
}

Again, most of what is happening above should be quite obvious. All of the heavy lifting is done by the awkardly named Spring class PathMatchingResourcePatternResolver as well as the Spring provided utility classes ResourcePatternUtils & ClassUtils. This does bring in a Spring dependency but since the project already uses Spring - not a deal breaker.

With the above two classes included in my project, I can now simply right click on the AllTests.java class in the Eclipse Package Explorer and Run As » JUnit Test. Mission accomplished!


Markdown formatting supported