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

У меня есть объект 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;
}

Синглтоны приводят к целому миру боли. Избегайте синглтонов, и ваш код станет намного проще тестировать и станет еще лучше.

Tom Hawtin - tackline 05.09.2008 19:42
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
Как вычислять биты и понимать побитовые операторы в Java - объяснение с примерами
В компьютерном программировании биты играют важнейшую роль в представлении и манипулировании данными на двоичном уровне. Побитовые операции...
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Поднятие тревоги для долго выполняющихся методов в Spring Boot
Приходилось ли вам сталкиваться с требованиями, в которых вас могли попросить поднять тревогу или выдать ошибку, когда метод Java занимает больше...
Полный курс Java для разработчиков веб-сайтов и приложений
Полный курс Java для разработчиков веб-сайтов и приложений
Получите сертификат Java Web и Application Developer, используя наш курс.
32
1
22 661
5

Ответы 5

Когда я сталкиваюсь с подобными ситуациями, я предпочитаю использовать что-то вроде взлома. Вместо этого я мог бы открыть защищенный метод, такой как 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 (имя);

Gilead 02.10.2012 14:30

Как сделать этот ответ принятым? Это дает ответ на вопрос, тогда как текущий «принятый ответ» - нет.

irbull 26.09.2014 07:30

Спасибо за ответ. Я пытаюсь воссоздать это, но все мои классы все равно загружаются родительским загрузчиком классов, даже если они из исключенного пакета?

EngineerBetter_DJ 18.02.2015 23:22

Возможно, ваш TestClassloader ничего не находит и возвращается к родительскому объекту, который затем вернет ранее кэшированную копию. Не совсем уверен, почему - вы изменили что-нибудь еще, связанное с загрузкой классов в процессе?

AutomatedMike 31.03.2015 12:25

Обратите внимание, что это не работает в Java 9+, поскольку (URLClassLoader)getSystemClassLoader() выдаст исключение (blog.codefx.org/java/java-9-migration-guide/…)

Paul Wagland 20.09.2020 13:23

ну ответ 8 лет - хотите предложить обновление?

AutomatedMike 21.09.2020 16:16

При выполнении 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);
    }

  }

}

Другие вопросы по теме