Java 为不同的 JUnit 测试使用不同的类加载器?
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/42102/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
Using different classloaders for different JUnit tests?
提问by matt b
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.
我有一个单例/工厂对象,我想为其编写 JUnit 测试。工厂方法根据类路径上的属性文件中的类名决定要实例化哪个实现类。如果没有找到属性文件,或者属性文件不包含 classname 键,那么该类将实例化一个默认的实现类。
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?
JUnit(或其他单元测试包)有什么办法可以做到这一点?
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;
}
回答by Mike Stone
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.
当我遇到这种情况时,我更喜欢使用有点黑客的方法。我可能会公开一个受保护的方法,例如 reinitialize(),然后从测试中调用它以有效地将工厂设置回其初始状态。此方法仅适用于测试用例,我将其记录在案。
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).
这有点黑客,但它比其他选项容易得多,并且您不需要第 3 方库来执行此操作(尽管如果您更喜欢更清洁的解决方案,则可能有某种 3rd 方工具可供您使用)可以用)。
回答by Cem Catikkas
You can use Reflection to set myClassImpl
by calling instantiateMyClass()
again. Take a look at this answerto see example patterns for playing around with private methods and variables.
您可以myClassImpl
通过instantiateMyClass()
再次调用使用反射进行设置。看看这个答案,看看使用私有方法和变量的示例模式。
回答by AutomatedMike
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
使用 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.
将@RunWith(SeparateClassloaderTestRunner.class)
注释添加到您的测试类。
Create the SeparateClassloaderTestRunner
to look like this:
创建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);
}
}
}
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.
此外,此解决方案打破了依赖类加载技巧(例如 Mockito)的任何其他内容。
回答by barclar
If executing Junit via the Ant taskyou 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.
如果通过Ant 任务执行 Junit,您可以设置fork=true
在它自己的 JVM 中执行每一类测试。还将每个测试方法放在自己的类中,它们将分别加载和初始化自己的MyClass
. 这是极端但非常有效的。
回答by Neeme Praks
Below you can find a sample that does not need a separate JUnit test runner and works also with classloading tricks such as Mockito.
您可以在下面找到一个示例,它不需要单独的 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, 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);
}
}
}