diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanGenerator.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanGenerator.java index 1f8a6f3ccefcd3..fc2605fe4fe7d7 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanGenerator.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanGenerator.java @@ -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; @@ -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"; diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanProcessor.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanProcessor.java index 1b6a56b256da93..3aab9bbe6963cb 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanProcessor.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanProcessor.java @@ -157,7 +157,7 @@ public List 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 resources = new ArrayList<>(); diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ClientProxyGenerator.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ClientProxyGenerator.java index 08bd3a582b9faa..a2f4a4f1d9d1c8 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ClientProxyGenerator.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ClientProxyGenerator.java @@ -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; @@ -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"; @@ -105,7 +107,7 @@ Collection 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) @@ -216,14 +218,14 @@ Collection 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); diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ObserverGenerator.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ObserverGenerator.java index 29565b93f7a8a3..82786c60f3f1e6 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ObserverGenerator.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ObserverGenerator.java @@ -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; @@ -66,11 +69,12 @@ public class ObserverGenerator extends AbstractGenerator { private final Set existingClasses; private final Map observerToGeneratedName; private final Predicate injectionPointAnnotationsPredicate; + private final boolean mockable; public ObserverGenerator(AnnotationLiteralProcessor annotationLiterals, Predicate applicationClassPredicate, PrivateMembersCollector privateMembers, boolean generateSources, ReflectionRegistration reflectionRegistration, Set existingClasses, Map observerToGeneratedName, - Predicate injectionPointAnnotationsPredicate) { + Predicate injectionPointAnnotationsPredicate, boolean mockable) { super(generateSources); this.annotationLiterals = annotationLiterals; this.applicationClassPredicate = applicationClassPredicate; @@ -79,6 +83,7 @@ public ObserverGenerator(AnnotationLiteralProcessor annotationLiterals, Predicat this.existingClasses = existingClasses; this.observerToGeneratedName = observerToGeneratedName; this.injectionPointAnnotationsPredicate = injectionPointAnnotationsPredicate; + this.mockable = mockable; } /** @@ -164,8 +169,14 @@ Collection generate(ObserverInfo observer) { name -> name.equals(generatedName) ? SpecialType.OBSERVER : null, generateSources); // Foo_Observer_fooMethod_hash implements ObserverMethod + List> 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 @@ -175,6 +186,9 @@ Collection 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 injectionPointToProviderField = new HashMap<>(); initMaps(observer, injectionPointToProviderField); @@ -202,6 +216,10 @@ Collection generate(ObserverInfo observer) { } implementGetDeclaringBeanIdentifier(observerCreator, observer.getDeclaringBean()); + if (mockable) { + implementMockMethods(observerCreator); + } + observerCreator.close(); return classOutput.getResources(); } @@ -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(); @@ -547,7 +573,31 @@ 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(), "quarkus$$clearMock", 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(), "quarkus$$setMock", void.class, + Object.class)); + set.writeInstanceField(FieldDescriptor.of(observerCreator.getClassName(), MOCK_FIELD, boolean.class), + set.getThis(), + set.load(true)); + set.returnValue(null); + } + } diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/MockableProxy.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/MockableProxy.java deleted file mode 100644 index 93e66ffd20941b..00000000000000 --- a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/MockableProxy.java +++ /dev/null @@ -1,13 +0,0 @@ -package io.quarkus.arc; - -/** - * An interface that client proxies will implement when running in test mode. - * - * This allows normal scoped beans to be easily mocked for tests. - */ -public interface MockableProxy { - - void quarkus$$setMock(Object instance); - - void quarkus$$clearMock(); -} diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/ArcContainerImpl.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/ArcContainerImpl.java index b2aa3e8cb50e58..672cd5676b37e8 100644 --- a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/ArcContainerImpl.java +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/ArcContainerImpl.java @@ -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()); } diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/Mockable.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/Mockable.java new file mode 100644 index 00000000000000..013df98be4fcfc --- /dev/null +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/Mockable.java @@ -0,0 +1,13 @@ +package io.quarkus.arc.impl; + +/** + * An interface implemented by mockable components when running in test mode. + *

+ * This allows normal scoped beans to be easily mocked for tests. + */ +public interface Mockable { + + void arc$setMock(Object instance); + + void arc$clearMock(); +} diff --git a/integration-tests/injectmock/src/main/java/io/quarkus/it/mockbean/AlphaObserver.java b/integration-tests/injectmock/src/main/java/io/quarkus/it/mockbean/AlphaObserver.java new file mode 100644 index 00000000000000..d58e6133b87344 --- /dev/null +++ b/integration-tests/injectmock/src/main/java/io/quarkus/it/mockbean/AlphaObserver.java @@ -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 event) { + event.set(event.get().add(BigDecimal.ONE)); + } + +} diff --git a/integration-tests/injectmock/src/main/java/io/quarkus/it/mockbean/BravoObserver.java b/integration-tests/injectmock/src/main/java/io/quarkus/it/mockbean/BravoObserver.java new file mode 100644 index 00000000000000..4e7bbb9b1d1e3e --- /dev/null +++ b/integration-tests/injectmock/src/main/java/io/quarkus/it/mockbean/BravoObserver.java @@ -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 event) { + event.set(event.get().add(BigDecimal.ONE)); + } + +} diff --git a/integration-tests/injectmock/src/test/java/io/quarkus/it/mockbean/MockedObserverTest.java b/integration-tests/injectmock/src/test/java/io/quarkus/it/mockbean/MockedObserverTest.java new file mode 100644 index 00000000000000..7be20eb62196aa --- /dev/null +++ b/integration-tests/injectmock/src/test/java/io/quarkus/it/mockbean/MockedObserverTest.java @@ -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> event; + + @Test + public void testMockedObserverNotNotified() { + Mockito.when(observer.test()).thenReturn(false); + assertFalse(observer.test()); + AtomicReference payload = new AtomicReference(BigDecimal.ZERO); + event.fire(payload); + // BravoObserver is not mocked + assertEquals(BigDecimal.ONE, payload.get()); + } + +} diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/MockSupport.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/MockSupport.java index 31e4b617084fe9..2bbf9b6c1de557 100644 --- a/test-framework/junit5/src/main/java/io/quarkus/test/junit/MockSupport.java +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/MockSupport.java @@ -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; @@ -10,17 +11,19 @@ class MockSupport { private static final Deque> contexts = new ArrayDeque<>(); - @SuppressWarnings("unused") static void pushContext() { contexts.push(new ArrayList<>()); } - @SuppressWarnings("unused") static void popContext() { List 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); } @@ -34,14 +37,31 @@ static 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 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); + } }