Пересмешивание статических методов с помощью Mockito


374

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



5
Вы не можете с Mockito по Desing :)
MariuszS

25
@MariuszS Это не так, что Mockito (или EasyMock, или jMock) не поддерживает staticметоды насмешки , но случайно . Это ограничение (наряду с отсутствием поддержки finalклассов / методов newимитации или объектов -ed) является естественным (но непреднамеренным) следствием подхода, применяемого для реализации имитации, когда динамически создаются новые классы, которые реализуют / расширяют тип, подлежащий моделированию; другие библиотеки насмешек используют другие подходы, которые избегают этих ограничений. Это произошло и в мире .NET.
Рожерио

2
@ Rogério Спасибо за объяснение. github.com/mockito/mockito/wiki/FAQ Могу ли я издеваться над статическими методами? Нет. Mockito предпочитает ориентацию объектов и внедрение зависимостей, а не статический процедурный код, который трудно понять и изменить. За этим ограничением тоже есть дизайн :)
MariuszS

17
@MariuszS Я прочитал, что попытка отклонить законные варианты использования вместо признания инструмента имеет ограничения, которые нельзя (легко) удалить, и без предоставления какого-либо обоснованного обоснования. Кстати, вот такая дискуссия для противоположной точки зрения, со ссылками.
Rogério

Ответы:


350

Используйте PowerMockito поверх Mockito.

Пример кода:

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

    @Test
    public void shouldVerifyParameters() throws Exception {

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

        //when
        sut.execute(); // System Under Test (sut)

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

    }

Больше информации:


4
Хотя это работает в теории, на практике трудно ...
Нафтули Кей

38
К сожалению, огромный недостаток этого заключается в необходимости PowerMockRunner.
Иннокентий

18
sut.execute ()? Средства?
TejjD

4
Тестируемая система, класс, который требует макет DriverManager. kaczanowscy.pl/tomek/2011-01/testing-basics-sut-and-docs
MariuszS

8
К вашему сведению, если вы уже используете JUnit4, вы можете сделать это @RunWith(PowerMockRunner.class)и ниже @PowerMockRunnerDelegate(JUnit4.class).
EM-Creations

71

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

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

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

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


3
Я создал инструмент для автоматической генерации «смешанных» интерфейсов Java 8, которые обертывают статические вызовы: github.com/aro-tech/interface-it Сгенерированные миксины могут быть смоделированы как любой другой интерфейс, или если тестируемый вами класс «реализует» Интерфейс вы можете переопределить любой из его методов в подклассе для теста.
aro_tech

25

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

Не в состоянии выяснить? .MockStatic и?. Когда в настоящее время с JUnit 4
Тедди

PowerMock.mockStatic & Mockito.when, кажется, не работает.
Тедди

Для тех, кто видит это позже, мне нужно было ввести PowerMockito.mockStatic (StaticClass.class);
худенький

Вы должны включить артефакт Powermock-Api-Mockito Maven.
PeterS

23

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

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

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


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

Я бился головой о стену, пытаясь понять, почему мой статический класс не дразнил. Вы могли бы подумать, что во всех уроках по веб-сайтам, ОДИН бы пошел в большем, чем просто использование сценария.
SoftwareSavant

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

6

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

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


2
Пожалуйста, не ссылайтесь на сайт. Ответы должны включать в себя реальные полезные ответы. Если сайт не работает или изменяется, ответ больше не действителен.
the_new_mr

6

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



1

Поскольку этот метод является статическим, в нем уже есть все, что вам нужно для его использования, поэтому он побеждает цель насмешки. Насмешка над статическими методами считается плохой практикой.

Если вы попытаетесь это сделать, значит, что с тестированием что-то не так.

Конечно, вы можете использовать PowerMockito или любой другой фреймворк, способный сделать это, но попробуйте переосмыслить свой подход.

Например: попробуйте смоделировать / предоставить объекты, которые использует этот статический метод.


0

Используйте фреймворк JMockit . Это сработало для меня. Вам не нужно писать операторы для насмешливого метода DBConenction.getConnection (). Достаточно только приведенного ниже кода.

@Mock ниже - пакет mockit.Mock

Connection jdbcConnection = Mockito.mock(Connection.class);

MockUp<DBConnection> mockUp = new MockUp<DBConnection>() {

            DBConnection singleton = new DBConnection();

            @Mock
            public DBConnection getInstance() { 
                return singleton;
            }

            @Mock
            public Connection getConnection() {
                return jdbcConnection;
            }
         };
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.