繁体   English   中英

Mocking Static Java 中的块

[英]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 上运行DependentClassTestClassWithStaticInitTest ,因为您实际上希望 static 块为ClassWithStaticInitTest运行。

完成这项任务的方法是什么? 或者任何更好的、非基于 JMockit 的解决方案,您认为它们会更干净?

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

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

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

有时,我会在我的代码所依赖的类中找到 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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM