Издевательские статические методы с 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, издевайтесь над этим интерфейсом, вводите его через какую-то инъекцию зависимостей и проверяйте на этот макет.
Ответ 4
Чтобы издеваться над статическим методом, вы должны использовать взгляд Powermock:
https://github.com/powermock/powermock/wiki/MockStatic.
Mockito не предоставляет эту функциональность.
Вы можете прочитать хорошую статью о mockito:
http://refcardz.dzone.com/refcardz/mockito
Ответ 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());
Ответ 8
Я также написал комбинацию Mockito и AspectJ: https://github.com/iirekm/misc/tree/master/ajmock
Пример:
when(() -> DriverManager.getConnection(...)).thenReturn(...);