用Mockito模拟静态方法


371

我已经写了一家工厂来生产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。有没有模拟或验证此特定用例的好方法?



5
您无法通过
嘲笑来实现模仿

25
@MariuszS Mockito(或EasyMock或jMock)不是设计上不支持模拟static方法,而是偶然的。这种限制(不支持模拟final类/方法或new-ed对象)是实现模拟的方法的自然结果(但并非意料之外),在该方法中,动态创建新类来实现/扩展要模拟的类型;其他模拟库使用其他避免这些限制的方法。这也发生在.NET世界中。
罗杰里奥2015年

2
@Rogério感谢您的解释。github.com/mockito/mockito/wiki/FAQ 我可以模拟静态方法吗? 不需要。Mockito比静态的,难以理解和更改的过程代码更喜欢面向对象和依赖项注入。这个限制背后也有一些设计:)
MariuszS 2015年

17
@MariuszS我读到,这是一种试图消除合法用例的尝试,而不是承认该工具具有无法(轻松)消除的限制,并且没有提供任何合理的理由。顺便说一句,这里是针对相反观点的讨论,并附有参考文献。
罗杰里奥

Answers:


350

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

    }

更多信息:


4
尽管这在理论上
可行,但

38
不幸的是,这样做的最大缺点是需要PowerMockRunner。
Innokenty 2014年

18
sut.execute()?手段?
TejjD 2015年

4
System Under Test,需要模拟DriverManager的类。kaczanowscy.pl/tomek/2011-01/testing-basics-sut-and-docs
MariuszS 2015年

8
仅供参考,如果您已经使用JUnit4你可以做@RunWith(PowerMockRunner.class)再下面@PowerMockRunnerDelegate(JUnit4.class)
EM-Creations

71

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

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

包装对象可能像

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批注,那么它会更加容易。只需使您的包装器bean @ApplicationScoped,将其作为协作者注入即可(您甚至不需要杂乱的构造函数进行测试),然后继续进行模拟。


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

25

我有一个类似的问题。直到我进行更改后@PrepareForTest(TheClassThatContainsStaticMethod.class),接受的答案才对我不起作用: 根据PowerMock的嘲笑Static文档

而且我不必使用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和?。,当前使用JUnit 4
Teddy

PowerMock.mockStatic和Mockito.when似乎不起作用。
泰迪

对于以后看到它的任何人,对我来说,我必须键入PowerMockito.mockStatic(StaticClass.class);。
thinkereer

您需要包括powermock-api-mockito maven人工制品。
PeterS

23

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

如果不能更改测试框架,则可以执行以下操作:

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


7

观察:在静态实体中调用静态方法时,需要在@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());

我正在用力撞墙,试图弄清楚为什么我的静态课没有嘲笑。您可能会认为,在Internet上的所有教程中,“ ONE”的用途不仅仅包括准用例。
SoftwareSavant

6

您可以进行一些重构:

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代码。(来源

免责声明:Mockito团队认为通向静态的道路铺平了道路。但是,Mockito的工作不是保护代码免受静态方法的侵害。如果您不喜欢团队进行静态模拟,请停止在组织中使用Powermockito。Mockito需要发展为一个工具包,并对如何编写Java测试抱有坚定的看法(例如,不要模拟静态!!!)。但是,Mockito不是教条。我们不想阻止不建议使用的情况,例如静态模拟。这不是我们的工作。



1

由于该方法是静态的,因此它已经拥有使用它所需的一切,因此它无法实现模拟的目的。模拟静态方法被认为是不好的做法。

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

当然,您可以使用PowerMockito或任何其他能够执行此操作的框架,但是请尝试重新考虑您的方法。

例如:尝试模拟/提供静态方法消耗的对象。


0

使用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;
            }
         };
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.