Macs, Modularity and More

Running all tests in an Eclipse project

This is an extension of a hint posted by my good friend Björn Mårtensson that allowed all tests to be run inside Eclipse.

If you've ever done any programming inside Eclipse, and wanted to use a suite of JUnit tests, then you'll know that you either have to run each test individually inside Eclipse, or create a test suite (normally called AllTests) that encompasses all the tests you want to run. Having to manually update that class each time you want to run a new test is less than productive use of your time, and besides which, may result in you forgetting to add tests when they're created.

Fortunately, you don't need to specify a hard-coded list of tests to run; you can run all tests in an Eclipse project in one go. All you need to do is calculate the list of classes, and then pass those into the standard test runner and get the answer back again. And there's a really easy way of doing this using the TestCollector interface (and the ClassPathTestCollector implementation).

I've adapted Björn's original example to include a check for non-abstract classes, and also to allow the test to be run in a plugin as well as in Eclipse. This is necessary when doing Eclipse platform development, since your code may assume a particular behaviour of (say) Plugin.getStateLocation(), which wouldn't be available if you were just using ordinary JUnit testing. The AllTests looks for *Test on the classpath, filtering out *PlatformTest if not running as an Eclipse plugin. It does this by providing a second TestCollector implementation; if you want to copy and paste this code on non-plugin projects, just delete this implementation.

Owing to the fact that it's not possible (AFAIK) to determine the bundle name from the class that is being run (especially from static methods), the code makes the assumption that the package that contains the AllTests class is the same name as the plugin. So if you wanted to test the org.example.foo plugin, then you'd put it into the org.example.foo package. This can be changed in the source code.

The latest version should be available at AllTests.java but at the time of writing, the public CVS web repository is several days behind the HEAD that's been checked in, so YMMV. (Maybe this is why they're encouraging people to switch over to Subversion...) In the meantime, here's the contents of the code. It's released under the EPL, so you can use it in your own projects:

// Copyright (c) 2006 Alex Blewitt
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// which accompanies this distribution, and is available at
// http://www.eclipse.org/legal/epl-v10.html
//
// Contributors:
// Alex Blewitt - Initial API and implementation
//
package org.rcpapps.base;
import java.lang.reflect.Modifier;
import java.net.URL;
import java.util.Enumeration;
import java.util.Vector;
import junit.framework.Test;
import junit.framework.TestSuite;
import junit.runner.ClassPathTestCollector;
import junit.runner.TestCollector;
import org.eclipse.core.runtime.Platform;
/**
* Run all the tests in this project, either computed from the classpath or from
* the bundlepath. To use this as-is, drop it into a non-default package that
* has the same name as the plugin. For example, if the plugin is called
* org.example.foo, this should be placed in a package
* org.example.foo, and all tests should live under the
* org.example.foo package structure (either directly, or in any
* subpackage). By default this will include all non-abstract classes named
* XxxTest, excluding XxxPlatformTest if running
* outside of the platform.
*/
public class AllTests {
/**
* Detects classes from the bundle PLUGIN_NAME's entries. Uses
* bundle.findEntries to obtain a list of classes that live
* in the specified PACKAGE_NAME, and adds those to the test path, providing
* that they are {@link AllTests#isValidTest(String, boolean) valid}.
*/
private static class BundleTestDetector implements TestCollector {
/*
* @see junit.runner.TestCollector#collectTests()
*/
public Enumeration collectTests() {
final Vector tests = new Vector();
try {
Enumeration entries = Platform.getBundle(PLUGIN_NAME).findEntries("/", "*" + SUFFIX + ".class", true);
while (entries.hasMoreElements()) {
URL entry = (URL) entries.nextElement();
// Change the URLs to have Java class names
String path = entry.getPath().replace('/', '.');
int start = path.indexOf(PACKAGE_NAME);
String name = path.substring(start, path.length()
- ".class".length());
if (isValidTest(name, true)) {
tests.add(name);
}
}
} catch (Exception e) {
// If we get here, the Platform isn't installed and so we fail
// quietly. This isn't a problem; we might be outside of the
// Platform framework and just running tests locally. It's not
// even worth printing anything out to the error log as it would
// just confuse people investigating stack traces etc.
}
return tests.elements();
}
}
/**
* Searches the current classpath for tests, which are those ending with
* SUFFIX, excluding those which end in IN_CONTAINER_SUFFIX, providing that
* they are {@link AllTests#isValidTest(String, boolean) valid}.
*/
private static class ClassFileDetector extends ClassPathTestCollector {
/*
* @see junit.runner.ClassPathTestCollector#isTestClass(java.lang.String)
*/
protected boolean isTestClass(String classFileName) {
return classFileName.endsWith(SUFFIX + ".class")
&& isValidTest(classNameFromFile(classFileName), false);
}
}
/**
* All tests should end in XxxTest
*/
public static final String SUFFIX = "Test";
/**
* All in-container tests should end in XxxPlatformTest
*/
public static final String IN_CONTAINER_SUFFIX = "Platform" + SUFFIX;
/**
* The base package name of the tests to run. This defaults to the name of
* the package that the AllTests class is in for ease of management but may
* be trivially changed if required. Note that at least some identifiable
* part must be provided here (so default package names are not allowed)
* since the URL that comes up in the bundle entries have a prefix that is
* not detectable automatically. Even if this is "org" or "com" that should
* be enough.
*/
public static final String PACKAGE_NAME = AllTests.class.getPackage()
.getName();
/**
* The name of the plugin to search if the platform is loaded. This defaults
* to the name of the package that the AllTests class is in for ease of
* management but may be trivially changed if required.
*/
public static final String PLUGIN_NAME = AllTests.class.getPackage()
.getName();
/**
* Add the tests reported by collector to the list of tests to run
* @param collector the test collector to run
* @param suite the suite to add the tests to
*/
private static void addTestsToSuite(TestCollector collector, TestSuite suite) {
Enumeration e = collector.collectTests();
while (e.hasMoreElements()) {
String name = (String) e.nextElement();
try {
suite.addTestSuite(Class.forName(name));
} catch (ClassNotFoundException e1) {
System.err.println("Cannot load test: " + e1);
}
}
}
/**
* Is the test a valid test?
* @param name the name of the test
* @param inContainer true if we want to include the inContainer tests
* @return true if the name is a valid class (can be loaded), that it is not
* abstract, and that it ends with SUFFIX, and that either
* inContainer tests are to be included or the name does not end
* with IN_CONTAINER_SUFFIX
*/
private static boolean isValidTest(String name, boolean inContainer) {
try {
return name.endsWith(SUFFIX)
&& (inContainer || !name.endsWith(IN_CONTAINER_SUFFIX))
&& ((Class.forName(name).getModifiers() & Modifier.ABSTRACT) == 0);
} catch (ClassNotFoundException e) {
System.err.println(e.toString());
return false;
}
}
/**
* Return all the tests. If we're in a platform, return everything. If not,
* we return those tests that end in SUFFIX but excluding those ending in
* IN_CONTAINER_SUFFIX.
* @return a suite of tests for JUnit to run
* @throws Error if there are no tests to run.
*/
public static Test suite() {
TestSuite suite = new TestSuite(AllTests.class.getName());
addTestsToSuite(new ClassFileDetector(), suite);
addTestsToSuite(new BundleTestDetector(), suite);
if (suite.countTestCases() == 0) {
throw new Error("There are no test cases to run");
} else {
return suite;
}
}
}