在 Java 中模拟静态块

发布于 2024-07-05 06:36:22 字数 1362 浏览 6 评论 0原文

我对 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 上运行 DependentClassTestClassWithStaticInitTest,因为您实际上希望为 ClassWithStaticInitTest 运行静态块。

你会采取什么方式来完成这项任务? 或者您认为任何更好的、非基于 JMockit 的解决方案会更干净?

My motto for Java is "just because Java has static blocks, it doesn't mean that you should be using them." Jokes aside, there are a lot of tricks in Java that make testing a nightmare. Two of the most I hate are Anonymous Classes and Static Blocks. We have a lot of legacy code that make use of Static Blocks and these are one of the annoying points in our push in writing unit tests. Our goal is to be able to write unit tests for classes that depend on this static initialization with minimal code changes.

So far my suggestion to my colleagues is to move the body of the static block into a private static method and call it staticInit. This method can then be called from within the static block. For unit testing another class that depends on this class could easily mock staticInit with JMockit to not do anything. Let's see this in example.

public class ClassWithStaticInit {
  static {
    System.out.println("static initializer.");
  }
}

Will be changed to

public class ClassWithStaticInit {
  static {
    staticInit();
  }

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

So that we can do the following in a JUnit.

public class DependentClassTest {
  public static class MockClassWithStaticInit {
    public static void staticInit() {
    }
  }

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

However this solution also comes with its own problems. You can't run DependentClassTest and ClassWithStaticInitTest on the same JVM since you actually want the static block to run for ClassWithStaticInitTest.

What would be your way of accomplishing this task? Or any better, non-JMockit based solutions that you think would work cleaner?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(10

泪之魂 2024-07-12 06:36:22

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

ClassWithStaticInit staticInitClass = new ClassWithStaticInit()
Whitebox.invokeMethod(staticInitClass, "staticInit");

You can use PowerMock to execute the private method call like:

ClassWithStaticInit staticInitClass = new ClassWithStaticInit()
Whitebox.invokeMethod(staticInitClass, "staticInit");
孤千羽 2024-07-12 06:36:22

并不是真正的答案,只是想知道 - 有没有办法“反转”对 Mockit.redefineMethods 的调用?
如果不存在这样的显式方法,难道不应该以下面的方式再次执行它来解决这个问题吗?

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

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

但这只是一种预感,所以我可能会错过一些东西。

Not really an answer, but just wondering - isn't there any way to "reverse" the call to Mockit.redefineMethods?
If no such explicit method exists, shouldn't executing it again in the following fashion do the trick?

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

If such a method exists, you could execute it in the class' @AfterClass method, and test ClassWithStaticInitTest with the "original" static initializer block, as if nothing has changed, from the same JVM.

This is just a hunch though, so I may be missing something.

葬花如无物 2024-07-12 06:36:22

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

中分别使用它们。

I'm not super knowledgeable in Mock frameworks so please correct me if I'm wrong but couldn't you possibly have two different Mock objects to cover the situations that you mention? Such as

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

and

public static class MockClassWithStaticInit {
  public static void staticInit() {
    System.out.println("static initialized.");
  }
}

Then you can use them in your different test cases

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

and

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

respectively.

噩梦成真你也成魔 2024-07-12 06:36:22

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

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

Math.max 1, 2

如果您无法使用 Groovy,那么您确实需要重构代码(也许要注入初始化器之类的东西)。

亲切的问候

You could write your test code in Groovy and easily mock the static method using metaprogramming.

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

Math.max 1, 2

If you can't use Groovy, you will really need to refactoring the code (maybe to inject something like a initializator).

Kind Regards

歌枕肩 2024-07-12 06:36:22

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

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

不过,在没有看到您的代码的情况下很难判断是否可能。

I suppose you really want some kind of factory instead of the static initializer.

Some mix of a singleton and an abstract factory would probably be able to get you the same functionality as today, and with good testability, but that would add quite a lot of boiler-plate code, so it might be better to just try to refactor the static stuff away completely or if you could at least get away with some less complex solution.

Hard to tell if it´s possible without seeing your code though.

燃情 2024-07-12 06:36:22

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

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

Sounds to me like you are treating a symptom: poor design with dependencies on static initialization. Maybe some refactoring is the real solution. It sounds like you've already done a little refactoring with your staticInit() function, but maybe that function needs to be called from the constructor, not from a static initializer. If you can do away with static initializers period, you will be better off. Only you can make this decision (I can't see your codebase) but some refactoring will definitely help.

As for mocking, I use EasyMock, but I have run into the same issue. Side effects of static initializers in legacy code make testing difficult. Our answer was to refactor out the static initializer.

热风软妹 2024-07-12 06:36:22

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

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

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

When I run into this problem, I usually do the same thing you describe, except I make the static method protected so I can invoke it manually. On top of this, I make sure that the method can be invoked multiple times without problems (otherwise it is no better than the static initializer as far as the tests go).

This works reasonably well, and I can actually test that the static initializer method does what I expect/want it to do. Sometimes it is just easiest to have some static initialization code, and it just isn't worth it to build an overly complex system to replace it.

When I use this mechanism, I make sure to document that the protected method is only exposed for testing purposes, with the hopes that it won't be used by other developers. This of course may not be a viable solution, for example if the class' interface is externally visible (either as a sub-component of some kind for other teams, or as a public framework). It is a simple solution to the problem though, and doesn't require a third party library to set up (which I like).

踏雪无痕 2024-07-12 06:36:22

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

这实际上允许我们不在现有课程。

This is going to get into more "Advanced" JMockit. It turns out, you can redefine static initialization blocks in JMockit by creating a public void $clinit() method. So, instead of making this change

public class ClassWithStaticInit {
  static {
    staticInit();
  }

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

we might as well leave ClassWithStaticInit as is and do the following in the MockClassWithStaticInit:

public static class MockClassWithStaticInit {
  public void $clinit() {
  }
}

This will in fact allow us to not make any changes in the existing classes.

七度光 2024-07-12 06:36:22

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

Occasionally, I find static initilizers in classes that my code depends on. If I cannot refactor the code, I use PowerMock's @SuppressStaticInitializationFor annotation to suppress the static initializer:

@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...
}

Read more about suppressing unwanted behaviour.

Disclaimer: PowerMock is an open source project developed by two colleagues of mine.

捎一片雪花 2024-07-12 06:36:22

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

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

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

PowerMock is another mock framework that extends EasyMock and Mockito. With PowerMock you can easily remove unwanted behavior from a class, for example a static initializer. In your example you simply add the following annotations to your JUnit test case:

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

PowerMock does not use a Java agent and therefore does not require modification of the JVM startup parameters. You simple add the jar file and the above annotations.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文