Testing with a Classloader
When building utility classes, testing becomes more important than in other parts of the software. Utilities are hopefully (re)used a lot and thus should have the most reliable behavior. This also means that documentation should always be up to date. Though utilities are generally easy, they might not be the easiest to test.
I created a little example of a utility that I often use, depending on the application, often bearing the name "SettingsUtil". It's a simple class that returns properties from a property file somewhere on the classpath and returns either the value for a specified key from the properties file or a default value if no value is associated with the given key.
The interesting part of the SettingsUtil is in the initialization:
InputStream resourceStream = null;
// First attempt to load the resource from the context classloader.
final ClassLoader contextClassLoader = Thread
.currentThread().getContextClassLoader();
if (contextClassLoader != null) {
resourceStream = contextClassLoader
.getResourceAsStream(location);
}
// If the first attempt failed, use the settings class to resolve
// the resource.
if (resourceStream == null) {
resourceStream = SettingsUtil.class
.getResourceAsStream(location);
}
Now the utility itself is not complex, testing it is slightly more complex, this can be found at SettingsUtilTest. The reason we would want to test this, is because a container might make the utility behave in differently. In fact, the context classloader is quite essential to the proper behavior of any container. More specifically, it allows shared classes and utilities to look up resources that are in fact not visible within it's own classloader. Returning the test itself, the obvious test methods cover the obvious code. The 'testClassloaderResourceNotFound' method replaces the properties file with a variant that points to something non-existent:
/** Tests classloader cannot find resource. */
@Test
public void testClassloaderResourceNotFound() {
// Reset value to something non-existing.
SettingsUtil.getInstance().resetProperties();
SettingsUtil.getInstance().updatePropertiesLocation(
NON_EXISTING_PROPERTIES_LOCATION);
final String value1 = SettingsUtil.getValue(EXISTING_KEY_1,
NON_EXISTING_VALUE_1);
Assert.assertEquals(NON_EXISTING_VALUE_1, value1);
final String value2 = SettingsUtil.getValue(EXISTING_KEY_2,
NON_EXISTING_VALUE_2);
Assert.assertEquals(NON_EXISTING_VALUE_2, value2);
}
The 'testClassloaderLoadException' and 'testClassloaderCloseException' require a slightly complexer setup. Both require a custom classloader that delegates of it's functionality to it's parent, as per it's default behavior. Notably, it does not delegate the 'getResourceAsStream' method, it returns a custom input stream. Both as such:
/** Tests stream could not be read. */
@Test
public void testClassloaderLoadException() {
// Reset properties and replace classloader.
SettingsUtil.getInstance().resetProperties();
final ClassLoader originalClassloader = Thread.currentThread()
.getContextClassLoader();
final ClassLoader newClassloader = new
InterceptingInputStreamClassLoader(
new NonReadingInputStream());
Thread.currentThread().setContextClassLoader(newClassloader);
// We assume the reading has gone wrong.
final String value1 = SettingsUtil.getValue(EXISTING_KEY_1,
NON_EXISTING_VALUE_1);
Assert.assertEquals(NON_EXISTING_VALUE_1, value1);
final String value2 = SettingsUtil.getValue(EXISTING_KEY_2,
NON_EXISTING_VALUE_2);
Assert.assertEquals(NON_EXISTING_VALUE_2, value2);
// Return original classloader.
Thread.currentThread().setContextClassLoader(
originalClassloader);
}
/** Tests stream could not be closed. */
@Test
public void testClassloaderCloseException() {
// Reset properties and replace classloader.
SettingsUtil.getInstance().resetProperties();
final ClassLoader originalClassloader = Thread.currentThread()
.getContextClassLoader();
final InputStream resourceStream = getClass()
.getResourceAsStream(
EXISTING_PROPERTIES_LOCATION);
final ClassLoader newClassloader = new
InterceptingInputStreamClassLoader(
new NonClosingInputStream(resourceStream));
Thread.currentThread().setContextClassLoader(newClassloader);
// We assume the rest of the reading was done properly.
final String value1 = SettingsUtil.getValue(EXISTING_KEY_1,
NON_EXISTING_VALUE_1);
Assert.assertEquals(EXISTING_VALUE_1, value1);
final String value2 = SettingsUtil.getValue(EXISTING_KEY_2,
NON_EXISTING_VALUE_2);
Assert.assertEquals(EXISTING_VALUE_2, value2);
// Return original classloader.
Thread.currentThread().setContextClassLoader(
originalClassloader);
}
Together, all these bits and pieces allows us to achieve a very high code coverage for this utility. Try the code here. There is a reference to the parent pom that configures some reports that give some insight into the code coverage. Though it can also be found here.
Note that all code is distributed under the AGPLv3.