Using different classloaders for different JUnit tests?
Asked Answered
I

5

34

I have a Singleton/Factory object that I'd like to write a JUnit test for. The Factory method decides which implementing class to instantiate based upon a classname in a properties file on the classpath. If no properties file is found, or the properties file does not contain the classname key, then the class will instantiate a default implementing class.

Since the factory keeps a static instance of the Singleton to use once it has been instantiated, to be able to test the "failover" logic in the Factory method I would need to run each test method in a different classloader.

Is there any way with JUnit (or with another unit testing package) to do this?

edit: here is some of the Factory code that is in use:

private static MyClass myClassImpl = instantiateMyClass();

private static MyClass instantiateMyClass() {
    MyClass newMyClass = null;
    String className = null;

    try {
        Properties props = getProperties();
        className = props.getProperty(PROPERTY_CLASSNAME_KEY);

        if (className == null) {
            log.warn("instantiateMyClass: Property [" + PROPERTY_CLASSNAME_KEY
                    + "] not found in properties, using default MyClass class [" + DEFAULT_CLASSNAME + "]");
            className = DEFAULT_CLASSNAME;
        }

        Class MyClassClass = Class.forName(className);
        Object MyClassObj = MyClassClass.newInstance();
        if (MyClassObj instanceof MyClass) {
            newMyClass = (MyClass) MyClassObj;
        }
    }
    catch (...) {
        ...
    }

    return newMyClass;
}

private static Properties getProperties() throws IOException {

    Properties props = new Properties();

    InputStream stream = Thread.currentThread().getContextClassLoader().getResourceAsStream(PROPERTIES_FILENAME);

    if (stream != null) {
        props.load(stream);
    }
    else {
        log.error("getProperties: could not load properties file [" + PROPERTIES_FILENAME + "] from classpath, file not found");
    }

    return props;
}
Iridize answered 3/9, 2008 at 16:44 Comment(1)
Singletons lead to a whole world of hurt. Avoid singletons and your code becomes much easier to test and just all-round nicer.Tellford
C
46

This question might be old but since this was the nearest answer I found when I had this problem I though I'd describe my solution.

Using JUnit 4

Split your tests up so that there is one test method per class (this solution only changes classloaders between classes, not between methods as the parent runner gathers all the methods once per class)

Add the @RunWith(SeparateClassloaderTestRunner.class) annotation to your test classes.

Create the SeparateClassloaderTestRunner to look like this:

public class SeparateClassloaderTestRunner extends BlockJUnit4ClassRunner {

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

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

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

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

Note I had to do this to test code running in a legacy framework which I couldn't change. Given the choice I'd reduce the use of statics and/or put test hooks in to allow the system to be reset. It may not be pretty but it allows me to test an awful lot of code that would be difficult otherwise.

Also this solution breaks anything else that relies on classloading tricks such as Mockito.

Chokefull answered 8/2, 2012 at 11:7 Comment(6)
Instead of looking for "org.mypackages." in loadClass() you can also do something like this: return name.startsWith("java") || name.startsWith("org.junit") ? super.loadClass(name) : super.findClass(name);Creuse
How do we make this the accepted answer? This answers the question whereas the current 'accepted answer' does not.Velarde
Thanks for the answer. I'm trying to recreate this, but all my classes get loaded by the parent classloader anyway, even if they're from the excluded package?Abisia
Possibly your TestClassloader is not finding anything and it's falling back to the parent which will then return the previously cached copy. Not entirely sure why - have you changed anything else related to classloading in the process?Chokefull
Note that this doesn't work under Java 9+, since (URLClassLoader)getSystemClassLoader() will throw an exception (blog.codefx.org/java/java-9-migration-guide/…)Marchioness
well the answer is 8 years old - care to suggest an update?Chokefull
D
3

When I run into these sort of situations I prefer to use what is a bit of a hack. I might instead expose a protected method such as reinitialize(), then invoke this from the test to effectively set the factory back to its initial state. This method only exists for the test cases, and I document it as such.

It is a bit of a hack, but it's a lot easier than other options and you won't need a 3rd party lib to do it (though if you prefer a cleaner solution, there probably are some kind of 3rd party tools out there you could use).

Davina answered 3/9, 2008 at 16:50 Comment(0)
O
3

You can use Reflection to set myClassImpl by calling instantiateMyClass() again. Take a look at this answer to see example patterns for playing around with private methods and variables.

Ophidian answered 3/9, 2008 at 17:10 Comment(0)
T
2

If executing Junit via the Ant task you can set fork=true to execute every class of tests in it's own JVM. Also put each test method in its own class and they will each load and initialise their own version of MyClass. It's extreme but very effective.

Transpacific answered 23/7, 2013 at 9:3 Comment(0)
J
1

Below you can find a sample that does not need a separate JUnit test runner and works also with classloading tricks such as Mockito.

package com.mycompany.app;

import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;

import java.net.URLClassLoader;

import org.junit.Test;

public class ApplicationInSeparateClassLoaderTest {

