【问题标题】:Mocking static methods with Mockito使用 Mockito 模拟静态方法
【发布时间】:2014-02-02 00:46:03
【问题描述】:

我写了一个工厂来生产java.sql.Connection对象:

public class MySQLDatabaseConnectionFactory implements DatabaseConnectionFactory {

    @Override public Connection getConnection() {
        try {
            return DriverManager.getConnection(...);
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }
}

我想验证传递给DriverManager.getConnection 的参数,但我不知道如何模拟静态方法。我将 JUnit 4 和 Mockito 用于我的测试用例。有没有模拟/验证这个特定用例的好方法?

【问题讨论】:

  • 你不能用 mockito 设计 :)
  • @MariuszS Mockito(或 EasyMock 或 jMock)不支持模拟 static 方法并非设计使然,而是偶然。这种限制(以及不支持模拟final 类/方法或new-ed 对象)是用于实现模拟的方法的自然(但非预期)结果,其中动态创建实现/扩展的新类要模拟的类型;其他模拟库使用其他方法来避免这些限制。这也发生在 .NET 世界中。
  • @Rogério 感谢您的解释。 github.com/mockito/mockito/wiki/FAQ 我可以模拟静态方法吗? 不能。 Mockito 更喜欢面向对象和依赖注入,而不是难以理解和更改的静态过程代码。 这个限制背后也有一些设计 :)
  • @MariuszS 我读到这是试图驳回合法用例而不是承认该工具具有无法(轻松)消除的限制,并且没有提供任何合理的理由。顺便说一句,here is such a discussion 表示相反的观点,并附有参考资料。
  • Mockito 现在支持从 v3.4.0 开始模拟静态方法 github.com/mockito/mockito/pull/1955

标签: java unit-testing static mocking mockito


【解决方案1】:

在 Mockito 之上使用 PowerMockito

示例代码:

@RunWith(PowerMockRunner.class)
@PrepareForTest(DriverManager.class)
public class Mocker {

    @Test
    public void shouldVerifyParameters() throws Exception {

        //given
        PowerMockito.mockStatic(DriverManager.class);
        BDDMockito.given(DriverManager.getConnection(...)).willReturn(...);

        //when
        sut.execute(); // System Under Test (sut)

        //then
        PowerMockito.verifyStatic();
        DriverManager.getConnection(...);

    }

更多信息:

【讨论】:

【解决方案2】:

避免使用无法避免的静态方法的典型策略是创建包装对象并改用包装对象。

包装器对象成为真正静态类的外观,您无需对其进行测试。

包装器对象可能类似于

public class Slf4jMdcWrapper {
    public static final Slf4jMdcWrapper SINGLETON = new Slf4jMdcWrapper();

    public String myApisToTheSaticMethodsInSlf4jMdcStaticUtilityClass() {
        return MDC.getWhateverIWant();
    }
}

最后,您的测试类可以使用这个单例对象,例如, 有一个用于现实生活的默认构造函数:

public class SomeClassUnderTest {
    final Slf4jMdcWrapper myMockableObject;

    /** constructor used by CDI or whatever real life use case */
    public myClassUnderTestContructor() {
        this.myMockableObject = Slf4jMdcWrapper.SINGLETON;
    }

    /** constructor used in tests*/
    myClassUnderTestContructor(Slf4jMdcWrapper myMock) {
        this.myMockableObject = myMock;
    }
}

这里有一个可以轻松测试的类,因为您不直接使用具有静态方法的类。

如果您使用 CDI 并且可以使用 @Inject 注释,那么它会更容易。 只需将您的 Wrapper bean 设为 @ApplicationScoped,将该东西作为协作者注入(您甚至不需要凌乱的构造函数来进行测试),然后继续进行模拟。

【讨论】:

  • 我创建了一个工具来自动生成包含静态调用的 Java 8“mixin”接口:github.com/aro-tech/interface-it 生成的 mixin 可以像任何其他接口一样被模拟,或者如果您的测试类“实现”接口,您可以在测试的子类中覆盖其任何方法。
【解决方案3】:

自 Mockito 3.4.0 起,可以在 Mockito 中模拟静态方法。 更多详情见:

https://github.com/mockito/mockito/releases/tag/v3.4.0

https://github.com/mockito/mockito/issues/1013

https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Mockito.html#static_mocks

assertEquals("foo", Foo.method());
try (MockedStatic mocked = mockStatic(Foo.class)) {
 mocked.when(Foo::method).thenReturn("bar");
 assertEquals("bar", Foo.method());
 mocked.verify(Foo::method);
}
assertEquals("foo", Foo.method());

在你的情况下,是这样的:

