[英]Mocking Static Blocks in Java
我对 Java 的座右铭是“仅仅因为 Java 有 static 块,这并不意味着你应该使用它们。” 撇开玩笑不谈,Java 中有很多技巧让测试成为一场噩梦。 我最讨厌的两个是匿名类和 Static 块。 我们有很多使用 Static 块的遗留代码,这些是我们推动编写单元测试的恼人点之一。 我们的目标是能够以最少的代码更改为依赖于此 static 初始化的类编写单元测试。
到目前为止,我对同事的建议是将 static 块的主体移动到私有 static 方法中,并将其命名为staticInit
。 然后可以从 static 块中调用此方法。 对于依赖此 class 的另一个 class 的单元测试,可以轻松地使用JMockit模拟staticInit
以不做任何事情。 让我们看看这个例子。
public class ClassWithStaticInit {
static {
System.out.println("static initializer.");
}
}
将改为
public class ClassWithStaticInit {
static {
staticInit();
}
private static void staticInit() {
System.out.println("static initialized.");
}
}
这样我们就可以在JUnit中执行以下操作。
public class DependentClassTest {
public static class MockClassWithStaticInit {
public static void staticInit() {
}
}
@BeforeClass
public static void setUpBeforeClass() {
Mockit.redefineMethods(ClassWithStaticInit.class, MockClassWithStaticInit.class);
}
}
然而,这种解决方案也有其自身的问题。 您不能在同一个 JVM 上运行DependentClassTest
和ClassWithStaticInitTest
,因为您实际上希望 static 块为ClassWithStaticInitTest
运行。
完成这项任务的方法是什么? 或者任何更好的、非基于 JMockit 的解决方案,您认为它们会更干净?
有时,我会在我的代码所依赖的类中找到 static 初始化程序。 如果我无法重构代码,我使用PowerMock的@SuppressStaticInitializationFor
注释来抑制 static 初始化程序:
@RunWith(PowerMockRunner.class)
@SuppressStaticInitializationFor("com.example.ClassWithStaticInit")
public class ClassWithStaticInitTest {
ClassWithStaticInit tested;
@Before
public void setUp() {
tested = new ClassWithStaticInit();
}
@Test
public void testSuppressStaticInitializer() {
asserNotNull(tested);
}
// more tests...
}
阅读有关抑制不良行为的更多信息。
免责声明:PowerMock 是我的两个同事开发的开源项目。
这将进入更“高级”的 JMockit。 事实证明,您可以通过创建public void $clinit()
方法在 JMockit 中重新定义 static 初始化块。 所以,而不是做这个改变
public class ClassWithStaticInit {
static {
staticInit();
}
private static void staticInit() {
System.out.println("static initialized.");
}
}
我们不妨保留ClassWithStaticInit
并在MockClassWithStaticInit
中执行以下操作:
public static class MockClassWithStaticInit {
public void $clinit() {
}
}
这实际上将允许我们不对现有类进行任何更改。
在我看来,您正在治疗一种症状:依赖于 static 初始化的糟糕设计。 也许一些重构是真正的解决方案。 听起来您已经对您的staticInit()
function 进行了一些重构,但可能需要从构造函数调用 function,而不是从 ZA81259CEF8E959C3224DF1D456E 初始化程序调用。 如果您可以取消 static 初始化程序期间,您会过得更好。 只有你可以做出这个决定(我看不到你的代码库),但一些重构肯定会有所帮助。
至于mocking,我用的是EasyMock,但我也遇到了同样的问题。 遗留代码中 static 初始化程序的副作用使测试变得困难。 我们的答案是重构 static 初始化程序。
当我遇到这个问题时,我通常会做你描述的同样的事情,除了我将 static 方法保护起来,以便我可以手动调用它。 最重要的是,我确保可以多次调用该方法而不会出现问题(否则就测试而言,它并不比 static 初始化程序好)。
这工作得相当好,我实际上可以测试 static 初始化方法是否符合我的期望/希望它做的事情。 有时,拥有一些 static 初始化代码是最简单的,而构建一个过于复杂的系统来替换它是不值得的。
当我使用这种机制时,我确保记录受保护的方法仅用于测试目的,希望它不会被其他开发人员使用。 这当然可能不是一个可行的解决方案,例如,如果类的接口是外部可见的(或者作为其他团队的某种子组件,或者作为公共框架)。 不过,这是解决问题的简单方法,并且不需要设置第三方库(我喜欢)。
您可以在 Groovy 中编写测试代码,并使用元编程轻松模拟 static 方法。
Math.metaClass.'static'.max = { int a, int b ->
a + b
}
Math.max 1, 2
如果你不能使用 Groovy,你真的需要重构代码(也许注入类似初始化器的东西)。
亲切的问候
我想你真的想要某种工厂而不是 static 初始化程序。
singleton 和抽象工厂的一些组合可能能够为您提供与今天相同的功能,并且具有良好的可测试性,但这会添加相当多的样板代码,因此尝试重构可能会更好static 完全消失,或者如果您至少可以使用一些不太复杂的解决方案。
不过,如果没有看到您的代码,很难判断它是否可能。
我对 Mock 框架不是很了解,所以如果我错了请纠正我,但你不能有两个不同的 Mock 对象来涵盖你提到的情况吗? 如
public static class MockClassWithEmptyStaticInit {
public static void staticInit() {
}
}
和
public static class MockClassWithStaticInit {
public static void staticInit() {
System.out.println("static initialized.");
}
}
然后您可以在不同的测试用例中使用它们
@BeforeClass
public static void setUpBeforeClass() {
Mockit.redefineMethods(ClassWithStaticInit.class,
MockClassWithEmptyStaticInit.class);
}
和
@BeforeClass
public static void setUpBeforeClass() {
Mockit.redefineMethods(ClassWithStaticInit.class,
MockClassWithStaticInit.class);
}
分别。
不是真正的答案,只是想知道 - 有没有办法“逆转”对Mockit.redefineMethods
的调用?
如果不存在这样的显式方法,不应该以下列方式再次执行它吗?
Mockit.redefineMethods(ClassWithStaticInit.class, ClassWithStaticInit.class);
如果存在这样的方法,您可以在类的@AfterClass
方法中执行它,并使用“原始”static 初始化程序块测试ClassWithStaticInitTest
,就好像没有任何变化一样,从同一个 JVM。
不过,这只是一种预感,所以我可能会遗漏一些东西。
您可以使用 PowerMock 执行私有方法调用,例如:
ClassWithStaticInit staticInitClass = new ClassWithStaticInit()
Whitebox.invokeMethod(staticInitClass, "staticInit");
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.