in java ~ read.

Unit tests and Class loaders

Let's assume that we have a third-party library. This library is written with a lot of errors. The most terrible of them is an possible exception, which can be thrown in a static constructor of one of key library's classes :( Of cause, it is very sad... but we need to go further - we need to write unit tests for this library! :)

I know that it is stupid and useless... but let's look on it from academic point of view.

First of all, let's take a look on that tricky class:

public class Problem { 

    static { 
        InputStream is = ClassLoader.getSystemResourceAsStream("problem.cfg"); 
        if (null == is) { 
            throw new RuntimeException("Config not found"); 
        } 
    } 
}

So, what will happen when configuration file will not be found in a classpath? Yes, its obvious that RuntimeException will be thrown :) But what then? Then classloader will throw an ExceptionInInitializerError. But this is not all. Since classloader fails to load class it marks that class as "bad" and don't take any attempts to load it in future.

Hence, if we'll try to initialize Problem class without it's config, we get ExceptionInInitializerError at first time. If we repeat our actions we'll get NoClassDefFoundError.

Cool. But what if we want to write unit test for this class, what assumes several attempts of class initialization? And please, don't ask me why - we just want to do so :)

We can't unload a class from Java, except of case of garbage collection (which is uncontrollable for us). Also we can run each unit test in it's own JVM, but this approach requires a big changes in unit test framework. So, the only way that we have is launch each unit test with it's own classloader.


Class loaders

Let's distract for a few minutes and recall what is classloaders and how they work. I don't want to write "yet another" article about classloaders, so I just list the main facts:

  • Every class in runtime enviroment must be loaded by some class loader;
  • Most of classes are loaded by demand;
  • There are three types of class loaders:
    • bootstrap - loads base java classes (rt.jar, i18n.jar, etc), built in JVM, can be configured by JAVAOPTS -Xbootclasspath;
    • extension classloader - loads classes from $JAVAHOME/lib/ext, implemented by sun.misc.Launcher$ExtClassLoader;
    • system classloader - loads classes from classpath, implemented by sun.misc.Launcher$AppClassLoader;
  • Classloaders are built in hierarchy (as listed above). When classloader needs to load class he asks his parent first;
  • Apart of this hierarchy there are two other kind of classloaders:
    • current classloader - classloader of current class, which is used by default for class loading at runtime (Class.forName, ClassLoader.loadClass or simple first time class declaration). Can be retrieved by Clazz.class.getClassLoader() or this.getClass().getClassLoader();
    • context classloader - classloader for current thread. Can be used with Thread.getContextClassLoader()/Thread.setContextClassLoader() methods.
  • You can define your own classloaders (as inheritors of hierarchy or not) to provide special ways of loading (network, archives) or tricky behavior (as in our case);

Here is classloaders hierarchy diagram (stolen from some other blog):

So, if we need to create independent classloaders for our issue we should do next:

public class TestClassLoader extends URLClassLoader {  
    public TestClassLoader() { 
        super(((URLClassLoader) getSystemClassLoader()).getURLs(), null); 
    } 
}

Notice, if second parameter equals null in super constructor, that our class loader will not have a parent. In this case, new classloader will have it's own classes collection. This means that class A loaded with one instance of TestClassLoader will not be equals (and can't be casted) to class A loaded with another instance of TestClassLoader.

If we want to have classloaders with common set of classes, we must include them in current class loaders hierarchy:

public class TestClassLoader extends URLClassLoader {  
   public TestClassLoader() { 
      super(((URLClassLoader)getSystemClassLoader()).getURLs()); 
   } 
}

Every instance of this classloader will load it's own classes, if they don't already loaded by parent classloader (when class loader needs to load class he asks his parent first). Fortunately, you can override loading logic for specific classes:

public class TestClassLoader extends URLClassLoader {  
    public TestClassLoader() { 
        super(((URLClassLoader) getSystemClassLoader()).getURLs()); 
    } 

    @Override 
    public Class<?> loadClass(String name) throws ClassNotFoundException { 
        if (name.startsWith("example.Problem")) { 
            return super.findClass(name); 
        } 

        return super.loadClass(name); 
    } 
}

But remember, if parent classloader already loaded your class, you can't cast class from you classloader like this:

