it-swarm.com.ru

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

263
Naftuli Kay

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

    }

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

279
MariuszS

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

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

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

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

49
99Sono

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

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

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

16
ChrisM

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

Замечание: когда вы вызываете статический метод внутри статической сущности, вам нужно изменить класс в @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());
6
some random guy

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

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

6
marek.kapowicki

Вы можете сделать это с помощью небольшого рефакторинга:

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);
    }
}
5
Fermin Silva

Я также написал комбинацию Mockito и AspectJ: https://github.com/iirekm/varia/tree/develop/ajmock

Ваш пример становится:

when(() -> DriverManager.getConnection(...)).thenReturn(...);
3
iirekm

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
David Miguel

Используйте фреймворк 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;
            }
         };
0
Zlatan