■ 16.2—Collect tests automatically from an archive
4.5 Scan the file system for tests
◆ Problem
You want like to build a test suite from all the tests in a certain part of your file sys- tem, without having to maintain AllTests classes by hand.
◆ Background
When a project grows to a certain size, maintaining test suites by hand becomes a real chore. You feel this pain the most when you need to make a structural change to your project, such as moving classes from package to package or even moving packages around in the hierarchy. These kinds of changes should not affect the overall behavior of your system, and many modern IDEs support these operations with the click of a mouse, so why are you forced to rewrite all these AllTests classes? There has to be a better way.
117 Scan the file system for tests
The better way is to scan the file system for tests. Let some object discover all the test case classes in a given directory structure and then present them to you as a test suite.
◆ Recipe
Although you could build a utility to do the work of scanning a part of your file system for anything that might be a test, Mike Bowler’s GSBase (http://
gsbase.sourceforge.net) already provides that feature with RecursiveTestSuite. This test suite builder is simple to use: point it at a directory containing built test classes (not source) on your file system and specify a TestFilter, an object that identifies whether or not to add a test case class to the test suite.13 You are still building a custom test suite, but you are building it programmatically rather than by specifying individual test case classes. Listing 4.1 shows an example of using RecursiveTestSuite with a TestFilter that simply accepts all test case classes.
import junit.framework.Test;
import com.gargoylesoftware.base.testing.RecursiveTestSuite;
import com.gargoylesoftware.base.testing.TestFilter;
public class AllTests {
public static Test suite() throws Exception {
return new RecursiveTestSuite("classes", new TestFilter() { public boolean accept(Class eachTestClass) {
return true;
} });
} }
The first parameter to the constructor for RecursiveTestSuite is a location on the file system, either a path name (as a String) or a File object (representing a directory). This is where the test suite builder looks for Java class files that look like tests. Following a prevailing naming convention, any class whose name ends with Test is considered a test case class. So the first thing RecursiveTestSuite does is collect all the classes it believes are test case classes.
13This concept of filters is explained in detail in Jeff Langr’s Essential Java Style (Prentice Hall PTR, 2000).
Pay particular attention to chapter 4, “Collections.” The techniques he describes there are derived from Smalltalk, where filters like this are everywhere.
Listing 4.1 Collecting all the tests in one part of the file system
Custom suite method
Always add the test case No need to extend TestCase
118 CHAPTER 4 Managing test suites
You can then use the TestFilter to include (or exclude) test case classes according to any criterion you can determine from a class object, including whether it implements any interfaces. Implement the accept() method to return true for all the test case classes you want to include in your test suite and false for the ones you want to exclude. In our example, we include them all. You can use this technique to separate the different kinds of tests you write into different test suites. See recipe 4.6, “Separate the different kinds of test suites,” for details.
◆ Discussion
Although this works well, you may find a warning such as the following when you use RecursiveTestSuite to run your tests:
junit.framework.AssertionFailedError: Cannot instantiate test case
You will see this warning if you have Abstract Test Cases (see recipe 2.6, “Test an interface”), which generally look like test case classes but, being declared abstract, cannot be instantiated. You can use the TestFilter to eliminate those abstract classes from the suite, as listing 4.2 shows.
import java.lang.reflect.Modifier;
import junit.framework.Test;
import com.gargoylesoftware.base.testing.RecursiveTestSuite;
import com.gargoylesoftware.base.testing.TestFilter;
public class AllTests {
public static Test suite() throws Exception {
return new RecursiveTestSuite("classes", new TestFilter() { public boolean accept(Class eachTestClass) {
boolean classIsConcrete = ! Modifier.isAbstract(
eachTestClass.getModifiers());
return classIsConcrete;
} });
} }
This test filter asks each test case class whether it is concrete (not abstract), ensur- ing that the suite contains only test case classes that JUnit can actually instantiate.
Listing 4.2 Eliminating abstract classes from a test suite using TestFilter
119 Scan the file system for tests
Alternative
JUnit-addons (http://junit-addons.sourceforge.net) also provides a way to build a test suite from an entire directory structure with its utility class junitx.util.
DirectorySuiteBuilder. It also supports filtering tests based on criteria you spec- ify, but the code looks a little different. We can write the previous example
“gather all concrete TestCase classes” as shown in listing 4.3.
public class AllConcreteTestsSuite {
public static Test suite() throws Exception { TestFilter filter = new TestFilter() {
public boolean include(Class eachTestClass) { boolean classIsConcrete =
!Modifier.isAbstract(eachTestClass.getModifiers());
return classIsConcrete;
}
public boolean include(String eachTestClassName) { return true;
} };
DirectorySuiteBuilder builder =
new DirectorySuiteBuilder(filter);
return builder.suite(new File("test/classes"));
} }
We can think of only one strong difference between this code and the Recursive- TestSuite worth mentioning: RecursiveTestSuite’s filter has a single “accept”
method, whereas DirectorySuiteBuilder tests the class object and the class name separately. You may prefer the former because it is simpler. Deciding which tool to use is largely a matter of preference and depends mostly on which tools you are already using. If you are already using GSBase or JUnit-addons for something else, then stick with that package. There is talk between Mike Bowler and Vladimir Bossicard of joining the projects, anyway.
◆ Related
■ 2.6—Test an interface
■ 4.4—Collect all the tests for your entire system
■ 4.6—Separate the different kinds of test suites
Listing 4.3 Eliminating abstract classes from a test suite using DirectorySuiteBuilder Include only concrete classes
Do not filter based on class name
Use the filter we created
Look in the directory test/classes for tests
120 CHAPTER 4 Managing test suites
■ GSBase (http://gsbase.sourceforge.net)
■ JUnit-addons (http://junit-addons.sourceforge.net)