在 JUnit 测试期间,静态初始值设定项不 运行
Static initializer doesn't run during JUnit tests
我这里有一个有趣的 JUnit 问题 (JUnit 4.12)。我有一个只有静态方法的基 class。由于它们的使用方式,它们必须是静态的。我从基础 class 继承了其他 classes。所以,如果基数 class 是 Base
,我们有 ChildA
和 ChildB
.
大部分方法都包含在基础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
字段在所有访问者之间共享:Base
、ChildA
和 ChildB
。以下事件序列可能会导致您看到的失败:
- 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。感谢所有的输入!
我这里有一个有趣的 JUnit 问题 (JUnit 4.12)。我有一个只有静态方法的基 class。由于它们的使用方式,它们必须是静态的。我从基础 class 继承了其他 classes。所以,如果基数 class 是 Base
,我们有 ChildA
和 ChildB
.
大部分方法都包含在基础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
字段在所有访问者之间共享:Base
、ChildA
和 ChildB
。以下事件序列可能会导致您看到的失败:
- JUnit 测试调用
ChildA.methodOne()
开始执行,导致 JVM classloader 加载ChildA.class
并执行其static
初始化程序块,将Base.myType
设置为ChildType.ChildA
, - JUnit 测试调用
ChildB.methodOne()
开始执行,导致 JVM classloader 加载ClassB.class
并执行 itsstatic
初始化块,将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。感谢所有的输入!