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

630   9  

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

Comments

    Ничего не найдено.