Skip to content

Commit

Permalink
InjectMock should not mock an intercepted subclass
Browse files Browse the repository at this point in the history
- it should mock the implementation class instead, i.e. the bean class,
return type of a producer method, etc.
- also use BeanManager#isQualifier() to detect qualifiers of an
InjectMock/InjectSpy injection point
  • Loading branch information
mkouba committed Apr 4, 2023
1 parent a6aa204 commit 4a2b96b
Show file tree
Hide file tree
Showing 2 changed files with 51 additions and 55 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,18 @@
import java.lang.reflect.Field;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;

import jakarta.enterprise.inject.spi.Bean;
import jakarta.enterprise.inject.spi.BeanManager;
import jakarta.inject.Qualifier;

import org.mockito.Mockito;

import io.quarkus.arc.Arc;
import io.quarkus.arc.ArcContainer;
import io.quarkus.arc.ClientProxy;
import io.quarkus.arc.InjectableBean;
import io.quarkus.arc.impl.Mockable;
import io.quarkus.arc.InstanceHandle;
import io.quarkus.arc.Subclass;
import io.quarkus.test.junit.callback.QuarkusTestAfterConstructCallback;
import io.quarkus.test.junit.mockito.InjectMock;

Expand All @@ -33,51 +29,32 @@ public void afterConstruct(Object testInstance) {
InjectMock injectMockAnnotation = field.getAnnotation(InjectMock.class);
if (injectMockAnnotation != null) {
boolean returnsDeepMocks = injectMockAnnotation.returnsDeepMocks();
Object beanInstance = getBeanInstance(testInstance, field, InjectMock.class);
Optional<Object> result = createMockAndSetTestField(testInstance, field, beanInstance,
Object contextualReference = getContextualReference(testInstance, field, InjectMock.class);
Optional<Object> result = createMockAndSetTestField(testInstance, field, contextualReference,
new MockConfiguration(returnsDeepMocks));
if (result.isPresent()) {
MockitoMocksTracker.track(testInstance, result.get(), beanInstance);
MockitoMocksTracker.track(testInstance, result.get(), contextualReference);
}
}
}
current = current.getSuperclass();
}
}

