使用不同的类加载器进行不同的JUnit测试?

我有一个Singleton / Factory对象,我想为其编写一个JUnit测试。 Factory方法根据类路径上属性文件中的类名决定实例化哪个实现类。 如果未找到属性文件,或者属性文件不包含classname键,则该类将实例化默认实现类。

由于工厂在实例化后保持Singleton的静态实例使用,为了能够在Factory方法中测试“故障转移”逻辑,我需要在不同的类加载器中运行每个测试方法。

有没有办法使用JUnit(或其他unit testing包)来做到这一点?

编辑:这里是一些正在使用的Factory代码:

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; } 

这个问题可能已经过时了,但因为这是我遇到这个问题时最接近的答案,但我会描述我的解决方案。

使用JUnit 4

拆分你的测试,以便每个类有一个测试方法(这个解决方案只更改类之间的类加载器,而不是方法之间,因为父运行器每个类收集一次所有方法)

@RunWith(SeparateClassloaderTestRunner.class)注释添加到测试类中。

创建SeparateClassloaderTestRunner ,如下所示:

 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); } } } 

注意我必须这样做来测试在遗留框架中运行的代码,我无法改变。 鉴于选择,我会减少使用静态和/或放入测试挂钩以允许系统重置。 它可能不是很漂亮,但它允许我测试很多代码,否则很难。

此解决方案还打破了依赖于类加载技巧的其他任何东西,例如Mockito。

当我遇到这种情况时,我更喜欢使用什么有点黑客。 我可能会反而暴露一个受保护的方法,例如reinitialize(),然后从测试中调用它来有效地将工厂设置回其初始状态。 此方法仅适用于测试用例,我将其记录下来。

这有点像黑客,但它比其他选项容易得多,你不需要第三方库来做(虽然如果你更喜欢更清洁的解决方案,那里可能有某种第三方工具你可以用)。

您可以使用Reflection通过再次调用instantiateMyClass()来设置myClassImpl 。 看看这个答案 ,看看用于处理私有方法和变量的示例模式。

如果通过Ant任务执行Junit,您可以设置fork=true来执行它自己的JVM中的每一类测试。 还将每个测试方法放在它自己的类中,它们将加载并初始化它们自己的MyClass版本。 这是极端但非常有效的。

您可以在下面找到一个不需要单独的JUnit测试运行器的示例,也可以使用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 makeCodeToRunInSeparateClassLoader( String packageName, Class testCodeInterfaceClass, Class 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); } } }