Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v1.8 #31

Merged
merged 3 commits into from
Nov 30, 2024
Merged

v1.8 #31

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.MD
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Mock in Bean

[@MockInBean](src/main/java/com/teketik/test/mockinbean/MockInBean.java) and [@SpyInBean](src/main/java/com/teketik/test/mockinbean/SpyInBean.java) are alternatives to @MockBean and @SpyBean for Spring Boot tests *(>= 2.2.0 including >= 3.X.X)*.
[@MockInBean](src/main/java/com/teketik/test/mockinbean/MockInBean.java) and [@SpyInBean](src/main/java/com/teketik/test/mockinbean/SpyInBean.java) are alternatives to @MockBean and @SpyBean for Spring Boot tests *(>= 2.6.15 including >= 3.X.X)*.

They surgically replace a field value in a Spring Bean by a Mock/Spy for the duration of a test and set back the original value afterwards, leaving the Spring Context clean.

Expand Down Expand Up @@ -86,7 +86,7 @@ Simply include the maven dependency (from central maven) to start using @MockInB
<dependency>
<groupId>com.teketik</groupId>
<artifactId>mock-in-bean</artifactId>
<version>boot2-v1.7</version>
<version>boot2-v1.8</version>
<scope>test</scope>
</dependency>
```
Expand Down
4 changes: 2 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
<modelVersion>4.0.0</modelVersion>
<groupId>com.teketik</groupId>
<artifactId>mock-in-bean</artifactId>
<version>boot2-v1.7</version>
<version>boot2-v1.8</version>
<name>Mock in Bean</name>
<description>Surgically Inject Mockito Mock/Spy in Spring Beans</description>
<url>https://github.com/antoinemeyer/mock-in-bean</url>
Expand Down Expand Up @@ -36,7 +36,7 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.2.0.RELEASE</version>
<version>2.6.15</version>
</parent>

<dependencies>
Expand Down
17 changes: 15 additions & 2 deletions src/main/java/com/teketik/test/mockinbean/BeanFieldState.java
Original file line number Diff line number Diff line change
@@ -1,21 +1,34 @@
package com.teketik.test.mockinbean;

import org.springframework.test.context.TestContext;
import org.springframework.util.ReflectionUtils;

import java.lang.reflect.Field;

class BeanFieldState extends FieldState {

private Object bean;
final Object bean;

final Object originalValue;

public BeanFieldState(Object bean, Field field, Object originalValue, Definition definition) {
super(field, originalValue, definition);
super(field, definition);
this.bean = bean;
this.originalValue = originalValue;
}

@Override
public Object resolveTarget(TestContext testContext) {
return bean;
}

public void rollback(TestContext testContext) {
final Object target = resolveTarget(testContext);
ReflectionUtils.setField(field, target, originalValue);
}

public Object createMockOrSpy() {
return definition.create(originalValue);
}

}
31 changes: 28 additions & 3 deletions src/main/java/com/teketik/test/mockinbean/BeanUtils.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.teketik.test.mockinbean;

import org.springframework.aop.TargetSource;
import org.springframework.aop.framework.Advised;
import org.springframework.aop.framework.AopProxyUtils;
import org.springframework.aop.support.AopUtils;
import org.springframework.context.ApplicationContext;
Expand Down Expand Up @@ -44,8 +46,8 @@ static <T> T findBean(Class<T> type, @Nullable String name, ApplicationContext a
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("No beans of type " + type + " and name " + name));
}
return AopUtils.isAopProxy(beanOrProxy)
? (T) AopProxyUtils.getSingletonTarget(beanOrProxy)
return AopUtils.isAopProxy(beanOrProxy)
? (T) AopProxyUtils.getSingletonTarget(beanOrProxy)
: beanOrProxy;
}

Expand Down Expand Up @@ -73,7 +75,7 @@ static Field findField(Class<?> clazz, @Nullable String name, Class<?> type) {
} else {
results[1] = Boolean.FALSE; //multiple matching fields
}
}, field -> field.getType().equals(type));
}, field -> field.getType().isAssignableFrom(type));
if (results[0] != null) {
Assert.isTrue(
!(results[0] instanceof Boolean),
Expand All @@ -96,4 +98,27 @@ static Field findField(Class<?> clazz, @Nullable String name, Class<?> type) {
return null;
}

static @Nullable TargetSource getProxyTarget(Object candidate) {
try {
while (AopUtils.isAopProxy(candidate) && candidate instanceof Advised) {
Advised advised = (Advised) candidate;
TargetSource targetSource = advised.getTargetSource();

if (targetSource.isStatic()) {
Object target = targetSource.getTarget();

if (target == null || !AopUtils.isAopProxy(target)) {
return targetSource;
}
candidate = target;
} else {
return null;
}
}
} catch (Throwable ex) {
throw new IllegalStateException("Failed to unwrap proxied object", ex);
}
return null;
}

}
7 changes: 1 addition & 6 deletions src/main/java/com/teketik/test/mockinbean/FieldState.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.teketik.test.mockinbean;

import org.springframework.lang.Nullable;
import org.springframework.test.context.TestContext;

import java.lang.reflect.Field;
Expand All @@ -9,14 +8,10 @@ abstract class FieldState {

final Field field;

@Nullable
final Object originalValue;

final Definition definition;

public FieldState(Field targetField, Object originalValue, Definition definition) {
public FieldState(Field targetField, Definition definition) {
this.field = targetField;
this.originalValue = originalValue;
this.definition = definition;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import org.junit.jupiter.api.Nested;
import org.mockito.Mock;
import org.mockito.Spy;
import org.springframework.aop.TargetSource;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.test.context.TestContext;
import org.springframework.test.context.TestExecutionListener;
Expand Down Expand Up @@ -59,30 +60,22 @@ public void beforeTestClass(TestContext testContext) throws Exception {
for (InBeanDefinition inBeanDefinition : definitionToInbeans.getValue()) {
final Object inBean = BeanUtils.findBean(inBeanDefinition.clazz, inBeanDefinition.name, testContext.getApplicationContext());
beanField = BeanUtils.findField(inBean.getClass(), definition.getName(), mockOrSpyType);
Assert.notNull(beanField, "Cannot find any field for definition:" + definitionToInbeans.getKey());
beanField.setAccessible(true);
originalValues.add(
new BeanFieldState(
inBean,
beanField,
ReflectionUtils.getField(
beanField,
inBean
),
definition
)
);
final Object beanFieldValue = ReflectionUtils.getField(beanField, inBean);
final TargetSource proxyTarget = BeanUtils.getProxyTarget(beanFieldValue);
final BeanFieldState beanFieldState;
if (proxyTarget != null) {
beanFieldState = new ProxiedBeanFieldState(inBean, beanField, beanFieldValue, proxyTarget, definition);
} else {
beanFieldState = new BeanFieldState(inBean, beanField, beanFieldValue, definition);
}
originalValues.add(beanFieldState);
}
Assert.notNull(beanField, "Cannot find any field for definition:" + definitionToInbeans.getKey());
Assert.isTrue(visitedFields.add(beanField), beanField + " can only be mapped once, as a mock or a spy, not both!");
final Field testField = ReflectionUtils.findField(targetTestClass, definition.getName(), mockOrSpyType);
testField.setAccessible(true);
originalValues.add(
new TestFieldState(
testField,
null,
definition
)
);
originalValues.add(new TestFieldState(testField, definition));
}
testContext.setAttribute(ORIGINAL_VALUES_ATTRIBUTE_NAME, originalValues);
super.beforeTestClass(testContext);
Expand All @@ -100,10 +93,13 @@ public void beforeTestMethod(TestContext testContext) throws Exception {
final Map<Object, Object> spyTracker = new IdentityHashMap<>();
//First loop to setup all the mocks and spies
fieldStates
.stream()
.filter(BeanFieldState.class::isInstance)
.map(BeanFieldState.class::cast)
.forEach(fieldState -> {
Object mockOrSpy = mockOrSpys.get(fieldState.definition);
if (mockOrSpy == null) {
mockOrSpy = fieldState.definition.create(fieldState.originalValue);
mockOrSpy = fieldState.createMockOrSpy();
mockOrSpys.put(fieldState.definition, mockOrSpy);
if (fieldState.definition instanceof SpyDefinition) {
spyTracker.put(fieldState.originalValue, mockOrSpy);
Expand Down Expand Up @@ -143,15 +139,10 @@ public void afterTestClass(TestContext testContext) throws Exception {
return;
}
((LinkedList<FieldState>) testContext.getAttribute(ORIGINAL_VALUES_ATTRIBUTE_NAME))
.forEach(fieldValue -> {
if (fieldValue.originalValue != null) {
ReflectionUtils.setField(
fieldValue.field,
fieldValue.resolveTarget(testContext),
fieldValue.originalValue
);
}
});
.stream()
.filter(BeanFieldState.class::isInstance)
.map(BeanFieldState.class::cast)
.forEach(fieldState -> fieldState.rollback(testContext));
ROOT_TEST_CONTEXT_TRACKER.remove(testContext.getTestClass());
super.afterTestClass(testContext);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.teketik.test.mockinbean;

import org.springframework.aop.TargetSource;
import org.springframework.test.context.TestContext;
import org.springframework.test.util.ReflectionTestUtils;

import java.lang.reflect.Field;

/**
* Special kind of {@link BeanFieldState} handling proxied beans (like aspects).<br>
* The mock is not injected into the <code>field</code> but into the <code>target</code> of its {@link TargetSource}.
* @author Antoine Meyer
* @see https://github.com/antoinemeyer/mock-in-bean/issues/23
*/
class ProxiedBeanFieldState extends BeanFieldState {

private static void setTargetSourceValue(TargetSource targetSource, Object value) {
ReflectionTestUtils.setField(targetSource, "target", value);
}

final TargetSource proxyTargetSource;

final Object proxyTargetOriginalValue;

public ProxiedBeanFieldState(Object inBean, Field beanField, Object beanFieldValue, TargetSource proxyTargetSource, Definition definition) throws Exception {
super(inBean, beanField, beanFieldValue, definition);
this.proxyTargetSource = proxyTargetSource;
this.proxyTargetOriginalValue = proxyTargetSource.getTarget();
}

@Override
public void rollback(TestContext testContext) {
setTargetSourceValue(proxyTargetSource, proxyTargetOriginalValue);
}

@Override
public Object createMockOrSpy() {
Object applicableMockOrSpy = definition.create(proxyTargetOriginalValue);
setTargetSourceValue(proxyTargetSource, applicableMockOrSpy);
return originalValue; //the 'mock or spy' to operate for proxied beans are the actual proxy
}

}
4 changes: 2 additions & 2 deletions src/main/java/com/teketik/test/mockinbean/TestFieldState.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@

class TestFieldState extends FieldState {

TestFieldState(Field targetField, Object originalValue, Definition definition) {
super(targetField, originalValue, definition);
TestFieldState(Field targetField, Definition definition) {
super(targetField, definition);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.teketik.test.mockinbean.test;


import com.teketik.test.mockinbean.MockInBean;
import com.teketik.test.mockinbean.test.InterfaceImplementationTestConfig.LoggingService;
import com.teketik.test.mockinbean.test.InterfaceImplementationTestConfig.ProviderServiceImpl;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Import;

@Import(InterfaceImplementationTestConfig.class)
public class InterfaceImplementation1Test extends BaseTest {

@Autowired
protected LoggingService loggingService;

@MockInBean(LoggingService.class)
private ProviderServiceImpl providerService;

@MockInBean(LoggingService.class)
private ProviderServiceImpl providerServiceImpl;

@Test
public void test() {
Mockito.when(providerService.provideValue()).thenReturn("mocked value");
Mockito.when(providerServiceImpl.provideValue()).thenReturn("mocked value 2");

Assertions.assertEquals("mocked value", loggingService.logCurrentValue());
Assertions.assertEquals("mocked value 2", loggingService.logCurrentValueWithImpl());
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.teketik.test.mockinbean.test;


import com.teketik.test.mockinbean.MockInBean;
import com.teketik.test.mockinbean.test.InterfaceImplementationTestConfig.LoggingService;
import com.teketik.test.mockinbean.test.InterfaceImplementationTestConfig.ProviderService;
import com.teketik.test.mockinbean.test.InterfaceImplementationTestConfig.ProviderServiceImpl;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Import;

@Import(InterfaceImplementationTestConfig.class)
public class InterfaceImplementation2Test extends BaseTest {

@Autowired
protected LoggingService loggingService;

@MockInBean(LoggingService.class)
private ProviderService providerServiceMock;

@MockInBean(LoggingService.class)
private ProviderServiceImpl providerServiceImpl;

@Test
public void test() {
Mockito.when(providerServiceMock.provideValue()).thenReturn("mocked value");
Mockito.when(providerServiceImpl.provideValue()).thenReturn("mocked value 2");

Assertions.assertEquals("mocked value", loggingService.logCurrentValue());
Assertions.assertEquals("mocked value 2", loggingService.logCurrentValueWithImpl());
}

}
Loading