Подтвердить что ты не робот

Использование разных загрузчиков классов для разных тестов JUnit?

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

Поскольку Factory хранит статический экземпляр Singleton для использования после его создания, чтобы иметь возможность протестировать логику "failover" в методе Factory, мне нужно будет запускать каждый метод тестирования в другом Загрузчик классов.

Есть ли способ с JUnit (или с другим модулем тестирования модулей)?

edit: вот какой код 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;
}
4b9b3361

Ответ 1

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

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

Примечание. Я должен был сделать это, чтобы проверить код, запущенный в устаревшей структуре, которую я не мог изменить. Учитывая выбор, я бы уменьшил использование статики и/или поставил тестовые крючки, чтобы система была reset. Это может быть не очень красиво, но это позволяет мне проверять очень много кода, что было бы трудно в противном случае.

Также это решение разрушает все остальное, что полагается на трюки для загрузки классов, такие как Mockito.

Ответ 2

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

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

Ответ 3

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

Ответ 4

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

Ответ 5

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

  }

}