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