  @Test
  public void testApplicationInSeparateClassLoader1() throws Exception {
    testApplicationInSeparateClassLoader();
  }

  @Test
  public void testApplicationInSeparateClassLoader2() throws Exception {
    testApplicationInSeparateClassLoader();
  }

  private void testApplicationInSeparateClassLoader() throws Exception {
    //run application code in separate class loader in order to isolate static state between test runs
    Runnable runnable = mock(Runnable.class);
    //set up your mock object expectations here, if needed
    InterfaceToApplicationDependentCode tester = makeCodeToRunInSeparateClassLoader(
        "com.mycompany.app", InterfaceToApplicationDependentCode.class, CodeToRunInApplicationClassLoader.class);
    //if you want to try the code without class loader isolation, comment out above line and comment in the line below
    //CodeToRunInApplicationClassLoader tester = new CodeToRunInApplicationClassLoaderImpl();
    tester.testTheCode(runnable);
    verify(runnable).run();
    assertEquals("should be one invocation!", 1, tester.getNumOfInvocations());
  }

  /**
   * Create a new class loader for loading application-dependent code and return an instance of that.
   */
  @SuppressWarnings("unchecked")
  private <I, T> I makeCodeToRunInSeparateClassLoader(
      String packageName, Class<I> testCodeInterfaceClass, Class<T> testCodeImplClass) throws Exception {
    TestApplicationClassLoader cl = new TestApplicationClassLoader(
        packageName, getClass(), testCodeInterfaceClass);
    Class<?> testerClass = cl.loadClass(testCodeImplClass.getName());
    return (I) testerClass.newInstance();
  }

  /**
   * Bridge interface, implemented by code that should be run in application class loader.
   * This interface is loaded by the same class loader as the unit test class, so
   * we can call the application-dependent code without need for reflection.
   */
  public static interface InterfaceToApplicationDependentCode {
    void testTheCode(Runnable run);
    int getNumOfInvocations();
  }

  /**
   * Test-specific code to call application-dependent code. This class is loaded by 
   * the same class loader as the application code.
   */
  public static class CodeToRunInApplicationClassLoader implements InterfaceToApplicationDependentCode {
    private static int numOfInvocations = 0;

    @Override
    public void testTheCode(Runnable runnable) {
      numOfInvocations++;
      runnable.run();
    }

    @Override
    public int getNumOfInvocations() {
      return numOfInvocations;
    }
  }

  /**
   * Loads application classes in separate class loader from test classes.
   */
  private static class TestApplicationClassLoader extends URLClassLoader {

    private final String appPackage;
    private final String mainTestClassName;
    private final String[] testSupportClassNames;

    public TestApplicationClassLoader(String appPackage, Class<?> mainTestClass, Class<?>... testSupportClasses) {
      super(((URLClassLoader) getSystemClassLoader()).getURLs());
      this.appPackage = appPackage;
      this.mainTestClassName = mainTestClass.getName();
      this.testSupportClassNames = convertClassesToStrings(testSupportClasses);
    }

    private String[] convertClassesToStrings(Class<?>[] classes) {
      String[] results = new String[classes.length];
      for (int i = 0; i < classes.length; i++) {
        results[i] = classes[i].getName();
      }
      return results;
    }

    @Override
    public Class<?> loadClass(String className) throws ClassNotFoundException {
      if (isApplicationClass(className)) {
        //look for class only in local class loader
        return super.findClass(className);
      }
      //look for class in parent class loader first and only then in local class loader
      return super.loadClass(className);
    }

    private boolean isApplicationClass(String className) {
      if (mainTestClassName.equals(className)) {
        return false;
      }
      for (int i = 0; i < testSupportClassNames.length; i++) {
        if (testSupportClassNames[i].equals(className)) {
          return false;
        }
      }
      return className.startsWith(appPackage);
    }

  }

}
Jobie answered 8/12, 2015 at 11:2 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.