private Optional<Object> createMockAndSetTestField(Object testInstance, Field field, Object beanInstance,
private Optional<Object> createMockAndSetTestField(Object testInstance, Field field, Object contextualReference,
MockConfiguration mockConfiguration) {
Class<?> beanClass = beanInstance.getClass();
// make sure we don't mock proxy classes, especially given that they don't have generics info
if (ClientProxy.class.isAssignableFrom(beanClass)) {
// and yet some of them appear to have Object as supertype, avoid them
if (beanClass.getSuperclass() != Object.class)
beanClass = beanClass.getSuperclass();
else {
// try to find the mocked interface
Set<Class<?>> foundInterf = new HashSet<>();
for (Class<?> interf : beanClass.getInterfaces()) {
if (interf == Mockable.class || interf == ClientProxy.class)
continue;
foundInterf.add(interf);
}
// only act if we found a single interface
if (foundInterf.size() == 1) {
beanClass = foundInterf.iterator().next();
}
}
}
Class<?> implementationClass = getImplementationClass(contextualReference);
Object mock;
boolean isNew;
Optional<Object> currentMock = MockitoMocksTracker.currentMock(testInstance, beanInstance);
Optional<Object> currentMock = MockitoMocksTracker.currentMock(testInstance, contextualReference);
if (currentMock.isPresent()) {
mock = currentMock.get();
isNew = false;
} else {
if (mockConfiguration.useDeepMocks) {
mock = Mockito.mock(beanClass, Mockito.RETURNS_DEEP_STUBS);
mock = Mockito.mock(implementationClass, Mockito.RETURNS_DEEP_STUBS);
} else {
mock = Mockito.mock(beanClass);
mock = Mockito.mock(implementationClass);
}
isNew = true;
}
Expand All @@ -94,38 +71,52 @@ private Optional<Object> createMockAndSetTestField(Object testInstance, Field fi
}
}

static Object getBeanInstance(Object testInstance, Field field, Class<? extends Annotation> annotationType) {
/**
* Contextual reference of a normal scoped bean is a client proxy.
*
* @param testInstance
* @param field
* @param annotationType
* @return a contextual reference of a bean
*/
static Object getContextualReference(Object testInstance, Field field, Class<? extends Annotation> annotationType) {
Type fieldType = field.getGenericType();
Annotation[] qualifiers = getQualifiers(field);
ArcContainer container = Arc.container();
BeanManager beanManager = container.beanManager();
Set<Bean<?>> beans = beanManager.getBeans(fieldType, qualifiers);
if (beans.isEmpty()) {
Annotation[] qualifiers = getQualifiers(field, beanManager);

InstanceHandle<?> handle = container.instance(fieldType, qualifiers);
if (!handle.isAvailable()) {
throw new IllegalStateException(
"Invalid use of " + annotationType.getTypeName() + " - could not resolve the bean of type: "
+ fieldType.getTypeName() + ". Offending field is " + field.getName() + " of test class "
+ testInstance.getClass());
}
Bean<?> bean = beanManager.resolve(beans);
if (!beanManager.isNormalScope(bean.getScope())) {
if (!beanManager.isNormalScope(handle.getBean().getScope())) {
throw new IllegalStateException(
"Invalid use of " + annotationType.getTypeName()
+ " - the injected bean does not declare a CDI normal scope but: " + bean.getScope().getName()
+ " - the injected bean does not declare a CDI normal scope but: "
+ handle.getBean().getScope().getName()
+ ". Offending field is " + field.getName() + " of test class "
+ testInstance.getClass());
}
return container.instance((InjectableBean<?>) bean).get();
return handle.get();
}

static Class<?> getImplementationClass(Object contextualReference) {
// Unwrap the client proxy if needed
Object contextualInstance = ClientProxy.unwrap(contextualReference);
// If the contextual instance is an intercepted subclass then mock the extended implementation class
return contextualInstance instanceof Subclass ? contextualInstance.getClass().getSuperclass()
: contextualInstance.getClass();
}

static Annotation[] getQualifiers(Field fieldToMock) {
static Annotation[] getQualifiers(Field fieldToMock, BeanManager beanManager) {
List<Annotation> qualifiers = new ArrayList<>();
Annotation[] fieldAnnotations = fieldToMock.getDeclaredAnnotations();
for (Annotation fieldAnnotation : fieldAnnotations) {
for (Annotation annotationOfFieldAnnotation : fieldAnnotation.annotationType().getAnnotations()) {
if (annotationOfFieldAnnotation.annotationType().equals(Qualifier.class)) {
qualifiers.add(fieldAnnotation);
break;
}
if (beanManager.isQualifier(fieldAnnotation.annotationType())) {
qualifiers.add(fieldAnnotation);
}
}
return qualifiers.toArray(new Annotation[0]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import org.mockito.AdditionalAnswers;
import org.mockito.Mockito;

import io.quarkus.arc.ClientProxy;
import io.quarkus.test.junit.callback.QuarkusTestAfterConstructCallback;
import io.quarkus.test.junit.mockito.InjectSpy;

Expand All @@ -18,19 +17,25 @@ public void afterConstruct(Object testInstance) {
for (Field field : current.getDeclaredFields()) {
InjectSpy injectSpyAnnotation = field.getAnnotation(InjectSpy.class);
if (injectSpyAnnotation != null) {
Object beanInstance = CreateMockitoMocksCallback.getBeanInstance(testInstance, field, InjectSpy.class);
Object spy = createSpyAndSetTestField(testInstance, field, beanInstance, injectSpyAnnotation.delegate());
MockitoMocksTracker.track(testInstance, spy, beanInstance);
Object contextualReference = CreateMockitoMocksCallback.getContextualReference(testInstance, field,
InjectSpy.class);
Object spy = createSpyAndSetTestField(testInstance, field, contextualReference,
injectSpyAnnotation.delegate());
MockitoMocksTracker.track(testInstance, spy, contextualReference);
}
}
current = current.getSuperclass();
}
}

private Object createSpyAndSetTestField(Object testInstance, Field field, Object beanInstance, boolean delegate) {
Object unwrapped = ClientProxy.unwrap(beanInstance);
Object spy = delegate ? Mockito.mock(unwrapped.getClass(), AdditionalAnswers.delegatesTo(unwrapped))
: Mockito.spy(unwrapped);
private Object createSpyAndSetTestField(Object testInstance, Field field, Object contextualReference, boolean delegate) {
Object spy;
if (delegate) {
spy = Mockito.mock(CreateMockitoMocksCallback.getImplementationClass(contextualReference),
AdditionalAnswers.delegatesTo(contextualReference));
} else {
spy = Mockito.spy(contextualReference);
}
field.setAccessible(true);
try {
field.set(testInstance, spy);
Expand Down

0 comments on commit 4a2b96b

Please sign in to comment.