Использование разных загрузчиков классов для разных тестов JUnit? - PullRequest
27 голосов
/ 03 сентября 2008

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

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

Ответы [ 5 ]

37 голосов
/ 08 февраля 2012

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

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

3 голосов
/ 03 сентября 2008

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

3 голосов
/ 03 сентября 2008

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

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

2 голосов
/ 23 июля 2013

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

0 голосов
/ 08 декабря 2015

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

  }

}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...