使用Mockito模拟静态方法

503

我已经编写了一个工厂来创建 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进行测试。有没有一种很好的方法来模拟/验证这个特定的用例?


7
"你不能通过Mockito设计来这样做 :)" - MariuszS
40
Mockito(或EasyMock、jMock)不支持模拟静态方法并非设计上如此,而是出于偶然。这种限制(以及对无法模拟的final类/方法或新创建对象的支持)是实现模拟所采用的方法自然而然产生的(但并非预期的)结果;该方法会动态创建新类以实现/扩展要进行模拟的类型。其他模拟库使用避免这些限制的其他方法。.NET世界中也发生了类似情况。 - Rogério
6
谢谢解释。https://github.com/mockito/mockito/wiki/FAQ 我能够模拟静态方法吗? 不可以。Mockito更倾向于面向对象和依赖注入,而非静态、过程化的代码,因为这种代码难以理解和修改。 这个限制背后也有一些设计的思考 :) - MariuszS
21
我读到这句话是想抨击正当使用情况,而不是承认工具有无法(轻易)消除的限制,并且没有提供任何合理的理由。顺便说一下,这里有一个相反观点的讨论,附有参考链接。 - Rogério
17
Mockito自版本3.4.0开始支持模拟静态方法。 https://github.com/mockito/mockito/pull/1955 - Mahatma_Fatal_Error
显示剩余7条评论
22个回答

414

在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(...);

    }

更多信息:


6
虽然这在理论上是可行的,但在实践中很难实现。在使用PowerMock和Mockito模拟静态方法时遇到了困难…… - Naftuli Kay
56
不幸的是,这样做的巨大劣势就在于需要使用PowerMockRunner。 - Innokenty
18
sut.execute()的意思是什么? - TejjD
5
被测系统(System Under Test)是需要对 DriverManager 进行模拟的类。http://www.kaczanowscy.pl/tomek/2011-01/testing-basics-sut-and-docs - MariuszS
8
如果您已经在使用JUnit4,则可以使用@RunWith(PowerMockRunner.class)并在其下方添加@PowerMockRunnerDelegate(JUnit4.class) - EM-Creations
显示剩余17条评论

131
自Mockito 3.4.0版本开始,可以对静态方法进行模拟。 更多详情请参考:

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 5.0.0 之前,模拟静态方法需要额外依赖 mockito-inline
(对于 JUnit5,还需要添加以下内容)
<dependency>
  <groupId>org.mockito</groupId>
  <artifactId>mockito-junit-jupiter</artifactId>
  <version>${mockito.version}</version>
  <scope>test</scope>
</dependency>

3
请参阅Mockito可模拟静态方法!和https://github.com/mockito/mockito/pull/1955。 - Gerold Broser
1
对我来说,一个测试类给了我一些非常好的见解,如何使用新的staticMock功能:StaticMockTest.java,请还要查看版本3.4.23.4.6中的错误修复。 - MichaelCkr
1
感谢leokom指出这一点。然而,我不是Mockito提出的try-with-resources方法的粉丝,因此已经用JUnit5扩展替换了它。我会添加一个答案来描述它。它允许您在测试类上简单地创建一个带注释的字段,用于静态模拟。更加清晰,特别是当您需要模拟多个静态时。 - Radboud
1
请参考以下带有参数的示例答案:https://dev59.com/imEi5IYBdhLWcg3wndZw#64714397 - Yaniv K.
1
从Mockito版本5.0.0开始,mockito-inline是默认的,不需要显式添加(并且从版本5.3.0开始,它甚至不再存在)。 - undefined
显示剩余4条评论

88

规避无法避免使用的静态方法的典型策略是创建包装对象并使用包装对象而非实际静态类。

包装对象成为实际静态类的门面,您不会对其进行测试。

包装对象可能是这样的东西

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,将其注入为协作者(甚至不需要混乱的构造函数进行测试),然后进行模拟测试。


4
我创建了一个工具,可以自动生成Java 8的“mixin”接口,用于包装静态调用:https://github.com/aro-tech/interface-it 生成的“mixin”接口可像其他接口一样进行模拟测试,或者如果您的被测类“实现”该接口,则可以在子类中覆盖其任何方法以供测试使用。 - aro_tech

31

我遇到过类似的问题。接受的答案对我没有用,直到我做出了更改:@PrepareForTest(TheClassThatContainsStaticMethod.class),根据PowerMock的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());
    }
}

目前在使用JUnit 4时,无法理解?.mockStatic和?.when的含义。 - Teddy
3
PowerMock.mockStatic和Mockito.when似乎无法正常工作。 - Teddy
对于后来看到这篇文章的任何人,我必须键入PowerMockito.mockStatic(StaticClass.class); - thinkereer
你需要在Maven中包含powermock-api-mockito依赖。 - PeterS

27

对于使用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()


自从Mockito 5.0.0版本以后,mockito-inline不再需要,而且自从5.3.0版本以后,它甚至不再可用。 - undefined

25

如前所述,Mockito 无法模拟静态方法。

如果更改测试框架不是一个选项,则可以执行以下操作:

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


1
你好,你有这方面的示例吗?谢谢。 - Francislainy Campos

8

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

例如:

securityAlgo = MessageDigest.getInstance(SECURITY_ALGORITHM);

如果您需要模拟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());

我一直在努力想弄清楚为什么我的静态类无法模拟。你会认为在互联网上所有的教程中,至少有一个会介绍更多的用例,而不仅仅是最基本的用法。 - SoftwareSavant

7

6
提供的链接失效了。 - Zian

7

通过一些重构,您可以实现这个目标:

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

6
Mockito无法捕获静态方法,但自从Mockito 2.14.0以来,您可以通过创建静态方法的调用实例来模拟它。
示例(摘自它们的测试):
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并不教条主义。我们不想阻止像静态模拟这样的不推荐使用情况。这只是不属于我们的工作范畴。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接