在 JUnit 测试期间,静态初始值设定项不 运行

Static initializer doesn't run during JUnit tests

我这里有一个有趣的 JUnit 问题 (JUnit 4.12)。我有一个只有静态方法的基 class。由于它们的使用方式,它们必须是静态的。我从基础 class 继承了其他 classes。所以,如果基数 class 是 Base,我们有 ChildAChildB.

大部分方法都包含在基础class中,但它必须知道它实际上是哪个child(仅仅调用方法作为基础class是无效的) .这是通过基 class:

中的静态数据成员完成的
public class Base {

    protected static ChildType myType = ChildType.Invalid;
    ...
}    

每个 child 通过静态初始化程序设置数据成员,因此:

static {
    myType = ChildType.ChildA;
}

然后当方法被调用时,基础class知道它是什么类型并加载适当的配置(类型实际上是一个配置名称)。

在 运行 应用程序时,这一切都完美无缺。在调试器中单步执行它并通过日志消息,我可以看到设置了适当的类型,并且方法根据 child 类型加载了适当的配置。

使用JUnit时出现问题。我们有一些 JUnit 测试来测试每个基本 class 方法。由于仅在基 class 上调用方法是无效的,我们在 child class 上调用方法,因此:

bool result = ChildA.methodTwo();

这个“'always fails'”。为什么?静态初始值设定项永远不会被调用。 运行将代码作为应用程序调用时,它会被调用,大家都很高兴。当我 运行 它作为 JUnit 测试时,静态初始值设定项被跳过并且方法具有无效数据。 JUnit 做了什么跳过了静态初始值设定项?有解决办法吗?

详情

实际上,我们并没有调用我上面发布的方法。我只是想让这个例子尽可能清楚。实际上,我们有一个使用 Jersey 框架编写的 Web 服务。调用的方法是 REST 端点之一。

@POST
@Produces(MediaType.TEXT_PLAIN)
public String methodPost() {
    ...
    return new String( itWorked ? "success" : "fail" );
}

我们这样称呼它(抱歉语法丑陋,这就是它的工作方式):

@Test
public void testThePost() throws Exception {

    javax.ws.rs.core.Response response = target("restapi/").request().post(Entity.entity(null, MediaType.TEXT_PLAIN));

    assertEquals( 200, response.getStatus() );
}

所有 GET 测试都有效,并且对所有这些测试都调用了静态初始化程序。只是这个 POST 失败了,而且只有在 运行 进行 JUnit 测试时才会失败。

只有一个 Base.myType 字段在所有访问者之间共享:BaseChildAChildB。以下事件序列可能会导致您看到的失败:

  • JUnit 测试调用 ChildA.methodOne() 开始执行,导致 JVM classloader 加载 ChildA.class 并执行其 static 初始化程序块,将 Base.myType 设置为ChildType.ChildA,
  • JUnit 测试调用 ChildB.methodOne() 开始执行,导致 JVM classloader 加载 ClassB.class 并执行 its static初始化块,将 Base.myType 设置为 ChildType.ChildB,然后
  • JUnit 测试调用 ChildA.methodTwo() 开始执行,而不是首先执行 ChildA static 初始化程序块,因为 ChildA 已经被 JVM class 加载加载程序,导致 JUnit 测试失败,因为 Base.myType(因此 ChildA.myType)目前等于 ChildType.ChildB.

基本的设计问题是您的部分代码期望子类型拥有 myType 字段,但该字段实际上由所有子类型共享。

请提供您的 JUnit 测试的顺序 运行 验证上述理论。谢谢!


附录:感谢您在评论中澄清您只有一个 JUnit 测试仅调用 ChildA.methodTwo(),它仅在 Base 中定义,而不是 ChildA。发生的情况很可能是 JVM 决定不需要初始化 ChildA 只是为了调用其父 Base class 的 methodTwo() 方法。 @ShyJ 为 的父子 static 字段 访问提供了一个很好的解释。我相信您的 JUnit 测试中也发生了类似的事情。


