Знущання над статичними методами з Mockito


372

Я написав фабрику з виробництва 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
чи допомогло б це? stackoverflow.com/questions/19464975 / ...
sasankad

5
Ви не можете з Mockito по Desing :)
MariuszS

25
@MariuszS Мокето (або EasyMock, або jMock) не задуму не підтримує глузуючі staticметоди, а випадково . Це обмеження (поряд із відсутністю підтримки для глузливих finalкласів / методів чи newоб'єктів -ed) є природним (але ненавмисним) наслідком підходу, який застосовується для здійснення макетів, де динамічно створюються нові класи, які реалізують / розширюють тип, що підлягає знущанню; інші біблійні бібліотеки використовують інші підходи, які уникають цих обмежень. Це трапилося і в світі .NET.
Rogério

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
Хоча це працює теоретично, маючи важкий час на практиці ...
Naftuli Kay

38
На жаль, величезним недоліком цього є потреба в PowerMockRunner, хоча.
Інокентій

18
sut.execute ()? Засоби?
TejjD

4
Система під тестом - клас, який вимагає макету DriverManager. kaczanowscy.pl/tomek/2011-01/testing-basics-sut-and-docs
MariuszS

8
FYI, якщо ви вже використовуєте 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 bean @ApplicationScoped, введіть цю річ як співавтор (для тестування вам навіть не потрібні брудні конструктори), і продовжуйте знущатися.


3
Я створив інструмент для автоматичного генерування інтерфейсів Java 8 "mixin", які містять статичні виклики: 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);
thinkereer

Потрібно включити артефакт Мавенмок-Апі-Мокіто.
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 не забезпечує цю функціональність.

Ви можете прочитати приємну статтю про макет: 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.