8

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

大多数方法都包含在基类中,但它必须知道它实际上是哪个子类(只是调用这些方法作为基类是无效的)。这是通过基类中的静态数据成员完成的:

public class Base {

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

每个孩子通过静态初始化器设置数据成员,因此:

static {
    myType = ChildType.ChildA;
}

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

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

使用 JUnit 时会出现问题。我们有一些 JUnit 测试来测试每个基类方法。由于只调用基类的方法是无效的,我们调用子类的方法,因此:

bool result = ChildA.methodTwo();

这“总是失败”。为什么?静态初始化程序永远不会被调用。当将代码作为应用程序运行时,它会被调用,每个人都很高兴。当我将它作为 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 测试时才会失败。

4

3 回答 3

4

You are trying to implement polymorphic behavior for static methods, a language feature that is present in other programming languages, but is missing in Java.

[myType is] a protected member of the base class

Relying on static initializers to set static fields in the base class is very fragile, because multiple subclasses "compete" for a single field in the base class. This "locks in" the behavior of the base class into the behavior desirable for the subclass whose initializer ran last. Among other bad things, it denies a possibility of using multiple subclasses along with the Base class, and makes it possible for ChildA.methodTwo() to run functionality designed for ChildB.methodTwo(). In fact, there is no ChildA.methodTwo() and ChildB.methodTwo(), there's only Base.methodTwo() that relies on information prepared for it by the static initialization sequence.

There are several solutions to this problem. One possibility is to pass Class<Child###> object to methods of the base class:

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

Now the callers would need to change

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

to

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

Another solution would be to replace static implementation with non-static, and use "regular" polymorphic behavior in conjunction with singletons created in derived 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
}

and call

ChildA.getInstance().method1("hello");
ChildA.getInstance().method2(42, "world");
于 2017-10-24T16:12:25.893 回答
2

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

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

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

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


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


附录 2:以下是我的代码建模和重现所描述的在 JUnit 测试期间myType具有价值的问题,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
于 2017-10-24T16:00:09.067 回答
0

我决定尝试@Arkdiy 的建议,并在子类中使用传递方法。

让我重申一下:正如我所拥有的那样,代码在作为应用程序运行时可以完美运行。只有通过 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();
    }
}

由于我无法覆盖静态方法,因此基类中的方法版本具有不同的签名(methodTwoBase()而不是methodTwo)。我将它作为常规应用程序在 JUnit 中尝试过,它可以双向工作。

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

于 2017-10-24T17:47:35.183 回答