用Java模拟静态块
我对Java的座右铭是"仅仅因为Java具有静态块,并不意味着我们应该使用它们。"除了笑话,Java中还有许多使测试成为噩梦的技巧。我最讨厌的两个是匿名类和静态块。我们有很多使用静态块的遗留代码,这些是我们编写单元测试时最讨厌的点之一。我们的目标是能够以最小的代码更改为依赖于此静态初始化的类编写单元测试。
到目前为止,我对我的同事的建议是将静态块的主体移到私有静态方法中,并将其称为" staticInit"。然后可以从静态块中调用此方法。对于单元测试,依赖于该类的另一个类可以轻松地使用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",因为我们实际上希望静态块针对" ClassWithStaticInitTest"运行。
我们完成这项任务的方式是什么?还是我们认为会更好的,基于非JMockit的更好解决方案?
解决方案
回答
在我看来,我们正在对待一种症状:糟糕的设计依赖于静态初始化。也许重构是真正的解决方案。听起来我们已经对staticInit()函数进行了一些重构,但也许该函数需要从构造函数而不是静态初始化程序中调用。如果我们可以取消静态初始值设定项期,我们会更好。只有我们可以做出此决定(我看不到代码库),但是某些重构肯定会有所帮助。
至于模拟,我使用EasyMock,但是遇到了同样的问题。遗留代码中静态初始化程序的副作用使测试变得困难。我们的答案是重构静态初始化程序。
回答
我想我们真的想要某种工厂而不是静态初始值设定项。
单例和抽象工厂的某种混合可能能够为我们提供与今天相同的功能,并且具有良好的可测试性,但是这会添加很多样板代码,因此尝试重构可能会更好。完全解决静态问题,或者至少可以解决一些不太复杂的问题。
尽管看不到代码,却很难说出它是否可行。
回答
我们可以用Groovy编写测试代码,并使用元编程轻松模拟静态方法。
Math.metaClass.'static'.max = { int a, int b -> a + b } Math.max 1, 2
如果我们不能使用Groovy,则确实需要重构代码(也许要注入类似Initializator的东西)。
亲切的问候
回答
遇到此问题时,我通常会执行与我们描述的相同的操作,除了将静态方法设置为受保护,以便可以手动调用它。最重要的是,我确保可以多次调用该方法而不会出现问题(否则就测试而言,它并不比静态初始化程序好)。
这工作得相当好,我可以实际测试静态初始化方法是否达到了我期望/想要的目的。有时,拥有一些静态初始化代码是最容易的,而构建一个过于复杂的系统来替换它则不值得。
使用此机制时,请确保记录该受保护的方法仅出于测试目的而公开,并希望其他开发人员不会使用它。当然,这可能不是一个可行的解决方案,例如,如果类的接口在外部可见(作为其他团队的某种子组件,或者作为公共框架)。不过,这是解决问题的简单方法,不需要第三方库来建立(我喜欢)。
回答
我对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); }
分别。
回答
这将涉及更多的"高级" JMockit。事实证明,我们可以通过创建public void $ clinit()
方法在JMockit中重新定义静态初始化块。因此,与其进行此更改,不如
public class ClassWithStaticInit { static { staticInit(); } private static void staticInit() { System.out.println("static initialized."); } }
我们不妨按原样保留ClassWithStaticInit
并在MockClassWithStaticInit
中执行以下操作:
public static class MockClassWithStaticInit { public void $clinit() { } }
实际上,这将使我们无法在现有类中进行任何更改。
回答
PowerMock是另一个扩展EasyMock和Mockito的模拟框架。使用PowerMock,我们可以轻松地从类(例如静态初始化程序)中删除不需要的行为。在示例中,我们只需将以下注释添加到JUnit测试用例中:
@RunWith(PowerMockRunner.class) @SuppressStaticInitializationFor("some.package.ClassWithStaticInit")
PowerMock不使用Java代理,因此不需要修改JVM启动参数。我们只需添加jar文件和上面的注释。