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

23 ответа

Решение

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

    }

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

Мокинг статических методов в Mockito возможен начиная с Mockito 3.4.0. Подробнее см.:

https://github.com/mockito/mockito/tree/v3.4.0

https://github.com/mockito/mockito/issues/1013.

В вашем случае примерно так:

  @Test
  public void testStaticMockWithVerification() throws SQLException {
    try (MockedStatic<DriverManager> dummy = Mockito.mockStatic(DriverManager.class)) {
      DatabaseConnectionFactory factory = new MySQLDatabaseConnectionFactory();
      dummy.when(() -> DriverManager.getConnection("arg1", "arg2", "arg3"))
        .thenReturn(new Connection() {/*...*/});

      factory.getConnection();

      dummy.verify(() -> DriverManager.getConnection(eq("arg1"), eq("arg2"), eq("arg3")));
    }
  }

ПРИМЕЧАНИЕ: эта функция требует наличия зависимости mockito-inline.

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

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

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

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

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

Для тех, кто использует JUnit 5, Powermock не подходит. Вам потребуются следующие зависимости, чтобы успешно смоделировать статический метод с помощью только Mockito.

testCompile    group: 'org.mockito', name: 'mockito-core',           version: '3.6.0'
testCompile    group: 'org.mockito', name: 'mockito-junit-jupiter',  version: '3.6.0'
testCompile    group: 'org.mockito', name: 'mockito-inline',         version: '3.6.0'

mockito-junit-jupiter добавить поддержку JUnit 5.

А поддержка имитации статических методов обеспечивается mockito-inline зависимость.

Пример:

@Test
void returnUtilTest() {
    assertEquals("foo", UtilClass.staticMethod("foo"));

    try (MockedStatic<UtilClass> classMock = mockStatic(UtilClass.class)) {

        classMock.when(() -> UtilClass.staticMethod("foo")).thenReturn("bar");

        assertEquals("bar", UtilClass.staticMethod("foo"));
     }

     assertEquals("foo", UtilClass.staticMethod("foo"));
}

Блок try-catch используется для того, чтобы статический макет оставался временным, поэтому он макетируется только в этой области. Но использовать блок try-catch не обязательно.

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

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

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

Замечание: когда вы вызываете статический метод внутри статического объекта, вам нужно изменить класс в @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/varia/tree/develop/ajmock

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

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

Я нашел одно решение в Mockito. Эта функция доступна только в версии от 3.4.0

https://asolntsev.github.io/en/2020/07/11/mockito-static-methods/

  • зависимость

    В вашем build.gradle замените mockito-core: 3.3.3 на mockito-inline:3.4.0:

            testImplementation('org.mockito:mockito-inline:3.4.0')
    
  • над чем мы будем издеваться

             class Buddy 
     {
         static String name() 
         {
            return "John";
         }
     }
    
  • Смокните статический метод

                @Test
        void lookMomICanMockStaticMethods() 
        {
             assertThat(Buddy.name()).isEqualTo("John");
    
            try (MockedStatic<Buddy> theMock = Mockito.mockStatic(Buddy.class)) 
            {
                theMock.when(Buddy::name).thenReturn("Rafael");
                assertThat(Buddy.name()).isEqualTo("Rafael");
            }
    
            assertThat(Buddy.name()).isEqualTo("John");
        }
    

Думаю, это может нам помочь.

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

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

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

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

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

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

            try (MockedStatic<Tester> tester = Mockito.mockStatic(Tester.class)) {
            tester.when(() -> Tester.testStatic("Testing..")).thenReturn(mock(ReturnObject.class));
    //Here you have to write the test cases
      }

В приведенном выше примере мы должны имитировать метод testStatic класса Tester с входным параметром «Testing...». Здесь этот метод вернет объект типа класса ReturnObject. Следовательно, мы пишем mockito, когда цепочка, как указано выше.

Не забудьте добавить ниже зависимость в свой Gradle/maven

          testImplementation 'org.mockito:mockito-inline:4.3.1'

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

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

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

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

Вы можете издеваться над статикой, не добавляя никаких зависимостей и не используя PowerMock. Это благодаря такому трюку :)

Добавьте файл org.mockito.plugins.MockMaker в папку src/test/resources/mockito-extensions/ со следующим содержимым:

      mock-maker-inline

Здесь я делюсь своим решением mockito MockStatic, основанным на расширении, как и было обещано в моем ответе на решение leokom.

Итак, почему Mockito выбирает попытку с ресурсами? Ну, просто потому, что хотят содержать корабль в чистоте. В конце концов, это хорошее программирование. Try-with-resources позволяет конструировать с гарантированным вызовом метода close. Но в JUnit это уже есть в BeforeEach и AfterEach. И их можно легко добавить для общих целей в каждый тестовый класс, используя расширение, которое реализует BeforeEachCallback и AfterEachCallback.