  @Test
  public void testStaticMockWithVerification() throws SQLException {
    try (MockedStatic<DriverManager> dummy = Mockito.mockStatic(DriverManager.class)) {
      DatabaseConnectionFactory factory = new MySQLDatabaseConnectionFactory();
      dummy.when(() -> DriverManager.getConnection("arg1", "arg2", "arg3"))
        .thenReturn(new Connection() {/*...*/});

      factory.getConnection();

      dummy.verify(() -> DriverManager.getConnection(eq("arg1"), eq("arg2"), eq("arg3")));
    }
  }

注意:此功能需要 mockito-inline 依赖而不是 mockito-core。

对于 JUnit5,还要添加:

<dependency>
  <groupId>org.mockito</groupId>
  <artifactId>mockito-junit-jupiter</artifactId>
  <version>${mockito.version}</version>
  <scope>test</scope>
</dependency>

【讨论】:

【解决方案4】:

我遇到了类似的问题。接受的答案对我不起作用,直到我做出改变:@PrepareForTest(TheClassThatContainsStaticMethod.class),根据PowerMock's documentation for mockStatic

而且我不必使用BDDMockito

我的班级:

public class SmokeRouteBuilder {
    public static String smokeMessageId() {
        try {
            return InetAddress.getLocalHost().getHostAddress();
        } catch (UnknownHostException e) {
            log.error("Exception occurred while fetching localhost address", e);
            return UUID.randomUUID().toString();
        }
    }
}

我的测试课:

@RunWith(PowerMockRunner.class)
@PrepareForTest(SmokeRouteBuilder.class)
public class SmokeRouteBuilderTest {
    @Test
    public void testSmokeMessageId_exception() throws UnknownHostException {
        UUID id = UUID.randomUUID();

        mockStatic(InetAddress.class);
        mockStatic(UUID.class);
        when(InetAddress.getLocalHost()).thenThrow(UnknownHostException.class);
        when(UUID.randomUUID()).thenReturn(id);

        assertEquals(id.toString(), SmokeRouteBuilder.smokeMessageId());
    }
}

【讨论】:

  • 无法弄清楚 ?.mockStatic 和 ?.when 目前使用 JUnit 4
  • PowerMock.mockStatic & Mockito.when 似乎不起作用。
  • 对于以后看到这个的任何人,对我来说,我必须输入 PowerMockito.mockStatic(StaticClass.class);
  • 你需要包含powermock-api-mockito maven arterfact。
【解决方案5】:

如前所述,您不能使用 mockito 模拟静态方法。

如果无法更改您的测试框架,您可以执行以下操作:

为 DriverManager 创建一个接口,模拟这个接口,通过某种依赖注入将其注入,并在该模拟上进行验证。

【讨论】:

  • 嗨,你能举个例子吗?谢谢。
【解决方案6】:

对于使用 JUnit 5 的用户,Powermock 不是一个选项。您需要以下依赖项才能使用 Mockito 成功模拟静态方法。

testCompile    group: 'org.mockito', name: 'mockito-core',           version: '3.6.0'
testCompile    group: 'org.mockito', name: 'mockito-junit-jupiter',  version: '3.6.0'
testCompile    group: 'org.mockito', name: 'mockito-inline',         version: '3.6.0'

mockito-junit-jupiter 添加对 JUnit 5 的支持。

mockito-inline 依赖提供了对模拟静态方法的支持。

例子:

@Test
void returnUtilTest() {
    assertEquals("foo", UtilClass.staticMethod("foo"));

    try (MockedStatic<UtilClass> classMock = mockStatic(UtilClass.class)) {

        classMock.when(() -> UtilClass.staticMethod("foo")).thenReturn("bar");

        assertEquals("bar", UtilClass.staticMethod("foo"));
     }

     assertEquals("foo", UtilClass.staticMethod("foo"));
}

try-with-resource 块用于使静态模拟保持临时状态,因此仅在该范围内模拟。

如果不使用 try 块,请确保在完成断言后关闭模拟。

MockedStatic<UtilClass> classMock = mockStatic(UtilClass.class)
classMock.when(() -> UtilClass.staticMethod("foo")).thenReturn("bar");
assertEquals("bar", UtilClass.staticMethod("foo"));
classMock.close();

模拟 void 方法:

当在一个类上调用 mockStatic 时,该类中的所有静态 void 方法都会自动模拟为 doNothing()

【讨论】:

    【解决方案7】:

    观察:在静态实体中调用静态方法时,需要更改@PrepareForTest 中的类。

    例如:

    securityAlgo = MessageDigest.getInstance(SECURITY_ALGORITHM);
    

    以上代码如果需要mock MessageDigest 类,请使用

    @PrepareForTest(MessageDigest.class)
    

    如果你有类似下面的东西:

    public class CustomObjectRule {
    
        object = DatatypeConverter.printHexBinary(MessageDigest.getInstance(SECURITY_ALGORITHM)
                 .digest(message.getBytes(ENCODING)));
    
    }
    

