Javassist API с Junit

Я пытаюсь изменить некоторые определения сторонних классов перед каждым тестом, чтобы имитировать разные результаты. Мне приходится использовать что-то вроде javassist, потому что расширение классов иногда просто невозможно из-за модификаторов доступа. Вот пример того, что я пытаюсь сделать с javassist и junit вместе:

public class SimulatedSession extends SomeThirdParty {

    private boolean isJoe = false;
    public SimulatedSession(final boolean isJoe) {
        this.isJoe = isJoe;
    }

    @Override
    public void performThis() {
        final ClassPool classPool = ClassPool.getDefault();
        final CtClass internalClass = classPool.get("some.package.Class");
        final CtMethod callMethod = internalClass.getDeclaredMethod("doThis");
        if (isJoe) {
            callMethod.setBody("{System.out.println(\"Joe\");}");
        } else {
            callMethod.setBody("{System.out.println(\"mik\");}");
        }
        internalClass.toClass();
    }
}

@Test
public void firstTest() {
     SimulatedSession toUse = new SimulatedSession(false);
     // do something with this object and this flow
}

@Test
public void nextTest() {
     SimulatedSession toUse = new SimulatedSession(true);
     // do something with this object and this flow
}

если я запускаю каждый тест по отдельности, я могу нормально запустить код. Когда я запускаю их с помощью набора модулей, один тест за другим, я получаю «зависшую проблему с классом». Чтобы обойти это, я просматриваю этот сообщение, однако должен признать, что не уверен, как можно использовать другой пул классов для решения проблемы.


person angryip    schedule 09.12.2016    source источник
comment
ваши тесты даже не компилируются, я думаю, вы имели в виду new SimulatedSession(false).performThis();   -  person Nicolas Filotto    schedule 09.12.2016
comment
@NicolasFilotto Я имел в виду скорее пример, но я обновил вопрос, чтобы можно было увидеть, что я пытаюсь сделать. Вызов метода perfomrThis будет вызван, вы правы.   -  person angryip    schedule 09.12.2016


Ответы (3)


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

Чтобы ваши модульные тесты прошли, мне пришлось:

  1. Создайте свой собственный временный ClassLoader, который сможет загружать some.package.Class (который я заменил на javassist.MyClass для целей тестирования), и который будет реализован таким образом, что он сначала попытается загрузить класс из него перед родительским CL.
  2. Установите мой собственный ClassLoader в качестве контекста ClassLoader.
  3. Измените код SimulatedSession#performThis(), чтобы иметь возможность получать экземпляр класса, созданный этим методом, и вызывать internalClass.defrost(), чтобы предотвратить "проблему с замороженным классом".
  4. Вызовите путем отражения метод doThis(), чтобы убедиться, что у меня есть разные выходные данные, используя экземпляр класса, возвращенный SimulatedSession#performThis(), чтобы убедиться, что используемый класс был загружен с моим ClassLoader.

Итак, если предположить, что мой класс javassist.MyClass:

package javassist;

public class MyClass {
    public void doThis() {

    }
}

Метод SimulatedSession#performThis() с модификациями:

public Class<?> performThis() throws Exception {
    final ClassPool classPool = ClassPool.getDefault();
    final CtClass internalClass = classPool.get("javassist.MyClass");
    // Prevent the "frozen class issue"
    internalClass.defrost();
    ...
    return internalClass.toClass();
}

Модульные тесты:

// The custom CL
private URLClassLoader cl;
// The previous context CL
private ClassLoader old;

@Before
public void init() throws Exception {
    // Provide the URL corresponding to the folder that contains the class
    // `javassist.MyClass`
    this.cl = new URLClassLoader(new URL[]{new File("target/classes").toURI().toURL()}){
        protected Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException {
            try {
                // Try to find the class for this CL
                return findClass(name);
            } catch( ClassNotFoundException e ) {
                // Could not find the class so load it from the parent
                return super.loadClass(name, resolve);
            }
        }
    };
    // Get the current context CL and store it into old
    this.old = Thread.currentThread().getContextClassLoader();
    // Set the custom CL as new context CL
    Thread.currentThread().setContextClassLoader(cl);
}

@After
public void restore() throws Exception {
    // Restore the context CL
    Thread.currentThread().setContextClassLoader(old);
    // Close the custom CL
    cl.close();
}


@Test
public void firstTest() throws Exception {
    SimulatedSession toUse = new SimulatedSession(false);
    Class<?> c = toUse.performThis();
    // Invoke doThis() by reflection
    Object o2 = c.newInstance();
    c.getMethod("doThis").invoke(o2);
}

@Test
public void nextTest() throws Exception {
    SimulatedSession toUse = new SimulatedSession(true);
    Class<?> c = toUse.performThis();
    // Invoke doThis() by reflection
    Object o2 = c.newInstance();
    c.getMethod("doThis").invoke(o2);
}

Вывод:

mik
Joe
person Nicolas Filotto    schedule 09.12.2016
comment
хороший! действительно впечатляющее решение. - person angryip; 09.12.2016

Взгляните на retransformer. Это библиотека на основе Javassist, которую я написал для запуска тестов, подобных этой. Это немного короче, чем использование сырого Javassist.

person Nicholas    schedule 10.12.2016

Возможно другой подход. У нас была аналогичная проблема, когда мы однажды издевались над зависимостью — мы не могли ее сбросить. Итак, мы сделали следующее: Перед каждым тестом мы заменяем «живой» экземпляр нашим макетом. После тестов восстанавливаем живой экземпляр. Поэтому я предлагаю вам заменить модифицированный экземпляр вашего стороннего кода для каждого теста.

@Before
public void setup()
{
    this.liveBeanImpl = (LiveBean) ReflectionTools.getFieldValue(this.beanToTest, "liveBean");
    ReflectionTools.setFieldValue(this.beanToTest, "liveBean", new TestStub());
}


@After
public void cleanup()
{
    ReflectionTools.setFieldValue(this.beanToTest, "liveBean", his.liveBeanImpl);
}

setFieldValue выглядит так:

public static void setFieldValue(Object instanceToModify, String fieldName, Object valueToSet)
{
    try
    {
        Field declaredFieldToSet = instanceToModify.getClass().getDeclaredField(fieldName);
        declaredFieldToSet.setAccessible(true);
        declaredFieldToSet.set(instanceToModify, valueToSet);
        declaredFieldToSet.setAccessible(false);
    }
    catch (Exception exception)
    {
        String className = exception.getClass().getCanonicalName();
        String message = exception.getMessage();
        String errorFormat = "\n\t'%s' caught when setting value of field '%s': %s";
        String error = String.format(errorFormat, className, fieldName, message);
        Assert.fail(error);
    }
}

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

person actc    schedule 09.12.2016