关于单元测试:用Java模拟静态块

关于单元测试:用Java模拟静态块

Mocking Static Blocks in Java

我对Java的座右铭是"仅仅因为Java具有静态块,并不意味着您应该使用它们。"除了笑话,Java中还有许多使测试成为噩梦的技巧。我最讨厌的两个是匿名类和静态块。我们有很多使用静态块的遗留代码,这些是我们编写单元测试时最讨厌的点之一。我们的目标是能够以最小的代码更改为依赖于此静态初始化的类编写单元测试。

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

1
2
3
4
5
public class ClassWithStaticInit {
  static {
    System.out.println("static initializer.");
  }
}

将更改为

1
2
3
4
5
6
7
8
9
public class ClassWithStaticInit {
  static {
    staticInit();
  }

  private static void staticInit() {
    System.out.println("static initialized.");
  }
}

这样我们就可以在JUnit中执行以下操作。

1
2
3
4
5
6
7
8
9
10
11
public class DependentClassTest {
  public static class MockClassWithStaticInit {
    public static void staticInit() {
    }
  }

  @BeforeClass
  public static void setUpBeforeClass() {
    Mockit.redefineMethods(ClassWithStaticInit.class, MockClassWithStaticInit.class);
  }
}

但是,该解决方案也有其自身的问题。您不能在同一JVM上运行DependentClassTestClassWithStaticInitTest,因为您实际上希望静态块为ClassWithStaticInitTest运行。

您将如何完成此任务?还是您认为任何更好的,非基于JMockit的解决方案都更清洁?


PowerMock是另一个扩展EasyMock和Mockito的模拟框架。使用PowerMock,您可以轻松地从类(例如静态初始化程序)中删除不需要的行为。在您的示例中,您只需将以下注释添加到您的JUnit测试用例中:

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

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


这将涉及更多的"高级" JMockit。事实证明,您可以通过创建public void $clinit()方法在JMockit中重新定义静态初始化块。因此,与其进行更改

1
2
3
4
5
6
7
8
9
public class ClassWithStaticInit {
  static {
    staticInit();
  }

  private static void staticInit() {
    System.out.println("static initialized.");
  }
}

我们不妨按原样保留ClassWithStaticInit并在MockClassWithStaticInit中执行以下操作:

1
2
3
4
public static class MockClassWithStaticInit {
  public void $clinit() {
  }
}

实际上,这将使我们无法在现有类中进行任何更改。


有时,我会在代码所依赖的类中找到静态初始化器。 如果无法重构代码,则可以使用PowerMock的@SuppressStaticInitializationFor注释来取消静态初始化程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@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是我的两个同事开发的一个开源项目。


在我看来,您正在对待一种症状:糟糕的设计依赖于静态初始化。也许某些重构才是真正的解决方案。听起来您已经对staticInit()函数进行了一些重构,但是也许该函数需要从构造函数而不是静态初始化程序中调用。如果您可以取消静态初始值设定项期,您会更好。只有您可以做出此决定(我看不到您的代码库),但是一定要进行重构肯定会有所帮助。

至于模拟,我使用EasyMock,但是遇到了同样的问题。遗留代码中静态初始化程序的副作用使测试变得困难。我们的答案是重构静态初始化程序。


遇到此问题时,我通常会执行与您描述的相同的操作,除了将静态方法设置为受保护的方法之外,以便可以手动调用它。最重要的是,我确保可以多次调用该方法而不会出现问题(否则就测试而言,它并不比静态初始化程序好)。

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

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


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

1
2
3
4
5
Math.metaClass.'static'.max = { int a, int b ->
    a + b
}

Math.max 1, 2

如果您不能使用Groovy,则确实需要重构代码(也许要注入类似Initializator的东西)。

亲切的问候


我对Mock框架不是很了解,所以如果我错了,请更正我,但是您可能无法拥有两个不同的Mock对象来解决您提到的情况吗?如

1
2
3
4
public static class MockClassWithEmptyStaticInit {
  public static void staticInit() {
  }
}

1
2
3
4
5
public static class MockClassWithStaticInit {
  public static void staticInit() {
    System.out.println("static initialized.");
  }
}

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

1
2
3
4
5
@BeforeClass
public static void setUpBeforeClass() {
  Mockit.redefineMethods(ClassWithStaticInit.class,
                         MockClassWithEmptyStaticInit.class);
}

1
2
3
4
5
@BeforeClass
public static void setUpBeforeClass() {
  Mockit.redefineMethods(ClassWithStaticInit.class,
                         MockClassWithStaticInit.class);
}

分别。


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

单例和抽象工厂的某种混合可能能够为您提供与今天相同的功能,并且具有良好的可测试性,但是这会增加很多样板代码,因此尝试重构可能会更好。完全解决静态问题,或者至少可以解决一些不太复杂的问题。

尽管看不到您的代码,很难说是否有可能。


并不是一个真正的答案,只是在想-没有任何方法可以"撤消"对Mockit.redefineMethods的调用吗?
如果不存在这样的显式方法,不应该以以下方式再次执行该方法吗?

1
Mockit.redefineMethods(ClassWithStaticInit.class, ClassWithStaticInit.class);

如果存在这样的方法,则可以在类的@AfterClass方法中执行该方法,并使用"原始"静态初始化程序块对ClassWithStaticInitTest进行测试,就好像什么都没有改变一样,来自同一JVM。

不过这只是预感,因此我可能会缺少一些东西。


推荐阅读