    然后,您需要准备此代码所在的类。

    @PrepareForTest(CustomObjectRule.class)
    

    然后模拟方法:

    PowerMockito.mockStatic(MessageDigest.class);
    PowerMockito.when(MessageDigest.getInstance(Mockito.anyString()))
          .thenThrow(new RuntimeException());
    

    【讨论】:

    • 我正用头撞墙,试图弄清楚为什么我的静态课程没有嘲笑。您可能会认为,在互联网上的所有教程中,ONE 会涉及的不仅仅是简单的用例。
    【解决方案8】:

    我还写了一个 Mockito 和 AspectJ 的组合:https://github.com/iirekm/varia/tree/develop/ajmock

    你的例子变成了:

    when(() -> DriverManager.getConnection(...)).thenReturn(...);
    

    【讨论】:

    • 提供的链接已失效。
    【解决方案9】:

    你可以通过一点重构来做到这一点:

    public class MySQLDatabaseConnectionFactory implements DatabaseConnectionFactory {
    
        @Override public Connection getConnection() {
            try {
                return _getConnection(...some params...);
            } catch (SQLException e) {
                throw new RuntimeException(e);
            }
        }
    
        //method to forward parameters, enabling mocking, extension, etc
        Connection _getConnection(...some params...) throws SQLException {
            return DriverManager.getConnection(...some params...);
        }
    }
    

    然后您可以扩展您的类MySQLDatabaseConnectionFactory 以返回模拟连接、对参数进行断言等。

    扩展类可以驻留在测试用例中,如果它位于同一个包中(我鼓励你这样做)

    public class MockedConnectionFactory extends MySQLDatabaseConnectionFactory {
    
        Connection _getConnection(...some params...) throws SQLException {
            if (some param != something) throw new InvalidParameterException();
    
            //consider mocking some methods with when(yourMock.something()).thenReturn(value)
            return Mockito.mock(Connection.class);
        }
    }
    

    【讨论】:

      【解决方案10】:

      Mockito 无法捕获静态方法,但由于 Mockito 2.14.0 您可以通过创建静态方法的调用实例来模拟它。

      示例(摘自their tests):

      public class StaticMockingExperimentTest extends TestBase {
      
          Foo mock = Mockito.mock(Foo.class);
          MockHandler handler = Mockito.mockingDetails(mock).getMockHandler();
          Method staticMethod;
          InvocationFactory.RealMethodBehavior realMethod = new InvocationFactory.RealMethodBehavior() {
              @Override
              public Object call() throws Throwable {
                  return null;
              }
          };
      
          @Before
          public void before() throws Throwable {
              staticMethod = Foo.class.getDeclaredMethod("staticMethod", String.class);
          }
      
          @Test
          public void verify_static_method() throws Throwable {
              //register staticMethod call on mock
              Invocation invocation = Mockito.framework().getInvocationFactory().createInvocation(mock, withSettings().build(Foo.class), staticMethod, realMethod,
                      "some arg");
              handler.handle(invocation);
      
              //verify staticMethod on mock
              //Mockito cannot capture static methods so we will simulate this scenario in 3 steps:
              //1. Call standard 'verify' method. Internally, it will add verificationMode to the thread local state.
              //  Effectively, we indicate to Mockito that right now we are about to verify a method call on this mock.
              verify(mock);
              //2. Create the invocation instance using the new public API
              //  Mockito cannot capture static methods but we can create an invocation instance of that static invocation
              Invocation verification = Mockito.framework().getInvocationFactory().createInvocation(mock, withSettings().build(Foo.class), staticMethod, realMethod,
                      "some arg");
              //3. Make Mockito handle the static method invocation
              //  Mockito will find verification mode in thread local state and will try verify the invocation
              handler.handle(verification);
      
              //verify zero times, method with different argument
              verify(mock, times(0));
              Invocation differentArg = Mockito.framework().getInvocationFactory().createInvocation(mock, withSettings().build(Foo.class), staticMethod, realMethod,
                      "different arg");
              handler.handle(differentArg);
          }
      
          @Test
          public void stubbing_static_method() throws Throwable {
              //register staticMethod call on mock
              Invocation invocation = Mockito.framework().getInvocationFactory().createInvocation(mock, withSettings().build(Foo.class), staticMethod, realMethod,
                      "foo");
              handler.handle(invocation);
      
              //register stubbing
              when(null).thenReturn("hey");
      
              //validate stubbed return value
              assertEquals("hey", handler.handle(invocation));
              assertEquals("hey", handler.handle(invocation));
      
              //default null value is returned if invoked with different argument
              Invocation differentArg = Mockito.framework().getInvocationFactory().createInvocation(mock, withSettings().build(Foo.class), staticMethod, realMethod,
                      "different arg");
              assertEquals(null, handler.handle(differentArg));
          }
      
