Categories: Java
Introduction
Java classloaders can be very useful things. They can also be tricky to work with. But the trickiest thing can be writing unit tests for code that expects (or is required to function) with specific classloader hierarchies.
This article demonstrates a simple technique that allows a junit test class to control the classloader structure used to execute its test methods.
The project which prompted the development of this technique is the Jakarta Commons Logging (JCL) project which provides a common API for a number of different logging systems (java.util.logging, log4j and others). JCL is intended to behave differently depending upon what libraries are available in the classpath, and is also required to function in various complex classloader configurations such as may be found in J2EE frameworks.
Note: This article was written around the year 2003, and has been sitting around waiting to be published. It should still apply, but might need some tweaking to be 100% compatible with the latest junit version..
Main-method approach
Junit tests are usually executed with junit, the classes being tested and any libraries that are required all being in the system classloader.
Whether the unit tests are being run via ant, a gui junit front-end or otherwise it comes down to something like this:
java
-classpath tests.jar:app.jar:lib1.jar:junit.jar
junit.textui.TestRunner FirstTestCase SecondTestCase
This provides no mechanism to build a complex classloader hierarchy for the unit tests to run within, nor does it provide the ability to set different classpaths for the test methods within FirstTestCase and SecondTestCase.
A common solution is to have test case classes define a main method:
public class FirstTestCase extends TestCase {
public static void main(String[] args) {
ClassLoader parentLoader = new URLClassLoader(...);
ClassLoader childLoader = new URLClassLoader(...);
Class testCase = childLoader.loadClass(FirstTestCase.class.getName());
junit.textui.TestRunner.run(testCase);
}
}
The tests within FirstTestCase can then be executed as:
java
-classpath tests.jar:app.jar:lib1.jar:junit.jar
FirstTestCase
This approach has the following disadvantages:
- this really only works with the junit text ui.
- the main method needs to figure out where the needed jar files are
- the main method runs only one test case.
When using Ant to execute many test cases, the last point implies that there needs to be a separate target defined for each test case class to be executed, which can complicate the ant build file significantly. And this approach doesn’t work with GUI-based junit front ends at all.
An alternative solution is to provide a “wrapper” class that looks at its command-line parameters to decide how to set up the classloader hierarchy, then executes junit.textui.TestRunner or equivalent. While this removes the need for the test case classes to define main methods, it moves a fundamental part of the actual test (the classloader environment) out of the test itself and into the wrapper class or a file that executes the wrapper class with various arguments (eg an Ant build file). This makes the test case classes incomplete (they can’t be understood in isolation) as well as still suffering from the one-target-per-test-case issue.
Solution
The key to a more elegant solution is recognising that a junit TestSuite is a collection of references to methods to be executed - and each method object in the collection has an implicit reference to a specific class object:
public class FirstTestCase extends TestCase {
public static Test suite() throws Exception {
// create a classloader which is a child of the system classloader
URL[] parentPath = {...};
URLClassLoader parent = new URLClassLoader(parentPath);
URL[] childPath = {...};
URLClassLoader child = new URLClassLoader(childPath, parent);
Class testClass = child.loadClass(FirstTestCase.class.getName());
return new TestSuite(testClass);
}
public void testSomething() {
....
}
}
When presented with a class, junit first checks for a static suite() method. If present, this method is expected to return a Suite object 1 which is a collection of test methods available to be executed - and each test method knows the TestCase object it is a member of. When a suite method is not available, junit simply creates a TestSuite object initialised by introspecting the provided class to find its test methods.
Here the class being executed by junit builds a standard junit TestSuite object as junit would - except that the class being introspected is not the “original” instance of the FirstTestCase class but instead one loaded via a custom classloader. The effect is that when the Method objects found by the TestSuite are executed, they are invoked on a class that sees the custom classloader hierarchy set up by the original suite method.
If you are happy for your tests to have the system classpath visible in the custom classloader hierarchy then this solution works well. However tests are more reliable when they completely control their environment, so it would be much better to make the system classloader completely invisible to the test:
// create a classloader which is a direct child of the
// boot classloader
URLClassLoader parent = new URLClassLoader(parentPath, null);
Unfortunately bypassing the system classloader introduces a problem: when the test’s
methods are invoked by the TestSuite they manipulate classes from the junit
library. Simply ensuring that the junit library is in the classpath of one of the
custom classloaders isn’t enough - when code in the “reloaded” test case calls
child.getClass("junit.framework.Assert")
the returned class must be the same class object visible to the calling junit code,
not just a copy.
There’s a couple of other problems with the first version of the code shown above:
- The test case class needs to know the exact URLs of the libraries and directories that are needed in the classpath.
- The URLClassLoader class doesn’t provide the ability to do child-first class lookup as is commonly done by servlet and j2ee containers.
- There is no control over the context classloader set during the test
All these problems can be addressed by fairly simple custom ClassLoader and TestSuite classes:
public class FirstTestCase extends TestCase {
public static Test suite() throws Exception {
PathableClassLoader parent = new PathableClassLoader(null);
parent.useSystemLoader("junit.");
parent.addLogicalLib("app");
parent.addLogicalLib("lib1");
PathableClassLoader child = new PathableClassLoader(parent);
child.setParentFirst(false);
child.addLogicalLib("testclasses");
child.addLogicalLib("lib2");
Class testClass = child.loadClass(FirstTestCase.class.getName());
return new PathableTestSuite(testClass, child);
}
public void testSomething() {
....
}
}
The useSystemLoader method instructs a PathableClassLoader to handle requests to load specific classes by forwarding the request to the system classloader instead of its parent classloader. This ensures that the test case code accesses exactly the same junit classes as the junit library invoking the tests.
The addLogicalLib method causes the PathableClassLoader to look up the provided library name in the system properties to obtain an actual URL for the library. The ant build file, IDE or other mechanism invoking the actual unit test defines the mapping from logical lib name to actual physical location by setting appropriate system properties.
The setParentFirst method controls whether classes present in a classloader’s path can override classes of the same name present in a parent classloader’s path or not.
And as you can see the PathableClassLoader allows components to be added to the classpath via individual calls rather than having an array passed to the constructor - this is not critical but is generally more convenient.
The constructor for the PathableTestSuite class takes not only the TestCase class to introspect but also a classloader parameter. The specific classloader will be set as the Thread Context Classloader before each test method in the suite is invoked. This isn’t an essential feature as tests could set this themselves, but it is more reliable and more convenient for the PathableTestSuite to handle this.
With test cases written in this style, they can be executed in exactly the same manner as test cases that don’t require any custom classpath setup, with only one additional burden: system properties must be defined to map any logical library names used by the test cases to the actual URLs at which those libraries can be found.
Suite as separate class
There is one constraint on the above solution: junit needs to be able to load the FirstTestCase class in order to execute its suite method. That implies that the FirstTestCase and all of the classes that it references (fields, parameters, local variable types, etc) must be available to junit. It is often cleaner to omit these from the classpath that junit sees. And in some cases different test case classes may require incompatible libraries.
This can be handled by observing that the class containing the suite() method doesn’t actually need to be the TestCase class:
public class FirstTests extents TestCase {
public void testSomething() {
example.AwkwardDependency dep = new example.AwkwardDependency();
....
}
}
public class FirstTestCase {
public static Test suite() throws Exception {
PathableClassLoader parent = new PathableClassLoader(null);
parent.useSystemLoader("junit.");
parent.addLogicalLib("app");
parent.addLogicalLib("lib1");
PathableClassLoader child = new PathableClassLoader(parent);
child.setParentFirst(false);
child.addLogicalLib("testclasses");
child.addLogicalLib("lib2");
Class testClass = child.loadClass("FirstTests");
return new PathableTestSuite(testClass, child);
}
}
While the example.AwkwardDependency class must be available via the custom classpath defined within FirstTestCase.suite, junit doesn’t need to have that class available in order to execute the FirstTestCase.suite method.
Executing the same tests with different classloader configurations
Sometimes it is necessary to verify that a certain set of tests have the same results despite changes in the classloader configuration. This can be achieved by creating an abstract TestCase (ie one which is not executed by junit) and creating concrete subclasses which just define suite() methods with the desired classloader hierarchies:
public abstract class FirstTests extend TestCase {
public void testSomething() {
....
}
}
public class FirstTestsWithSetup1 extends FirstTests {
public static Test suite() {
// set up hierarchy #1
}
}
public class FirstTestsWithSetup2 extends FirstTests {
public static Test suite() {
// set up hierarchy #2
}
}
Tests involving Singletons
A quick search of the internet reveals a number of people asking how to write unit tests for code which directly or indirectly accesses Singleton objects. This is indeed a problem for traditional unit tests as there is no clean way to reset a Singleton’s state between tests.
When a unit test executes in a custom classloader hierarchy, however, this issue is resolved as the class is reloaded into a “clean” classloader.
Limitations
The following restrictions apply to the above solution:
- classloader hierarchies are controllable only per TestCase, not per individual test method
- static variables visible from the suite() method are not visible to the test methods, as the test case class is actually a different instance. This means that it is more difficult for the suite() method to make data available to the test methods (esp. references to the classloaders it has created). This isn’t commonly needed, however, and there are reflection-based solutions if it is necessary.
- this approach may use more memory than traditional unit tests. As each TestCase has a custom classloader, all the libraries that test case loads are stored within that classloader hierarchy instead of being shared with other test cases.
Unresolved Issues
- How does this approach interact with the “fork” option for Ant? Presumably the execution of the suite() methods happens in the parent JVM, and then for each test class (method?) a JVM is forked and passed a message containing the name of the test (class?) to test. So what happens to the custom classloader setup?
- How does this approach interact with GUI front-ends?
- How does this approach interact with junit’s “reloading classloader”?
Footnotes
-
Well, actually the method just has to return a Test object. However a suite object is a Test object. ↩