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

JUnit support - disable observers declared on mocked beans #11122

Merged
merged 1 commit into from
Aug 4, 2020
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@
import javax.enterprise.context.spi.Contextual;
import javax.enterprise.context.spi.CreationalContext;
import javax.enterprise.inject.IllegalProductException;
import javax.enterprise.inject.TransientReference;
import javax.enterprise.inject.literal.InjectLiteral;
import javax.enterprise.inject.spi.InterceptionType;
import javax.interceptor.InvocationContext;
Expand All @@ -70,9 +69,7 @@
public class BeanGenerator extends AbstractGenerator {

static final String BEAN_SUFFIX = "_Bean";

static final String PRODUCER_METHOD_SUFFIX = "_ProducerMethod";

static final String PRODUCER_FIELD_SUFFIX = "_ProducerField";

protected static final String FIELD_NAME_DECLARING_PROVIDER_SUPPLIER = "declaringProviderSupplier";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ public List<Resource> generateResources(ReflectionRegistration reflectionRegistr
generateSources, reflectionRegistration, existingClasses);
ObserverGenerator observerGenerator = new ObserverGenerator(annotationLiterals, applicationClassPredicate,
privateMembers, generateSources, reflectionRegistration, existingClasses, observerToGeneratedName,
injectionPointAnnotationsPredicate);
injectionPointAnnotationsPredicate, allowMocking);
AnnotationLiteralGenerator annotationLiteralsGenerator = new AnnotationLiteralGenerator(generateSources);

