Пользовательский ClassLoader для прокси-класса статической библиотеки в тестах JUnit

Вопрос
Можно ли предоставить реализацию класса с использованием пользовательского ClassLoader, который будет правильно использоваться в статическом контексте?

Предыстория
Я работаю с платформой, которая рекомендует использовать статический класс для подключения зависимостей.
Это работает примерно так..

public class MyClass {

    @ThisIsADependency
    private MyDependency myDependency;

    public void initialize() {
        FrameworkProvidedDependencyResolver.resolveDependencies(this);
    }

}

Как и следовало ожидать, это кошмар для тестирования, и, конечно же, FrameworkProvidedDependencyResolver (не настоящее имя) выдает NullPointerException, если только он не вызывается из активной среды фреймворка, что невозможно из JUnit.

Что я хотел бы сделать, так это предоставить пользовательский ClassLoader, который я могу использовать в тестах JUnit, чтобы предоставить пользовательский FrameworkProvidedDependencyResolver, который подключает фиктивные зависимости или что-то еще.

Итак, вот как я хотел бы, чтобы мои модульные тесты выглядели:

@RunWith(MyTestRunner.class)
public class TestMyClass {

    @Test
    public void testInitialization() {
        MyClass myClass = new MyClass();
        myClass.initialize();
        // not much of a test, I know
    }

}

MyTestRunner — это то место, где я предпочитаю использовать свой собственный ClassLoader..

public class MyTestRunner extends BlockJUnit4ClassRunner {

    public MyTestRunner(Class<?> clazz) throws InitializationError {
        super(getFromMyClassLoader(clazz));
    }

    private static Class<?> getFromMyClassLoader(Class<?> clazz) throws InitializationError {
        try {
            ClassLoader testClassLoader = new MyClassLoader();
            return Class.forName(clazz.getName(), true, testClassLoader);
        } catch (ClassNotFoundException e) {
            throw new InitializationError(e);
        }
    }

}

Спасибо @AutomatedMike.

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

public class ZKTestClassLoader extends URLClassLoader {

    public ZKTestClassLoader() {
        super(((URLClassLoader) getSystemClassLoader()).getURLs());
    }

    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        Class<?> loadedClass = findLoadedClass(name);
        if (loadedClass != null) {
            return loadedClass;
        }
        System.out.println("Loading " + name);
        if (name.startsWith("my.test.classes")) {
            // Make sure we use MyClassLoader to load the test classes,
            // thus any classes it loads (eg: MyClass) will come through here.
            return super.findClass(name);
        } else if (name.endsWith("FrameworkProvidedDependencyResolver")) {
            // What should do we do here?
        }
        return super.loadClass(name);
    }

}

Итак, теперь мы можем загрузить пользовательский FrameworkProvidedDependencyResolver вместо того, который предоставляется фреймворком... но как мне это сделать?

Я могу проигнорировать запрос «FrameworkProvidedDependencyResolver» и вернуть другой класс, скажем, «MyMockFrameworkProvidedDependencyResolver». Это нормально, но когда MyClass.initialize вызывает FrameworkProvidedDependencyResolver из статического контекста, мы получаем NoClassDefFoundError. Имеет смысл.

Я могу попробовать назвать MyMockFrameworkProvidedDependencyResolver так же, как настоящий FrameworkProvidedDependencyResolver, и поместить его в другой пакет (например: i.hate.my.framework.FrameworkProvidedDependencyResolver). Это также не работает, так как MyClass специально смотрит на настоящего FrameworkProvidedDependencyResolver, упаковку и все такое.

Я могу попробовать назвать свой класс настоящим FrameworkProvidedDependencyResolver и поместить его в тот же пакет, что и мой фреймворк... но теперь мне даже не нужен ClassLoader. JVM будет сбита с толку этими двумя и загрузит то, что подходит по пути к классам, вероятно, моему. Проблема здесь в том, что теперь это относится ко всем тестам; не то решение, которое я ищу.

Наконец, я не могу использовать Proxy потому что FrameworkProvidedDependencyResolver не interface.

Хорошо, переформулирую мой вопрос:
Можно ли предоставить реализацию класса с использованием пользовательского ClassLoader, который будет правильно использоваться из статического контекста? Возможно, у меня есть класс на его собственном уникальном пути с уникальным именем, которое я могу редактировать по мере его загрузки, чтобы он отображался в JVM с ожидаемым путем и именем, которые я пытаюсь переопределить? Любое другое решение, конечно, приветствуется.


person Sean Connolly    schedule 10.07.2013    source источник


Ответы (2)


Во-первых, вы должны задаться вопросом, действительно ли необходимо имитировать статический метод resolveDependencies(). Вместо этого вы можете делегировать initialize() другому объекту/методу и издеваться над этим. Или вы можете использовать полумаску (например, через Mockito-шпион), которая имитирует метод initialize. на тестируемом классе. Или вы можете сделать MyClass настолько маленьким (переместив функциональность в другие классы), что его больше не нужно тестировать (юнит). Или, может быть, вы можете предотвратить вызов initialize() и вместо этого выполнить свою собственную инициализацию.

Если вы пришли к выводу, что вам абсолютно необходимо имитировать статические методы, во что бы то ни стало используйте mock-фреймворк, который это поддерживает, а не изобретайте собственное решение (что будет сложно). Двумя известными претендентами на этом рынке являются PowerMock и JMockit.

PS: Мне непонятно, почему вы намеренно вызываете метод initialize из теста. Каковы намерения?

person Peter Niederwieser    schedule 10.07.2013

Я нашел ваш вопрос, пытаясь разобраться с загрузчиками классов, и ваш код помог мне понять, где я ошибался. Затем я поиграл с вашим кодом, чтобы добиться того, о чем вы, кажется, просите. И хотя Питер Недервизер прав во всех отношениях и использование фиктивных фреймворков (PowerMock, JMockit) было бы правильным, просто для полноты картины вот моя версия loadClass, которая работает для меня:

@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
    Class<?> loadedClass = findLoadedClass(name);
    if (loadedClass != null) {
        return loadedClass;
    }
    System.out.println("Loading " + name);
    if (name.endsWith("FrameworkProvidedDependencyResolver")) {
        try {
            InputStream is =
                super
                    .getResourceAsStream("FrameworkProvidedDependencyResolver.class");
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            for (int i = is.read(); i != -1; i = is.read()) {
                baos.write(i);
            }
            byte[] buf = baos.toByteArray();
            return defineClass(name, buf, 0, buf.length);
        } catch (Exception e) {
            throw new ClassNotFoundException("", e);
        }
    } else if (name.startsWith("my.test.classes")) {
        // Make sure we use MyClassLoader to load the test classes,
        // thus any classes it loads (eg: MyClass) will come through here.
        return super.findClass(name);
    }
    return super.loadClass(name);
}

FrameworkProvidedDependencyResolver.class — это скомпилированный модифицированный класс. Он должен иметь тот же пакет и имя, что и исходный FrameworkProvidedDependencyResolver, поэтому может быть немного сложно заставить его сосуществовать в одном проекте с исходным FrameworkProvidedDependencyResolver: по крайней мере, IDE будет недовольна. Я только что создал класс, отредактировал его, затем взял его скомпилированный файл класса из папки сборки IDE, поместил его в корень классов (поэтому я делаю super.getResourceAsStream("FrameworkProvidedDependencyResolver.class") без указания пути), а затем переименовал файл java во что-то другое . В реальной среде способ получения/сохранения байт-кода, вероятно, будет другим (и, возможно, не стоит пытаться).

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

person starikoff    schedule 11.06.2015