Log4j Logger.getLogger(Класс) выбрасывает NPE при работе с jMockit и Cobertura

Я обнаружил странное взаимодействие между cobertura-maven-plugin 2.6 и jmockit 1.8. У определенного шаблона в нашем производственном коде есть класс со множеством статических методов, которые эффективно оборачивают другой класс, который действует как синглтон. Написание модульных тестов для этих классов шло хорошо, пока я не попытался запустить отчеты о покрытии с помощью cobertura, когда эта ошибка появилась:

java.lang.ExceptionInInitializerError
    at java.lang.reflect.Constructor.newInstance(Constructor.java:526)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.lang.reflect.Method.invoke(Method.java:606)
    at org.apache.maven.surefire.junit4.JUnit4Provider.execute(JUnit4Provider.java:252)
    at org.apache.maven.surefire.junit4.JUnit4Provider.executeTestSet(JUnit4Provider.java:141)
    at org.apache.maven.surefire.junit4.JUnit4Provider.invoke(JUnit4Provider.java:112)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.lang.reflect.Method.invoke(Method.java:606)
    at org.apache.maven.surefire.util.ReflectionUtils.invokeMethodWithArray(ReflectionUtils.java:189)
    at org.apache.maven.surefire.booter.ProviderFactory$ProviderProxy.invoke(ProviderFactory.java:165)
    at org.apache.maven.surefire.booter.ProviderFactory.invokeProvider(ProviderFactory.java:85)
    at org.apache.maven.surefire.booter.ForkedBooter.runSuitesInProcess(ForkedBooter.java:115)
    at org.apache.maven.surefire.booter.ForkedBooter.main(ForkedBooter.java:75)
Caused by: java.lang.NullPointerException
    at com.example.foo.MySingleton.<clinit>(MySingleton.java:7)
    ... 13 more

Это тогда приводит к NoClassDefFoundError и неспособность инициализировать класс синглтона. Вот полный SSCCE (самый короткий, который я могу получить это), который копирует ошибку; строка 7 MySingleton является Logger.getLogger(),

Вот такой "синглтон"...

package com.example.foo;

import org.apache.log4j.Logger;

public class MySingleton {

    private static final Logger LOG = Logger.getLogger(MySingleton.class);

    private boolean inited = false;
    private Double d;

    MySingleton() {
    }

    public boolean isInited() {
        return inited;
    }

    public void start() {
        inited = true;
    }

    public double getD() {
        return d;
        }
}

И статический класс...

package com.example.foo;

import org.apache.log4j.Logger;

public class MyStatic {

    private static final Logger LOGGER = Logger.getLogger(MyStatic.class);

    private static MySingleton u = new MySingleton();

    public static double getD() {
        if (u.isInited()) {
            return u.getD();
        }
        return 0.0;
    }

}

И тест, который ломает все...

package com.example.foo;

import mockit.Expectations;
import mockit.Mocked;
import mockit.Tested;

import org.junit.Test;

public class MyStaticTest {

    @Tested MyStatic myStatic;

    @Mocked MySingleton single;

    @Test
    public void testThatBombs() {
        new Expectations() {{
            single.isInited(); result = true;
            single.getD(); /*result = 1.2;*/
        }};

//        Deencapsulation.invoke(MyStatic.class, "getD");
        MyStatic.getD();

    }

}

И мавен пом

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.example.foo</groupId>
    <artifactId>test</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>Test</name>

    <dependencies>
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.16</version>
        </dependency>

        <dependency>
            <groupId>org.jmockit</groupId>
            <artifactId>jmockit</artifactId>
            <version>1.8</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.11</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <pluginManagement>
            <plugins>
                <plugin>
                    <groupId>org.codehaus.mojo</groupId>
                    <artifactId>cobertura-maven-plugin</artifactId>
                    <version>2.6</version>
                </plugin>
            </plugins>
        </pluginManagement>
    </build>

</project>

Подведем итоги: при запуске обычного юнит-теста (mvn clean test) вышеуказанные тесты просто отлично; когда бегать с кобертурой (mvn clean cobertura:cobertura) это бросает неприятный набор исключений, показанных наверху. Очевидно, где-то ошибка, но чья?

1 ответ

Решение

Причиной этой проблемы является не столько ошибка, сколько отсутствие надежности в JMockit при насмешках над классом, содержащим статический инициализатор. Следующая версия JMockit (1.9) будет улучшена в этом пункте (у меня уже есть рабочее решение).

Кроме того, проблема не возникла бы, если бы Cobertura пометил свои сгенерированные методы (четыре из них с именами, начинающимися с "__cobertura_", добавленные к каждому инструментированному классу) как "синтетические", так что JMockit проигнорировал бы их при издевательстве над инструментами Cobertura. учебный класс. Во всяком случае, к счастью, в этом нет необходимости.

На данный момент есть два простых способа избежать этой проблемы:

  1. Убедитесь, что любой класс, который нужно смоделировать, уже был инициализирован JVM к моменту начала теста. Это можно сделать, создав его экземпляр или вызвав для него статический метод.
  2. Объявите фиктивное поле или фиктивный параметр как @Mocked(stubOutClassInitialization = true),

Оба обходных пути предотвращают NPE, который в противном случае был бы вызван изнутри статического инициализатора класса, который модифицирован Cobertura (чтобы увидеть эти модификации байт-кода, вы можете использовать javap инструмент JDK, на занятиях под target/generated-classes каталог).

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