List<Resource> resources = new ArrayList<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
import io.quarkus.arc.ClientProxy;
import io.quarkus.arc.InjectableBean;
import io.quarkus.arc.InjectableContext;
import io.quarkus.arc.MockableProxy;
import io.quarkus.arc.impl.CreationalContextImpl;
import io.quarkus.arc.impl.Mockable;
import io.quarkus.arc.processor.ResourceOutput.Resource;
import io.quarkus.gizmo.AssignableResultHandle;
import io.quarkus.gizmo.BytecodeCreator;
Expand Down Expand Up @@ -48,6 +48,8 @@ public class ClientProxyGenerator extends AbstractGenerator {
static final String CLIENT_PROXY_SUFFIX = "_ClientProxy";

static final String DELEGATE_METHOD_NAME = "arc$delegate";
static final String SET_MOCK_METHOD_NAME = "arc$setMock";
static final String CLEAR_MOCK_METHOD_NAME = "arc$clearMock";
static final String GET_CONTEXTUAL_INSTANCE_METHOD_NAME = "arc_contextualInstance";
static final String GET_BEAN = "arc_bean";
static final String BEAN_FIELD = "bean";
Expand Down Expand Up @@ -105,7 +107,7 @@ Collection<Resource> generate(BeanInfo bean, String beanClassName,
superClass = providerTypeName;
}
if (mockable) {
interfaces.add(MockableProxy.class.getName());
interfaces.add(Mockable.class.getName());
}

ClassCreator clientProxy = ClassCreator.builder().classOutput(classOutput).className(generatedName)
Expand Down Expand Up @@ -216,14 +218,14 @@ Collection<Resource> generate(BeanInfo bean, String beanClassName,

private void implementMockMethods(ClassCreator clientProxy) {
MethodCreator clear = clientProxy
.getMethodCreator(MethodDescriptor.ofMethod(clientProxy.getClassName(), "quarkus$$clearMock", void.class));
.getMethodCreator(MethodDescriptor.ofMethod(clientProxy.getClassName(), CLEAR_MOCK_METHOD_NAME, void.class));
clear.writeInstanceField(FieldDescriptor.of(clientProxy.getClassName(), MOCK_FIELD, Object.class), clear.getThis(),
clear.loadNull());
clear.returnValue(null);

MethodCreator set = clientProxy
.getMethodCreator(
MethodDescriptor.ofMethod(clientProxy.getClassName(), "quarkus$$setMock", void.class, Object.class));
MethodDescriptor.ofMethod(clientProxy.getClassName(), SET_MOCK_METHOD_NAME, void.class, Object.class));
set.writeInstanceField(FieldDescriptor.of(clientProxy.getClassName(), MOCK_FIELD, Object.class), set.getThis(),
set.getMethodParam(0));
set.returnValue(null);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
package io.quarkus.arc.processor;

import static io.quarkus.arc.processor.ClientProxyGenerator.MOCK_FIELD;
import static org.objectweb.asm.Opcodes.ACC_FINAL;
import static org.objectweb.asm.Opcodes.ACC_PRIVATE;
import static org.objectweb.asm.Opcodes.ACC_PUBLIC;
import static org.objectweb.asm.Opcodes.ACC_VOLATILE;

import io.quarkus.arc.InjectableBean;
import io.quarkus.arc.InjectableObserverMethod;
import io.quarkus.arc.impl.CreationalContextImpl;
import io.quarkus.arc.impl.CurrentInjectionPointProvider;
import io.quarkus.arc.impl.Mockable;
import io.quarkus.arc.processor.BeanProcessor.PrivateMembersCollector;
import io.quarkus.arc.processor.BuiltinBean.GeneratorContext;
import io.quarkus.arc.processor.ResourceOutput.Resource;
Expand Down Expand Up @@ -66,11 +69,12 @@ public class ObserverGenerator extends AbstractGenerator {
private final Set<String> existingClasses;
private final Map<ObserverInfo, String> observerToGeneratedName;
private final Predicate<DotName> injectionPointAnnotationsPredicate;
private final boolean mockable;

public ObserverGenerator(AnnotationLiteralProcessor annotationLiterals, Predicate<DotName> applicationClassPredicate,
PrivateMembersCollector privateMembers, boolean generateSources, ReflectionRegistration reflectionRegistration,
Set<String> existingClasses, Map<ObserverInfo, String> observerToGeneratedName,
Predicate<DotName> injectionPointAnnotationsPredicate) {
Predicate<DotName> injectionPointAnnotationsPredicate, boolean mockable) {
super(generateSources);
this.annotationLiterals = annotationLiterals;
this.applicationClassPredicate = applicationClassPredicate;
Expand All @@ -79,6 +83,7 @@ public ObserverGenerator(AnnotationLiteralProcessor annotationLiterals, Predicat
this.existingClasses = existingClasses;
this.observerToGeneratedName = observerToGeneratedName;
this.injectionPointAnnotationsPredicate = injectionPointAnnotationsPredicate;
this.mockable = mockable;
}

/**
Expand Down Expand Up @@ -164,8 +169,14 @@ Collection<Resource> generate(ObserverInfo observer) {
name -> name.equals(generatedName) ? SpecialType.OBSERVER : null, generateSources);

// Foo_Observer_fooMethod_hash implements ObserverMethod<T>
List<Class<?>> interfaces = new ArrayList<>();
interfaces.add(InjectableObserverMethod.class);
if (mockable) {
// Observers declared on mocked beans can be disabled during tests
interfaces.add(Mockable.class);
}
ClassCreator observerCreator = ClassCreator.builder().classOutput(classOutput).className(generatedName)
.interfaces(InjectableObserverMethod.class)
.interfaces(interfaces.toArray((new Class[0])))
.build();

// Fields
Expand All @@ -175,6 +186,9 @@ Collection<Resource> generate(ObserverInfo observer) {
if (!observer.getQualifiers().isEmpty()) {
observedQualifiers = observerCreator.getFieldCreator(QUALIFIERS, Set.class).setModifiers(ACC_PRIVATE | ACC_FINAL);
}
if (mockable) {
observerCreator.getFieldCreator(MOCK_FIELD, boolean.class).setModifiers(ACC_PRIVATE | ACC_VOLATILE);
}

Map<InjectionPointInfo, String> injectionPointToProviderField = new HashMap<>();
initMaps(observer, injectionPointToProviderField);
Expand Down Expand Up @@ -202,6 +216,10 @@ Collection<Resource> generate(ObserverInfo observer) {
}
implementGetDeclaringBeanIdentifier(observerCreator, observer.getDeclaringBean());

if (mockable) {
implementMockMethods(observerCreator);
}

observerCreator.close();
return classOutput.getResources();
}
Expand Down Expand Up @@ -275,6 +293,14 @@ protected void implementNotify(ObserverInfo observer, ClassCreator observerCreat
return;
}

if (mockable) {
// If mockable and mocked then just return from the method
ResultHandle mock = notify.readInstanceField(
FieldDescriptor.of(observerCreator.getClassName(), MOCK_FIELD, boolean.class.getName()),
notify.getThis());
notify.ifTrue(mock).trueBranch().returnValue(null);
}

boolean isStatic = Modifier.isStatic(observer.getObserverMethod().flags());
// It is safe to skip CreationalContext.release() for observers with noor normal scoped declaring provider, and
boolean skipRelease = observer.getInjection().injectionPoints.isEmpty();
Expand Down Expand Up @@ -547,7 +573,33 @@ protected void createConstructor(ClassOutput classOutput, ClassCreator observerC
unmodifiableQualifiersHandle);
}

if (mockable) {
constructor.writeInstanceField(
FieldDescriptor.of(observerCreator.getClassName(), MOCK_FIELD, boolean.class.getName()),
constructor.getThis(),
constructor.load(false));
}

constructor.returnValue(null);
}

private void implementMockMethods(ClassCreator observerCreator) {
MethodCreator clear = observerCreator
.getMethodCreator(MethodDescriptor.ofMethod(observerCreator.getClassName(),
ClientProxyGenerator.CLEAR_MOCK_METHOD_NAME, void.class));
clear.writeInstanceField(FieldDescriptor.of(observerCreator.getClassName(), MOCK_FIELD, boolean.class),
clear.getThis(),
clear.load(false));
clear.returnValue(null);
MethodCreator set = observerCreator
.getMethodCreator(
MethodDescriptor.ofMethod(observerCreator.getClassName(), ClientProxyGenerator.SET_MOCK_METHOD_NAME,
void.class,
Object.class));
set.writeInstanceField(FieldDescriptor.of(observerCreator.getClassName(), MOCK_FIELD, boolean.class),
set.getThis(),
set.load(true));
set.returnValue(null);
}

}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -697,6 +697,23 @@ static ArcContainerImpl unwrap(ArcContainer container) {
}
}

public static void mockObservers(String beanIdentifier, boolean mock) {
instance().mockObserversFor(beanIdentifier, mock);
}

private void mockObserversFor(String beanIdentifier, boolean mock) {
for (InjectableObserverMethod<?> observer : observers) {
if (observer instanceof Mockable && beanIdentifier.equals(observer.getDeclaringBeanIdentifier())) {
Mockable mockable = (Mockable) observer;
if (mock) {
mockable.arc$setMock(null);
} else {
mockable.arc$clearMock();
}
}
}
}

public static ArcContainerImpl instance() {
return unwrap(Arc.container());
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package io.quarkus.arc.impl;

/**
* An interface implemented by mockable components when running in test mode.
* <p>
* This allows normal scoped beans to be easily mocked for tests.
*/
public interface Mockable {

void arc$setMock(Object instance);

void arc$clearMock();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package io.quarkus.it.mockbean;

import java.math.BigDecimal;
import java.util.concurrent.atomic.AtomicReference;

import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.event.Observes;

@ApplicationScoped
public class AlphaObserver {

boolean test() {
return true;
}

void onBigDecimal(@Observes AtomicReference<BigDecimal> event) {
event.set(event.get().add(BigDecimal.ONE));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package io.quarkus.it.mockbean;

import java.math.BigDecimal;
import java.util.concurrent.atomic.AtomicReference;

import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.event.Observes;

@ApplicationScoped
public class BravoObserver {

void onBigDecimal(@Observes AtomicReference<BigDecimal> event) {
event.set(event.get().add(BigDecimal.ONE));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package io.quarkus.it.mockbean;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;

import java.math.BigDecimal;
import java.util.concurrent.atomic.AtomicReference;

import javax.enterprise.event.Event;
import javax.inject.Inject;

import org.junit.jupiter.api.Test;
import org.mockito.Mockito;

import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.junit.mockito.InjectMock;

@QuarkusTest
class MockedObserverTest {

@InjectMock
AlphaObserver observer;

@Inject
Event<AtomicReference<BigDecimal>> event;

@Test
public void testMockedObserverNotNotified() {
Mockito.when(observer.test()).thenReturn(false);
assertFalse(observer.test());
AtomicReference<BigDecimal> payload = new AtomicReference<BigDecimal>(BigDecimal.ZERO);
event.fire(payload);
// BravoObserver is not mocked
assertEquals(BigDecimal.ONE, payload.get());
}

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.quarkus.test.junit;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayDeque;
import java.util.ArrayList;
Expand All @@ -10,17 +11,19 @@ class MockSupport {

private static final Deque<List<Object>> contexts = new ArrayDeque<>();

@SuppressWarnings("unused")
static void pushContext() {
contexts.push(new ArrayList<>());
}

@SuppressWarnings("unused")
static void popContext() {
List<Object> val = contexts.pop();
for (Object i : val) {
try {
i.getClass().getDeclaredMethod("quarkus$$clearMock").invoke(i);
i.getClass().getDeclaredMethod("arc$clearMock").invoke(i);

// Enable all observers declared on the mocked bean
mockObservers(i, false);

} catch (Exception e) {
throw new RuntimeException(e);
}
Expand All @@ -34,14 +37,31 @@ static <T> void installMock(T instance, T mock) {
throw new IllegalStateException("No test in progress");
}
try {
Method setMethod = instance.getClass().getDeclaredMethod("quarkus$$setMock", Object.class);
Method setMethod = instance.getClass().getDeclaredMethod("arc$setMock", Object.class);
setMethod.invoke(instance, mock);
inst.add(instance);

// Disable all observers declared on the mocked bean
mockObservers(instance, true);

} catch (Exception e) {
throw new RuntimeException(instance
+ " is not a normal scoped CDI bean, make sure the bean is a normal scope like @ApplicationScoped or @RequestScoped");

}
}

private static <T> void mockObservers(T instance, boolean mock) throws NoSuchMethodException, SecurityException,
ClassNotFoundException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
// io.quarkus.arc.ClientProxy.arc_bean()
Method getBeanMethod = instance.getClass().getDeclaredMethod("arc_bean");
Object bean = getBeanMethod.invoke(instance);
// io.quarkus.arc.InjectableBean.getIdentifier()
Method getIdMethod = bean.getClass().getDeclaredMethod("getIdentifier");
String id = getIdMethod.invoke(bean).toString();
// io.quarkus.arc.impl.ArcContainerImpl.mockObservers(String, boolean)
Method mockObserversMethod = instance.getClass().getClassLoader().loadClass("io.quarkus.arc.impl.ArcContainerImpl")
.getDeclaredMethod("mockObservers", String.class, boolean.class);
mockObserversMethod.invoke(null, id, mock);
}
}