Так много для теории. Давайте сделаем статичный макет для

      Instant.now()

Я начал с аннотации, чтобы иметь возможность помечать поля в моем тестовом классе, которые я хочу использовать в качестве статических макетов.

      @Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface StaticMock {

}

Это позволяет мне создать поле в моем тестовом классе для статической имитации, которое я могу легко найти в своем классе Extension.

        @StaticMock
  private MockedStatic<Instant> staticInstantMock;

Я добавил расширение, которое я создал, в свой тестовый класс. У вас есть два варианта.

  1. Создайте для этой цели расширение и добавьте его в класс рядом с MockitoExtension, который вам также понадобится.
  2. Создайте расширение и наследуйте его от MockitoExtension. Теперь вы можете заменить MockitoExtension в своем тестовом классе.

Я использовал последний из двух.

      @ExtendWith({CompanyMockitoExtension.class})
class MyExtendedTestClass {

Теперь нам нужно что-то, что будет возвращено для статики при ее вызове:

        @Mock
  private Instant now;

  staticInstantMock.when(Instant::now).thenReturn(now);

Весь тестовый класс:

      @ExtendWith({CompanyMockitoExtension.class})
class MyExtendedTestClass {

  @StaticMock
  private MockedStatic<Instant> staticInstantMock;

  @Mock
  private Instant now;

  @Test
  void myTestMethod() {
    staticInstantMock.when(Instant::now).thenReturn(now);

    assertThat(Instant::now).isSameAs(now); // This would normally happen in the class you are testing...
  }
}

Теперь давайте взглянем на класс Extension.

      import static org.mockito.Mockito.mockStatic;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.List;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;

public class CompanyMockitoExtension extends MockitoExtension {

  @Override
  public void beforeEach(ExtensionContext context) {
    super.beforeEach(context); // Don't forget to call the super!!
    if (context.getTestInstance().isEmpty()) { // Just to be sure...
      return;
    }
    // Get the unit test instance
    Object testSubject = context.getTestInstance().get();
    initializeStaticMocks(testSubject);
  }

  private void initializeStaticMocks(Object testSubject) {
    // Find all fields that I want to static mock
    List<Field> staticMockFields = ReflectionHelper.getFieldsWithAnnotation(testSubject, StaticMock.class);
    staticMockFields.forEach(field -> initializeStaticMock(field, testSubject));
  }

  private void initializeStaticMock(Field field, Object testSubject) {
    // Get the type of the static mock. It is within the generic MockedStatic<> class type.
    Class<?> typeForStaticMock = (Class<?>) ReflectionHelper.getTypesForGeneric(field)[0];
    try {
      // Now set the field with the mockStatic method of Mockito.
      field.setAccessible(true);
      field.set(testSubject, mockStatic(typeForStaticMock));
    } catch (IllegalAccessException e) {
      throw new RuntimeException("Failed to instantiate Static Mock with type: " + typeForStaticMock.getName());
    }
  }

  @Override
  public void afterEach(ExtensionContext context) {
    super.afterEach(context); // Again, do not forget to call the super.
    if (context.getTestInstance().isEmpty()) {
      return;
    }
    Object testSubject = context.getTestInstance().get();
    closeStaticMocks(testSubject); // Close all static mocks.
  }

  private void closeStaticMocks(Object testSubject) {
    // Again find all fields we annotated
    List<Field> staticMockFields = ReflectionHelper.getFieldsWithAnnotation(testSubject, StaticMock.class);
    staticMockFields.forEach(field -> closeStaticMock(field, testSubject));
  }

  private void closeStaticMock(Field field, Object testSubject) {
    // Get the instance and simply call close.
    MockedStatic<?> mockedStaticInstance = ReflectionHelper.getFieldInstance(field, testSubject, MockedStatic.class);
    mockedStaticInstance.close();
  }
}

Хорошая вещь в этом расширении заключается в том, что вы можете добавить дополнительные насмешливые вещи. Я добавил проверку отсутствия взаимодействий со всеми макетами в файле AfterEach. Теперь это происходит автоматически, когда мы используем это расширение. Я также добавил такое же поведение для имитации конструкции, как и для статической имитации.

Как видите, я создал свой собственный вспомогательный класс отражения. Я знаю, что есть некоторые стандартные вспомогательные классы отражения, и они могут быть лучше. Вот мой для этой цели.

      public class ReflectionHelper {

  public static List<Field> getFieldsWithAnnotation(
      Object testSubject,
      Class<? extends Annotation> annotationType
  ) {
    Class<?> testSubjectClass = testSubject.getClass();

    return Arrays.stream(testSubjectClass.getDeclaredFields())
                 .filter(field -> field.isAnnotationPresent(annotationType))
                 .collect(toUnmodifiableList());
  }

