2008-09-03 6 views
26

У меня есть объект Singleton/Factory, который я бы хотел написать для теста JUnit. Метод Factory определяет, какой класс реализации должен создавать экземпляр на основе имени класса в файле свойств в пути к классам. Если файл свойств не найден или файл свойств не содержит ключ класса, класс будет создавать экземпляр класса реализации по умолчанию.Использование разных загрузчиков классов для разных тестов JUnit?

Поскольку фабрика хранит статический экземпляр Singleton для использования после его создания, чтобы иметь возможность протестировать логику «failover» в методе 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; 
} 
+0

Одиночные игры приводят к целому миру боли. Избегайте синглтонов, и ваш код становится намного легче тестировать, и все будет круче. – 2008-09-05 15:42:11

ответ

3

Когда я бегу в этих рода ситуациях я предпочитаю использовать то, что это немного рубить. Вместо этого я мог бы открыть защищенный метод, такой как reinitialize(), а затем вызвать это из теста, чтобы эффективно вернуть завод в исходное состояние. Этот метод существует только для тестовых случаев, и я документирую его как таковой.

Это немного взломанный, но это намного проще, чем другие варианты, и вам не понадобится сторонняя библиотека, чтобы сделать это (хотя, если вы предпочитаете более чистое решение, возможно, есть какая-то сторонняя сторона инструменты там вы могли бы использовать).

3

Вы можете использовать Reflection для установки myClassImpl, позвонив по телефону instantiateMyClass(). Взгляните на this answer, чтобы увидеть примеры шаблонов для игр с частными методами и переменными.

36

Этот вопрос может быть старым, но поскольку это был ближайший ответ, который я нашел, когда у меня возникла эта проблема, я бы описал свое решение.

Использование 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.

+0

Вместо поиска "org.mypackages." в loadClass() вы также можете сделать что-то вроде этого: return name.startsWith ("java") || name.startsWith ("org.junit")? super.loadClass (name): super.findClass (name); – Gilead 2012-10-02 10:30:12

2

Если выполнение JUnit вы можете установить fork=true выполнить каждый класс тестов в своей собственной виртуальной машины Java с помощью Ant task. Также поместите каждый метод тестирования в свой класс, и каждый из них загрузит и инициализирует свою собственную версию MyClass. Это экстремально, но очень эффективно.

0

Ниже вы можете найти образец, который не нуждается в отдельном тестировщике 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); 
    } 

    } 

}