Monday, November 05, 2007

Dynamically add to the Eclipse JUnit classpath

When you run JUnit tests inside Eclipse something that may bite you is the fact that the compile classpath and runtime classpath are separate.

Let's say your Eclipse project directories are layed out something like the following:

PROJ_ROOT
  • bin
  • conf
  • src
  • test
where src contains your core application source code and test contains your JUnit tests.

When a JUnit test runs, by default the base run directory and the classpath root is the PROJ_ROOT directory. Therefore, if your unit test loads a file relative to the the PROJ_ROOT, say:


new FileInputStream("conf/context.xml");


or


Thread.currentThread().getContextClassLoader().getResourceAsStream("conf/context.xml");


it will work. If, however, you've told Eclipse that conf is a "src" directory (i.e. part of the compile classpath) and then try to load a file from somewhere in the classpath from your unit test like so:


Thread.currentThread().getContextClassLoader().getResourceAsStream("context.xml");


it will fail.

So what can you do to change the classpath for the unit tests that need it?

You can manually add the conf directory to the runtime classpath for just those unit tests that need them, but unfortunately so does everyone else in your team.

A cute trick is to change the thread classloader to a URLClassLoader, whose constructor allows you to add URLs (including directories) to the runtime classpath:


ClassLoader currentThreadClassLoader
= Thread.currentThread().getContextClassLoader();

// Add the conf dir to the classpath
// Chain the current thread classloader
URLClassLoader urlClassLoader
= new URLClassLoader(new URL[]{new File("conf").toURL()},
currentClassLoader);

// Replace the thread classloader - assumes
// you have permissions to do so
Thread.currentThread().setContextClassLoader(urlClassLoader);

// This should work now!
Thread.currentThread().getContextClassLoader().getResourceAsStream("context.xml");


To give credit where it is due the above is a slightly simplified version of a solution from my colleague Randy who came up with it when we worked together on a project earlier this year. His solution makes the assumption that the JVMs system classloader is a URLClassLoader (may not be true for all JVMs) and uses a bit of reflection magic to add to the classpath:


public void addURL(URL url) throws Exception {
URLClassLoader classLoader
= (URLClassLoader) ClassLoader.getSystemClassLoader();
Class clazz= URLClassLoader.class;

// Use reflection
Method method= clazz.getDeclaredMethod("addURL", new Class[] { URL.class });
method.setAccessible(true);
method.invoke(classLoader, new Object[] { url });
}

addURL(new File("conf").toURL());

// This should work now!
Thread.currentThread().getContextClassLoader().getResourceAsStream("context.xml");

1 comment:

Sri Sankaran said...

Cool. That's exactly what I needed.