47

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

到目前为止,我对同事的建议是将静态块的主体移动到私有静态方法中并调用它staticInit。然后可以从静态块中调用此方法。对于依赖于这个类的另一个类的单元测试,可以很容易地staticInitJMockit模拟不做任何事情。让我们看看这个例子。

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 的解决方案,您认为它们会更干净?

4

10 回答 10

57

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

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

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

于 2009-01-28T19:40:02.983 回答
14

有时,我会在我的代码所依赖的类中找到静态初始化程序。如果我无法重构代码,我使用PowerMock@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 是我的两个同事开发的开源项目。

于 2011-08-30T11:20:53.420 回答
13

这将进入更“高级”的 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() {
  }
}

这实际上将允许我们不对现有类进行任何更改。

于 2008-09-28T00:38:20.323 回答
6

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

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

于 2008-09-14T05:55:21.417 回答
5

当我遇到这个问题时,我通常会做你描述的同样的事情,除了我将静态方法保护起来,以便我可以手动调用它。最重要的是,我确保可以多次调用该方法而不会出现问题(否则就测试而言,它并不比静态初始化程序好)。

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

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

于 2008-09-14T09:17:33.067 回答
3

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

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

Math.max 1, 2

如果你不能使用 Groovy,你真的需要重构代码(也许注入类似初始化器的东西)。

亲切的问候

于 2008-09-14T07:42:58.100 回答
1

我想你真的想要某种工厂而不是静态初始化程序。

单例和抽象工厂的一些组合可能能够为您提供与今天相同的功能,并且具有良好的可测试性,但这会添加相当多的样板代码,因此尝试重构可能会更好静态的东西完全消失,或者如果你至少可以摆脱一些不太复杂的解决方案。

不过,如果没有看到您的代码,很难判断它是否可能。

于 2008-09-14T07:12:20.810 回答
1

我对 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);
}

分别。

于 2008-09-14T14:55:56.250 回答
0

不是真正的答案,只是想知道 - 有没有办法“逆转”对 的调用Mockit.redefineMethods
如果不存在这样的显式方法,不应该以下列方式再次执行它吗?

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

如果存在这样的方法,您可以在类的@AfterClass方法中执行它,并ClassWithStaticInitTest使用“原始”静态初始化程序块进行测试,就好像没有任何改变一样,来自同一个 JVM。

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

于 2015-11-16T14:17:15.237 回答
0

您可以使用 PowerMock 执行私有方法调用,例如:

ClassWithStaticInit staticInitClass = new ClassWithStaticInit()
Whitebox.invokeMethod(staticInitClass, "staticInit");
于 2020-10-05T21:36:36.673 回答