关于java:使用Mockito模拟静态方法

Mocking static methods with Mockito

我写了一个工厂来生产java.sql.Connection物体:

1
2
3
4
5
6
7
8
9
10
public class MySQLDatabaseConnectionFactory implements DatabaseConnectionFactory {

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

我想验证传递给DriverManager.getConnection的参数,但我不知道如何模拟静态方法。我正在使用JUnit4和Mockito作为我的测试用例。有没有一个好的方法来模拟/验证这个特定的用例?


在mockito上使用powermockito。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@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(...);

    }

更多信息:

  • 为什么Mockito不模拟静态方法?


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

包装器对象成为真实静态类的外观,而您不测试这些静态类。

包装对象可以是

1
2
3
4
5
6
7
public class Slf4jMdcWrapper {
    public static final Slf4jMdcWrapper SINGLETON = new Slf4jMdcWrapper();

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

最后,被测试的类可以使用这个singleton对象,例如,在现实生活中使用默认构造函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
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注释,那么它就更容易使用了。只需让您的包装bean@applicationscoped,将其作为合作者注入(您甚至不需要混乱的构造函数进行测试),然后继续模拟。


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

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

为DriverManager创建一个接口,模拟这个接口,通过某种依赖注入注入它,并验证该模拟。


我也有类似的问题。根据PowerMock的MockStatic文档,接受的答案对我来说不起作用,直到我做了更改:@PrepareForTest(TheClassThatContainsStaticMethod.class)

我不需要使用BDDMockito

我的班级:

1
2
3
4
5
6
7
8
9
10
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();
        }
    }
}

我的测试课:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@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());
    }
}


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

例如:

1
securityAlgo = MessageDigest.getInstance(SECURITY_ALGORITHM);

对于上述代码,如果需要模拟MessageDigest类,请使用

1
@PrepareForTest(MessageDigest.class)

如果你有如下的东西:

1
2
3
4
5
6
public class CustomObjectRule {

    object = DatatypeConverter.printHexBinary(MessageDigest.getInstance(SECURITY_ALGORITHM)
             .digest(message.getBytes(ENCODING)));

}

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

1
@PrepareForTest(CustomObjectRule.class)

然后模仿这个方法:

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


要模拟静态方法,您应该使用powermock查看:https://github.com/powermock/powermock/wiki/mockstatic。Mockito不提供此功能。

你可以读一篇关于mockito的文章:http://refcardz.dzone.com/refcardz/mockito


您可以通过一点重构来完成这项工作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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以返回模拟连接、对参数进行断言等。

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

1
2
3
4
5
6
7
8
9
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);
    }
}

我还写了mockito和aspectj的组合:https://github.com/iirekm/varia/tree/develop/ajmock

您的示例变为:

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


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

示例(从测试中提取):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
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代码。(源)

Disclaimer: Mockito team thinks that the road to hell is paved with static methods. However, Mockito's job is not to protect your code from static methods. If you don’t like your team doing static mocking, stop using Powermockito in your organization. Mockito needs to evolve as a toolkit with an opinionated vision on how Java tests should be written (e.g. don't mock statics!!!). However, Mockito is not dogmatic. We don't want to block unrecommended use cases like static mocking. It's just not our job.


使用jmockit框架。这对我很有用。不必为模拟dbconenction.getConnection()方法编写语句。下面的代码就足够了。

@下面的模拟是mockit.mock package

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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;
            }
         };