  public static List<Field> getCollectionFields(Object testSubject) {
    Class<?> testSubjectClass = testSubject.getClass();

    return Arrays.stream(testSubjectClass.getDeclaredFields())
                 .filter(field -> Collection.class.isAssignableFrom(field.getType()))
                 .collect(toUnmodifiableList());
  }

  @SuppressWarnings("unchecked")
  public static <T> T getFieldInstance(Field field, Object testSubject, Class<T> type) {
    return (T) getFieldInstance(field, testSubject);
  }

  public static Object getFieldInstance(Field field, Object testSubject) {
    try {
      boolean isStatic = isStatic(field.getModifiers());
      Object context = isStatic ? null : testSubject;
      field.setAccessible(true);
      return field.get(context);
    } catch (IllegalAccessException e) {
      throw new RuntimeException("Failed to get instance of field.");
    }
  }

  public static Type[] getTypesForGeneric(Field field) {
    ParameterizedType parameterizedType = (ParameterizedType) field.getGenericType();
    return parameterizedType.getActualTypeArguments();
  }
}

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

Как отмечают @leokom, так как Mockito 3.4.0 вы можете использовать Mockito.mockStatic функция для создания макетов статических методов. Обратите внимание, что есть исправления для этой функции в версиях v3.4.2 и v3.4.6. Рассмотрите возможность использования более новой версии.

В вашем случае это могло быть примерно так:

  @Test
  public void testStaticMockWithVerification() throws SQLException {
    try (MockedStatic<DriverManager> dummy = Mockito.mockStatic(DriverManager.class)) {
      DatabaseConnectionFactory factory = new MySQLDatabaseConnectionFactory();
      dummy.when(() -> DriverManager.getConnection("arg1", "arg2", "arg3"))
        .thenReturn(new Connection() {/*...*/});

      factory.getConnection();

      dummy.verify(() -> DriverManager.getConnection(eq("arg1"), eq("arg2"), eq("arg3")));
    }
  }

Мой пример того, как я издеваюсь над конечными статическими полями Mockito:

      import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.broker.provider.ConfigConstants;
import org.keycloak.broker.provider.IdentityBrokerException;
import org.keycloak.models.*;
import org.keycloak.provider.ProviderConfigProperty;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.junit.jupiter.MockitoExtension;

import java.util.List;
import java.util.Map;
import java.util.stream.Stream;

import static org.junit.Assert.assertEquals;
import static org.junit.jupiter.api.Assertions.*;
import static org.keycloak.broker.provider.IdentityProviderMapper.ANY_PROVIDER;
import static org.mockito.Mockito.verify;
import static org.powermock.api.mockito.PowerMockito.when;

@ExtendWith(MockitoExtension.class)
class HardcodedGroupMapperTest {
    @Mock
    private KeycloakSession session;
    @Mock
    private RealmModel realm;
    @Mock
    private Logger logger;
    
    private final HardcodedGroupMapper sut = new HardcodedGroupMapper();

    
     @Test
void shouldReturnEmptyTokenWhenHttpResponseStatusCode400() throws IOException {
    prepareMockEnvironment();
    when(statusLine.getStatusCode()).thenReturn(400);
    Whitebox.setInternalState(AeAuthTokenOidcMapper.class, "logger", logger);

    AccessToken token = new AccessToken();
    token.setOtherClaims("tenant", "tenant1");

    AccessToken transformedToken = sut.transformAccessToken(token, mappingModel, session, userSession, clientSessionCtx);

    String expectedJsonBody = "{\"platSite\":[\"plat-site\",\"site1\",\"site2\"],\"expiresIn\":259200,\"customerAssociation\":{\"ecrId\":\"tenant1\",\"ipAddress\":\"127.0.0.1\"}}";
    Assertions.assertNull(transformedToken.getOtherClaims().get("auth_token"));
    verify(logger).error("Bad requeset from access key API " + expectedJsonBody);
}
}

Зависимость для приведенного выше кода (включен PowerMock для имитации статических конечных полей с помощью Whitebox):

      <dependency>
        <groupId>org.powermock</groupId>
        <artifactId>powermock-api-mockito2</artifactId>
        <version>2.0.9</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.powermock</groupId>
        <artifactId>powermock-module-junit4</artifactId>
        <version>2.0.9</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter-api</artifactId>
        <version>5.8.0</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.junit.jupiter</groupId>
        <artifactId>junit-jupiter-engine</artifactId>
        <version>5.8.0</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.mockito</groupId>
        <artifactId>mockito-junit-jupiter</artifactId>
        <version>5.4.0</version>
        <scope>test</scope>
    </dependency>

Как я издеваюсь с помощью статических методов PowerMock:

