Пересмешивание статических методов с помощью 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;
Я добавил расширение, которое я создал, в свой тестовый класс. У вас есть два варианта.
- Создайте для этой цели расширение и добавьте его в класс рядом с MockitoExtension, который вам также понадобится.
- Создайте расширение и наследуйте его от 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);
}
}