Издевательские статические методы с Mockito

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

Ответ 1

Используйте 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(...);

    }

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

Ответ 2

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

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

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

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

Ответ 3

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

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

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

Ответ 5

У меня была аналогичная проблема. Принятый ответ не работал у меня, пока я не сделал изменение: @PrepareForTest(TheClassYouWriteTestFor.class).

И мне не нужно использовать 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());
    }
}

Ответ 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);
    }
}

Ответ 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());