Насмешливые статические методы с Mockito

Я написал завод по производству 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 для своих тестовых случаев. Есть ли хороший способ издеваться / проверять этот конкретный вариант использования?

9 ответов


использовать PowerMockito сверху Mockito.

пример кода:

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

    @Test
    public void testName() throws Exception {

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

        //when
        sut.execute();

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

    }

дополнительная информация:


типичная стратегия уклонения от статических методов, которую вы не можете избежать, заключается в создании обернутых объектов и использовании объектов-оболочек.

объекты-оболочки становятся фасадами реальных статических классов, и вы не тестируете их.

объект-обертка может быть чем-то вроде

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, введите эту вещь в качестве сотрудника (вам даже не нужны грязные конструкторы для тестирования) и продолжайте издеваться.


Как упоминалось ранее, вы не можете издеваться над статическими методами с помощью mockito.

Если изменение структуры тестирования не является опцией, вы можете сделать следующее:

создайте интерфейс для DriverManager, издевайтесь над этим интерфейсом, вводите его через какую-то инъекцию зависимостей и проверяйте на этом макете.


у меня была аналогичная проблема. Принятый ответ не работа для меня, пока я не внес изменения: @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());
    }
}

чтобы издеваться над статическим методом, вы должны использовать Powermock: https://github.com/powermock/powermock/wiki/MockStatic. Mockito не дает эта функция.

вы можете прочитать хорошую статью о mockito: http://refcardz.dzone.com/refcardz/mockito


вы можете сделать это с немного рефакторинга:

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

наблюдение: когда вы вызываете статический метод в статическом объекте, вам нужно изменить класс в @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());

Я также написал комбинацию Mockito и AspectJ:https://github.com/iirekm/misc/tree/master/ajmock

ваш пример будет:

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

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 (например, не издевайтесь над статикой!!!). Однако Мокито не догматичен. Мы не хотим блокировать случаях использовать вредные как статические издевательски. Это просто не наша работа.