          static class Foo {
      
              private final String arg;
      
              public Foo(String arg) {
                  this.arg = arg;
              }
      
              public static String staticMethod(String arg) {
                  return "";
              }
      
              @Override
              public String toString() {
                  return "foo:" + arg;
              }
          }
      }
      

      他们的目标不是直接支持静态模拟,而是改进其公共 API,以便其他库(如 Powermockito)不必依赖内部 API 或直接复制一些 Mockito 代码。 (source)

      免责声明:Mockito 团队认为通往地狱的道路是用静态方法铺就的。但是,Mockito 的工作不是保护您的代码免受静态方法的影响。如果您不喜欢您的团队进行静态模拟,请停止在您的组织中使用 Powermockito。 Mockito 需要发展为一个工具包,对应该如何编写 Java 测试有一个固执的愿景(例如,不要模拟静态!!!)。然而,Mockito 并不是教条主义的。我们不想阻止不推荐的用例,例如静态模拟。这不是我们的工作。

      【讨论】:

        【解决方案11】:

        要模拟静态方法,您应该使用 Powermock 查看: https://github.com/powermock/powermock/wiki/MockStatic。 Mockito doesn't provide 这个功能。

        您可以阅读一篇关于 mockito 的精彩文章: http://refcardz.dzone.com/refcardz/mockito

        【讨论】:

        • 请不要链接到网站。答案应包括实际可用的答案。如果网站出现故障或发生变化,则答案不再有效。
        【解决方案12】:

        我在 Mockito 中找到了一种解决方案。此功能仅来自3.4.0的版本

        https://asolntsev.github.io/en/2020/07/11/mockito-static-methods/

        • 依赖

          在您的 build.gradle 中,将 mockito-core:3.3.3 替换为 mockito-inline:3.4.0:

          testImplementation('org.mockito:mockito-inline:3.4.0')
          
        • 我们要模拟什么

           class Buddy 
           {
               static String name() 
               {
                  return "John";
               }
           }
          
        • 模拟静态方法

              @Test
              void lookMomICanMockStaticMethods() 
              {
                   assertThat(Buddy.name()).isEqualTo("John");
          
                  try (MockedStatic<Buddy> theMock = Mockito.mockStatic(Buddy.class)) 
                  {
                      theMock.when(Buddy::name).thenReturn("Rafael");
                      assertThat(Buddy.name()).isEqualTo("Rafael");
                  }
          
                  assertThat(Buddy.name()).isEqualTo("John");
              }
          

        我认为这可以帮助我们。

        【讨论】:

        • 我没有明白这里的模拟有什么用。如果巴迪,它不会抓住。名称()已更改。
        【解决方案13】:

        由于该方法是静态的,它已经具备了使用它所需的一切,因此它违背了模拟的目的。 模拟静态方法被认为是一种不好的做法。

        如果您尝试这样做,则意味着您执行测试的方式有问题。

        当然,您可以使用 PowerMockito 或任何其他能够做到这一点的框架,但请尝试重新考虑您的方法。

        例如:尝试模拟/提供该静态方法所使用的对象。

        【讨论】:

          【解决方案14】:

          使用 JMockit 框架。它对我有用。您不必为模拟 DBConenction.getConnection() 方法编写语句。只需以下代码即可。

          @Mock 下面是 mockit.Mock 包

          Connection jdbcConnection = Mockito.mock(Connection.class);
          
          MockUp<DBConnection> mockUp = new MockUp<DBConnection>() {
          
                      DBConnection singleton = new DBConnection();
          
                      @Mock
                      public DBConnection getInstance() { 
                          return singleton;
                      }
          
                      @Mock
                      public Connection getConnection() {
                          return jdbcConnection;
                      }
                   };
          

          【讨论】:

            【解决方案15】:

            使用 java FunctionalInterface 有一个简单的解决方案,然后将该接口添加为您尝试进行单元测试的类的依赖项。

            【讨论】:

              【解决方案16】:

              为了模拟静态函数,我可以这样做:

              • 在一些帮助类/对象中创建一个包装函数。 (使用名称变体可能有利于保持事物的分离和可维护性。)
              • 在您的代码中使用此包装器。 (是的,代码在实现时需要考虑到测试。)
              • 模拟包装函数。

              包装代码 sn-p(不是真正的功能,只是为了说明)

              class myWrapperClass ...
                  def myWrapperFunction (...) {
                      return theOriginalFunction (...)
                  }
              

              当然,在单个包装类中累积多个此类函数可能有利于代码重用。

              【讨论】:

                猜你喜欢
                • 2014-08-30
                • 1970-01-01
                • 2014-02-03
                • 2021-02-04
                相关资源
                最近更新 更多