附录 2:下面是我的代码建模并重现了所描述的问题 myType 在 JUnit 测试期间具有值 ChildType.Invalid 的最佳状态目前理解:

public enum ChildType {
    Invalid, ChildA
}

public class Base {
    protected static ChildType myType = ChildType.Invalid;

    public static boolean methodTwo() {
        return true;
    }
}

public class ChildA extends Base {
    static {
        myType = ChildType.ChildA;
    }
}

public class ChildATest {
    @org.junit.Test
    public void test() {
        boolean result = ChildA.methodTwo();
        System.out.println("result: " + result);
        System.out.println("Base.myType: " + Base.myType);
    }
}

ChildATest.test()的执行输出:

result: true
Base.myType: Invalid

您正在尝试为静态方法实现多态行为,这是一种存在于其他编程语言中但在 Java 中缺失的语言特性。

[myType is] a protected member of the base class

依靠静态初始值设定项来设置基class中的静态字段是非常脆弱的,因为多个子classes "compete"用于基[=42=中的单个字段].这 "locks in" 基 class 的行为变成了其初始化程序 运行 最后的子 class 所需的行为。在其他坏事中,它否定了使用多个 subclasses 以及 Base class 的可能性,并使 ChildA.methodTwo() 到 运行 功能成为可能专为 ChildB.methodTwo() 而设计。事实上,没有ChildA.methodTwo()ChildB.methodTwo(),只有Base.methodTwo()依赖静态初始化序列为它准备的信息。

这个问题有几种解决方法。一种可能性是将 Class<Child###> 对象传递给基础方法 class:

class Base {
    public static void method1(Class childConfig, String arg) {
        ...
    }
    public static void method2(Class childConfig, int arg1, String arg2) {
        ...
    }
}

现在调用者需要改变

ChildA.method1("hello");
ChildA.method2(42, "world");

Base.method1(ChildA.class, "hello");
Base.method2(ChildA.class, 42, "world");

另一种解决方案是用非静态替换静态实现,并使用 "regular" 多态行为与派生 classes:

中创建的单例相结合
class Base {
    protected Base(Class childConfig) {
        ...
    }
    public void method1(String arg) {
        ...
    }
    public void method2(int arg1, String arg2) {
        ...
    }
}
class ChildA extends Base {
    private static final Base inst = new ChildA();
    private ChildA() {
        super(ChildA.class);
    }
    public static Base getInstance() {
        return inst;
    }
    ... // Override methods as needed
}
class ChildB extends Base {
    private static final Base inst = new ChildB();
    private ChildB() {
        super(ChildB.class);
    }
    public static Base getInstance() {
        return inst;
    }
    ... // Override methods as needed
}

并致电

ChildA.getInstance().method1("hello");
ChildA.getInstance().method2(42, "world");

我决定尝试@Arkdiy 的建议,并在 child classes 中使用 pass-through 方法。

让我重申一下:我的代码在 运行 作为应用程序时可以完美运行。只有当 运行通过 JUnit 连接时才会失败

所以现在我有类似下面的内容:

public class BaseClass {

    protected static ChildType myType = ChildType.Invalid;

    ...

    public static boolean methodTwoBase() {
        ...
    }
}

public class ChildA extends BaseClass {

    public static boolean methodOne() {
        ...
    }

    public static boolean methodTwo() {

        myType = ChildType.ChildA;
        return methodTwoBase();
    }
}

public class ChildB extends BaseClass {

    public static boolean methodOne() {
        ...
    }

    public static boolean methodTwo() {

        myType = ChildType.ChildB;
        return methodTwoBase();
    }
}

由于我无法覆盖静态方法,因此基础 class 中的方法版本具有不同的签名(methodTwoBase() 而不是 methodTwo)。我将它作为常规应用程序和在 JUnit 中进行了尝试,它可以两种方式工作。

有点有趣的问题,我责怪 JUnit。感谢所有的输入!