在Java中模拟静态块

在Java中模拟静态块,java,unit-testing,mocking,jmockit,static-block,Java,Unit Testing,Mocking,Jmockit,Static Block,我对Java的座右铭是“仅仅因为Java有静态块,并不意味着你就应该使用它们。”撇开玩笑不谈,Java中有很多技巧使测试成为一场噩梦。我最讨厌的两个是匿名类和静态块。我们有很多使用静态块的遗留代码,这些是我们编写单元测试时令人恼火的地方之一。我们的目标是能够为依赖于此静态初始化的类编写单元测试,并且代码更改最少 到目前为止,我给同事们的建议是将静态块的主体移动到一个私有静态方法中,并将其称为staticInit。然后可以从静态块内调用此方法。对于单元测试,依赖于该类的另一个类可以轻松地模拟sta

我对Java的座右铭是“仅仅因为Java有静态块,并不意味着你就应该使用它们。”撇开玩笑不谈,Java中有很多技巧使测试成为一场噩梦。我最讨厌的两个是匿名类和静态块。我们有很多使用静态块的遗留代码,这些是我们编写单元测试时令人恼火的地方之一。我们的目标是能够为依赖于此静态初始化的类编写单元测试,并且代码更改最少

到目前为止,我给同事们的建议是将静态块的主体移动到一个私有静态方法中,并将其称为
staticInit
。然后可以从静态块内调用此方法。对于单元测试,依赖于该类的另一个类可以轻松地模拟
staticInit
,而不做任何事情。让我们在示例中看到这一点

public class ClassWithStaticInit {
  static {
    System.out.println("static initializer.");
  }
}
将更改为

public class ClassWithStaticInit {
  static {
    staticInit();
  }

  private static void staticInit() {
    System.out.println("static initialized.");
  }
}
这样我们就可以在一段时间内完成以下工作

然而,这种解决方案也有其自身的问题。您不能在同一JVM上运行
DependentClassTest
ClassWithStationItTest
,因为您实际上希望静态块为
ClassWithStationItTest
运行


你完成这项任务的方式是什么?或者任何更好的、基于非JMockit的解决方案,您认为这些解决方案可以更干净地工作?

在我看来,您似乎在治疗一种症状:设计糟糕,依赖于静态初始化。也许重构才是真正的解决方案。听起来您已经对
staticInit()
函数进行了一些重构,但是可能需要从构造函数调用该函数,而不是从静态初始值设定项调用该函数。若你们能在这段时间内去掉静态初始化器,你们会过得更好。只有您可以做出这个决定(我看不到您的代码库),但一些重构肯定会有所帮助


至于嘲笑,我使用EasyMock,但我遇到了同样的问题。遗留代码中静态初始值设定项的副作用使测试变得困难。我们的答案是重构静态初始值设定项。

我想你真的想要某种工厂而不是静态初始值设定项

单例和抽象工厂的某种混合可能会使您获得与今天相同的功能,并且具有良好的可测试性,但这会添加大量的锅炉板代码,因此最好尝试完全重构静态内容,或者至少可以使用一些不太复杂的解决方案


很难说不看代码是否可行。

您可以用Groovy编写测试代码,并使用元编程轻松模拟静态方法

Math.metaClass.'static'.max = { int a, int b -> 
    a + b
}

Math.max 1, 2
如果您不能使用Groovy,那么您将真正需要重构代码(可能需要注入类似于初始化器的东西)


当我遇到这个问题时,我通常会做与您描述的相同的事情,只是我会保护静态方法,以便手动调用它。除此之外,我确保可以多次调用该方法而不会出现问题(否则,就测试而言,它并不比静态初始值设定项好)

这工作得相当好,我可以实际测试静态初始值设定项方法是否实现了我期望/希望它实现的功能。有时候,拥有一些静态初始化代码是最容易的,而构建一个过于复杂的系统来替换它是不值得的


当我使用这种机制时,我会确保记录受保护的方法仅用于测试目的,希望其他开发人员不会使用它。当然,这可能不是一个可行的解决方案,例如,如果类的接口是外部可见的(或者作为其他团队的某种子组件,或者作为公共框架)。这是一个简单的问题解决方案,并且不需要第三方库来设置(我喜欢)。

我对模拟框架不是非常了解,所以如果我错了,请纠正我,但是你不能用两个不同的模拟对象来覆盖你提到的情况吗?比如

public static class MockClassWithEmptyStaticInit {
  public static void staticInit() {
  }
}

然后您可以在不同的测试用例中使用它们

@BeforeClass
public static void setUpBeforeClass() {
  Mockit.redefineMethods(ClassWithStaticInit.class, 
                         MockClassWithEmptyStaticInit.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() {
  }
}
实际上,这将允许我们不对现有类进行任何更改。

是扩展EasyMock和Mockito的另一个模拟框架。使用PowerMock,您可以轻松地从一个类(例如静态初始值设定项)中删除。在您的示例中,只需向JUnit测试用例添加以下注释:

@RunWith(PowerMockRunner.class)
@SuppressStaticInitializationFor("some.package.ClassWithStaticInit")

PowerMock不使用Java代理,因此不需要修改JVM启动参数。您只需添加jar文件和上述注释

有时,我会在我的代码所依赖的类中找到静态initilizer。如果无法重构代码,我将使用的
@SuppressStaticInitializationFor
注释来抑制静态初始值设定项:

@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是由我的两位同事开发的开源项目

这不是一个真正的答案,但只是想知道-难道没有任何方法可以“反转”调用
Mockit.redefineMethods

如果不存在这样的显式方法,是否应该以下面的方式再次执行它

Mockit.redefineMethods(ClassWithStaticInit.class, ClassWithStaticInit.class);
如果存在这样一个方法,您可以在类'
@AfterClass
方法中执行它,并使用“原始”静态初始值设定项块测试
classwithstaticinitest
,就好像从同一JVM没有任何更改一样


这只是一种预感,所以我可能遗漏了一些东西。

您可以使用PowerMock来执行
@RunWith(PowerMockRunner.class)
@SuppressStaticInitializationFor("some.package.ClassWithStaticInit")
@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...
}
Mockit.redefineMethods(ClassWithStaticInit.class, ClassWithStaticInit.class);
ClassWithStaticInit staticInitClass = new ClassWithStaticInit()
Whitebox.invokeMethod(staticInitClass, "staticInit");