У меня есть объект Singleton / Factory, для которого я хотел бы написать тест JUnit. Метод Factory решает, какой реализующий класс создать на основе имени класса в файле свойств в пути к классам. Если файл свойств не найден или файл свойств не содержит ключа имени класса, тогда класс создаст экземпляр класса реализации по умолчанию.
Поскольку фабрика хранит статический экземпляр синглтона для использования после его создания, чтобы иметь возможность протестировать логику «аварийного переключения» в методе Factory, мне нужно будет запускать каждый тестовый метод в другом загрузчике классов.
Есть ли способ сделать это с помощью JUnit (или другого пакета модульного тестирования)?
изменить: вот некоторые из используемых заводских кодов:
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;
}




Когда я сталкиваюсь с подобными ситуациями, я предпочитаю использовать что-то вроде взлома. Вместо этого я мог бы открыть защищенный метод, такой как reinitialize (), а затем вызвать его из теста, чтобы вернуть фабрику в исходное состояние. Этот метод существует только для тестовых случаев, и я документирую его как таковой.
Это немного похоже на взлом, но это намного проще, чем другие варианты, и для этого вам не понадобится сторонняя библиотека (хотя, если вы предпочитаете более чистое решение, вероятно, есть какие-то сторонние инструменты, которые вы можно было использовать).
Вы можете использовать Reflection, чтобы установить myClassImpl, снова позвонив instantiateMyClass(). Взгляните на этот ответ, чтобы увидеть примеры шаблонов для игры с частными методами и переменными.
Этот вопрос может быть старым, но поскольку это был ближайший ответ, который я нашел, когда у меня возникла эта проблема, я бы описал свое решение.
Использование 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.
Вместо поиска "org.mypackages". в loadClass () вы также можете сделать что-то вроде этого: return name.startsWith ("java") || name.startsWith ("org.junit")? super.loadClass (имя): super.findClass (имя);
Как сделать этот ответ принятым? Это дает ответ на вопрос, тогда как текущий «принятый ответ» - нет.
Спасибо за ответ. Я пытаюсь воссоздать это, но все мои классы все равно загружаются родительским загрузчиком классов, даже если они из исключенного пакета?
Возможно, ваш TestClassloader ничего не находит и возвращается к родительскому объекту, который затем вернет ранее кэшированную копию. Не совсем уверен, почему - вы изменили что-нибудь еще, связанное с загрузкой классов в процессе?
Обратите внимание, что это не работает в Java 9+, поскольку (URLClassLoader)getSystemClassLoader() выдаст исключение (blog.codefx.org/java/java-9-migration-guide/…)
ну ответ 8 лет - хотите предложить обновление?
При выполнении 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, 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);
}
}
}
Синглтоны приводят к целому миру боли. Избегайте синглтонов, и ваш код станет намного проще тестировать и станет еще лучше.