ClassLoader testClassLoader = new TestClassLoader();  
Problem problem = (Problem) Class.forName(Problem.class.getName(), true, testClassLoader).newInstance();  
Because now you have two different classes from different classloaders. If you want to call some method of object of loaded class you should use reflection: 

ClassLoader testClassLoader = new TestClassLoader(); 

Class<?>  problemClass = Class.forName(Problem.class.getName(), true, testClassLoader);  
Object obj = problemClass.newInstance(); 

Method method = problemClass.getMethod("methodName");  
method.invoke(obj);  

As for Unit tests, you can implement your own runner to run every test class with it's own class loader:

public class SeparateClassloaderTestRunner extends BlockJUnit4ClassRunner { 

    public SeparateClassloaderTestRunner(Class<?> clazz) throws InitializationError {
        super(getTestClass(clazz)); 
    } 

    private static Class<?> getTestClass(Class<?> clazz) throws InitializationError {
        try { 
            return Class.forName(clazz.getName(), true, new TestClassLoader()); 
        } catch (ClassNotFoundException e) { 
            throw new InitializationError(e); 
        } 
    } 
}


Class path

OK, now we know how to run several tests with different class loaders. But how can we change Problem class behaviour? If this class would use Problem.class.getSystemResourceAsStream() method we could manipulate it's classpath during classloader creation, controlling set of URL that is passed into constructor. Also it wouldn't be a problem if it used context loader, which can be easy changed with Thread.setContextClassLoader() method.
Unfortunately, it uses ClassLoader.getSystemResourceAsStream() method. This means that we should change current classloader's classpath..

Strictly speaking, we can't change classpath of current classloaders. But there is one dirty trick :)

URL url = new URL(this.getClass().getResource("/").toString() + "path/to/problem/config/");  
URLClassLoader urlClassLoader = (URLClassLoader) ClassLoader  
  .getSystemClassLoader(); 

Class<?> urlClass = URLClassLoader.class;  
Method method = urlClass.getDeclaredMethod("addURL", new Class[] { URL.class });  
method.setAccessible(true);  
method.invoke(urlClassLoader, new Object[] { url });  

This approach doesn't allow us to remove objects from classpath... but adding will be enough for our purpose.


Putting all together

Now we know that we can create instances of Problem class as many time as we want. Though, we have one restriction: test without config file in classpath should runs first. That's because we can't remove element from classpath after we added it. And that's why we can't use JUnit runner.

So, let's write a simple test:

public class ProblemTest { 

    @Test 
    public void testCommon() throws Exception { 

        testWithoutConfig(); 
        testWithConfig(); 
    } 

    public void testWithoutConfig() throws Exception { 
        ClassLoader testClassLoader = new TestClassLoader(); 

        try { 
            Class<?> problemClass = Class.forName(Problem.class.getName(), 
                    true, testClassLoader); 
            problemClass.newInstance(); 

            fail("Problem class can't be created without config"); 

        } catch (Throwable e) { 
            e.printStackTrace(); 
        } 
    } 

    public void testWithConfig() throws Exception { 
        ClassLoader testClassLoader = new TestClassLoader(); 

        URL url = new URL(this.getClass().getResource("/").toString() + "cfg/"); 
        URLClassLoader urlClassLoader = (URLClassLoader) ClassLoader 
                .getSystemClassLoader(); 

        Class<?> urlClass = URLClassLoader.class; 
        Method method = urlClass.getDeclaredMethod("addURL", 
                new Class[] { URL.class }); 
        method.setAccessible(true); 
        method.invoke(urlClassLoader, new Object[] { url }); 

        Class<?> problemClass = Class.forName(Problem.class.getName(), true, 
                testClassLoader); 
        problemClass.newInstance(); 
    } 

    private class TestClassLoader extends URLClassLoader { 
        public TestClassLoader() { 
            super(((URLClassLoader) getSystemClassLoader()).getURLs()); 
        } 

        @Override 
        public Class<?> loadClass(String name) throws ClassNotFoundException { 
            if (name.startsWith("package.name.Problem")) { 
                return super.findClass(name); 
            } 

            return super.loadClass(name); 
        } 
    } 
}
comments powered by Disqus