          import org.junit.Test;
import org.junit.runner.RunWith;
import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.broker.provider.ConfigConstants;
import org.keycloak.models.*;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.mockito.Mock;
import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;

import java.util.Map;

import static org.mockito.Mockito.verify;
import static org.powermock.api.mockito.PowerMockito.mockStatic;
import static org.powermock.api.mockito.PowerMockito.when;

@RunWith(PowerMockRunner.class)
@PrepareForTest({KeycloakModelUtils.class})
class HardcodedGroupMapperStaticTest {
    public HardcodedGroupMapperStaticTest() {
    }

    @Mock
    private KeycloakSession session;
    @Mock
    private RealmModel realm;
    @Mock
    private UserModel user;
    @Mock
    private IdentityProviderMapperModel mapperModel;
    @Mock
    private BrokeredIdentityContext context;
    @Mock
    private GroupModel group;
    private final HardcodedGroupMapper sut = new HardcodedGroupMapper();

    @Test
    public void shouldImportNewUser() {
        String testGroup = "testGroup";
        when(mapperModel.getConfig()).thenReturn(Map.of(ConfigConstants.GROUP, testGroup));
        mockStatic(KeycloakModelUtils.class);
        when(KeycloakModelUtils.findGroupByPath(realm, testGroup)).thenReturn(group);
        sut.importNewUser(session, realm, user, mapperModel, context);
        verify(user).joinGroup(group);
// assert ...
// assert ...
    }

}

Зависимость для приведенного выше кода ( https://howtodoinjava.com/java/library/mock-testing-using-powermock-with-junit-and-mockito/ ):

          <dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-core</artifactId>
    <version>3.12.4</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.powermock</groupId>
    <artifactId>powermock-core</artifactId>
    <version>2.0.9</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.powermock</groupId>
    <artifactId>powermock-api-mockito2</artifactId>
    <version>2.0.9</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.powermock</groupId>
    <artifactId>powermock-module-junit4</artifactId>
    <version>2.0.9</version>
    <scope>test</scope>
</dependency>

Внимательно следите за своими зависимостями и не смешивайте аннотации Mockito @Mock и Powermock @Mock.

Для имитации статических функций я смог сделать это следующим образом:

  • создать функцию-оболочку в некотором вспомогательном классе / объекте. (использование варианта имени может быть полезно для разделения вещей и удобства обслуживания.)
  • используйте эту оболочку в своих кодах. (Да, коды должны быть реализованы с учетом тестирования.)
  • имитируйте функцию оболочки.

фрагмент кода оболочки (не совсем функциональный, просто для иллюстрации)

      class myWrapperClass ...
    def myWrapperFunction (...) {
        return theOriginalFunction (...)
    }

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

Немного рефакторинг:

      public class MySQLDatabaseConnectionFactory implements DatabaseConnectionFactory {
    ConnectionSupplier connectionSupplier = () -> SupplierDriverManager.getConnection();

    public void setConnSupplier(ConnectionSupplier supplier) {
        this.connectionSupplier = supplier;
    }

    @Override 
    public Connection getConnection() {
        try {
            return connectionSupplier.conn();
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }
    @FunctionalInterface
    interface ConnectionSupplier {
        Connection conn();
    }
}

Затем вы можете использоватьmockito:

      MySQLDatabaseConnectionFactory.ConnectionSupplier connectionSupplier = mock(MySQLDatabaseConnectionFactory.ConnectionSupplier.class);
when(connectionSupplier.conn()).thenReturn(yourMockObject);
yourConnectionFactory.setConnSupplier(connectionSupplier);

Существует простое решение - использовать java FunctionalInterface и затем добавить этот интерфейс в качестве зависимости для класса, который вы пытаетесь тестировать.

Издевательство над LocalDate.now() для стабильных тестов.

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

Mockito.CALLS_REAL_METHODS гарантирует, что другие методы будут работать как обычно. В противном случае LocalDate.of() вернет значение null.

      private static final LocalDate TODAY_IS_2023_03_13 = LocalDate.of(2023, 03, 13);
@Test
void now() {
try (MockedStatic<LocalDate> localDateStaticMock = Mockito.mockStatic(LocalDate.class, Mockito.CALLS_REAL_METHODS)) {
  localDateStaticMock.when(LocalDate::now).thenReturn(TODAY_IS_2023_03_13);

  assertThat(LocalDate.now()).isEqualTo(TODAY_IS_2023_03_13);
assertThat(LocalDate.of(2023,03,16)).isNotNull().isEqualTo(TODAY_IS_2023_03_13);
}

}

Другие вопросы по тегам