【问题标题】:Refactoring static mocks in Mockito 3.4在 Mockito 3.4 中重构静态模拟
【发布时间】:2022-01-19 02:17:09
【问题描述】:

我在一个 java 项目中有一个基于 Jupiter 的 UnitTest,我需要在其中模拟一些外部实用程序的静态方法。 Mockito 3.4 允许模拟静态方法,因此不再需要 PowerMock。我们都通过在 try(){} 块中定义它们并覆盖块内那些静态模拟的方法来使用静态模拟。一切都很好,但是当你必须实现一个非平凡的模拟级联时,事情有时会变得很烦人。 尤其是烦人的是你必须在你的测试类中的几个测试用例中这样做。

class ClassA {
    public static void methodA() { ... }
    public static void methodB() { ... }
}

class ClassB {
    public static void methodA() { ... }
    public static void methodB() { ... }
}

class ClassC {
    public static void methodA() { ... }
    public static void methodB() { ... }
}

class ClassX {
    public int doSomething() {
        // does something using static methods from classes ClassA, ClassB, ClassC
        return 3;
    }
}

一个测试类可能(大致)看起来像这样(请原谅拼写错误和废话,这只是为了演示,这不是工作代码):

@Test
void test_doSomething() {

    int result = 0;
    ClassX classX = new ClassX();

    try (MockedStatic<ClassA> classAMockStatic = Mockito.mockStatic(ClassA.class);
         MockedStatic<ClassB> classBMockStatic = Mockito.mockStatic(ClassB.class);
         MockedStatic<ClassC> classCMockStatic = Mockito.mockStatic(ClassC.class)) {

        // this is a block where static methods get overwritten, also some mocks are created
        // this code does not make sense, it is just to demonstrate the issue of non trivial mocking scenarios
        // situations like this often arise when mocking building patterns for example
        classAMockStatic.when(ClassA::methodA).thenReturn("fooA");
        ClassB classBMock = mock(ClassB.class);
        when(classBMock.methodA()).thenReturn("barA");
        classAMockStatic.when(ClassA::methodB).thenReturn(classBMock);
        ClassC classCMock = mock(ClassC.class);
        when(classCMock.someMethodA()).thenReturn("abc");
        when(classCMock.methodA()).thenReturn("barA");
        classAMockStatic.when(ClassA::methodA).thenReturn(classCMock);
        // and so on and so on, you get the idea

        result = classX.doSomething();
    }

    assertThat(result).equals(3);
}

如果不能将其重构为仅实现一次模拟级联,通常会出现问题。并在几个测试用例中使用它。我试过了,我惊讶地发现 一些 我覆盖的方法按预期工作,但不是全部。我没有找到可行的解决方案:

// 这不起作用

@Test
void test_doSomething() {

    int result = 0;
    ClassX classX = new ClassX();

    try (MockedStatic<ClassA> classAMockStatic = createStaticMockA();
         MockedStatic<ClassB> classBMockStatic = createStaticMockB();
         MockedStatic<ClassC> classCMockStatic = createStaticMockC()) {

        result = classX.doSomething();
    }

    assertThat(result).equals(3);
}

private MockedStatic<ClassA> createStaticMockA() {
    MockedStatic<ClassA> classAMockStatic = Mockito.mockStatic(ClassA.class);

    classAMockStatic.when(ClassA::methodA).thenReturn("fooA");
    ClassB classBMock = mock(ClassB.class);
    when(classBMock.methodA()).thenReturn("barA");
    classAMockStatic.when(ClassA::methodB).thenReturn(classBMock);

    return classAMockStatic;
}

private MockedStatic<ClassB> createStaticMockB() {
    MockedStatic<ClassB> classAMockStatic = Mockito.mockStatic(ClassB.class);
    
    ClassB classBMock = mock(ClassB.class);
    when(classBMock.someMethodA()).thenReturn("aaa");
    when(classBMock.someMethodB()).thenReturn("bbb");
    when(classBMock.methodA()).thenReturn("barA");
    classBMockStatic.when(ClassB::methodA).thenReturn(classBMock);
    return classBMockStatic;
}

private MockedStatic<ClassC> createStaticMockC() {
    MockedStatic<ClassC> classAMockStatic = Mockito.mockStatic(ClassC.class);
    
    ClassC classCMock = mock(ClassC.class);
    when(classCMock.someMethodA()).thenReturn("abc");
    when(classCMock.methodB()).thenReturn("barC");
    classCMockStatic.when(ClassC::methodA).thenReturn(classCMock);
    return classCMockStatic;
}

所以这看起来很清楚,但不起作用。

我知道我显然可以将模拟部分提取到一个方法中,然后在 try 块中首先调用它。但这将静态设置与模拟生成分开。而且也没有我想的那么干净。

是的,我尝试实现一个执行方法,该方法接受一个包含实际要执行的代码的 lambda。这样try(){} 块就会被隐藏到一个方法中,在测试用例中只留下对该方法的调用和一个 lambda。有效,但难以阅读和理解。根据我的经验,这不是一个好的解决方案。

是的,我也知道应该尝试重构生产代码,以便更容易模拟它。有时这根本不可能(阅读:外部依赖项)。

那么我的问题是什么?

  1. 有没有人知道如何实现干净、重构的外观并真正起作用?
  2. 谁能向我解释为什么有些被覆盖的方法可以按预期工作,而有些则不能?我知道,网上所有的例子都表明你使用的是直截了当的方法,但这些例子显然是在展示琐碎的情况。
  3. 真的没有其他人会因为必须在令人困惑的地方设置模拟级联而感到恼火吗?并且您必须为每个测试用例重新实现它?

【问题讨论】:

  • 在 Stack Overflow 上最好使用实际运行代码而不是伪代码。伪代码的问题在于,如果有人在其中发现错误,则无法知道问题是否存在于原始代码中,或者是否只是翻译中的问题。上面重构的代码不等同于第一个代码块,它对classBMockStatic 没有任何作用。一个可能的问题是重构后的代码生成了两个断开连接的 ClassB 模拟实例。
  • 另一个想法:总是可以通过某种方式重构生产代码来减少这个问题。 Mockito 文档指出:"do not mock types you don’t own"。如果您发现自己想要模拟外部依赖项,通常最好将该依赖项的使用封装在您控制的接口后面。

标签: java unit-testing static mockito refactoring


【解决方案1】:

正如蒂姆·摩尔建议的那样,如果你有:

class StaticUtil {
  public static void foo() { ... }
}

你可以引入一个接口和一个实现来注入你的客户端:

interface Util {
  void foo();
}

class DefaultUtil {
  public void foo() {
    StaticUtil.foo();
}

这使得编写测试更简单,因为您可以以正常方式模拟 Util

【讨论】:

  • 感谢您的解释,虽然这并不是我问题的真正答案......
猜你喜欢
  • 1970-01-01
  • 2014-06-01
  • 1970-01-01
  • 1970-01-01
  • 2014-08-30
  • 2014-02-02
  • 2022-01-11
相关资源
最近更新 更多