diff --git a/docs/src/main/asciidoc/getting-started-testing.adoc b/docs/src/main/asciidoc/getting-started-testing.adoc index 5332aaa6093ba..d9637db67618e 100644 --- a/docs/src/main/asciidoc/getting-started-testing.adoc +++ b/docs/src/main/asciidoc/getting-started-testing.adoc @@ -1573,8 +1573,8 @@ public class FooTest { } ---- <1> The `QuarkusComponentTest` annotation registers the JUnit extension. -<2> Set a configuration property for the test. -<3> The test injects the component under the test. The types of all fields annotated with `@Inject` are considered the component types under test. You can also specify additional component classes via `@QuarkusComponentTest#value()`. +<2> Sets a configuration property for the test. +<3> The test injects the component under the test. The types of all fields annotated with `@Inject` are considered the component types under test. You can also specify additional component classes via `@QuarkusComponentTest#value()`. Furthermore, the static nested classes declared on the test class are components too. <4> The test also injects `Charlie`, a dependency for which a synthetic `@Singleton` bean is registered automatically. The injected reference is an "unconfigured" Mockito mock. <5> We can leverage the Mockito API in a test method to configure the behavior. @@ -1609,7 +1609,7 @@ public class FooTest { } } ---- -<1> The `QuarkusComponentTestExtension` is configured in a static field. +<1> The `QuarkusComponentTestExtension` is configured in a static field of the test class. === Lifecycle @@ -1638,4 +1638,98 @@ You can use the mock configurator API via the `QuarkusComponentTestExtension#moc A dedicated `SmallRyeConfig` is registered during the `before all` test phase. Moreover, it's possible to set the configuration properties via the `QuarkusComponentTestExtension#configProperty(String, String)` method or the `@TestConfigProperty` annotation. -If you only need to use the default values for missing config properties, then the `QuarkusComponentTestExtension#useDefaultConfigProperties()` or `@QuarkusComponentTest#useDefaultConfigProperties()` might come in useful. \ No newline at end of file +If you only need to use the default values for missing config properties, then the `QuarkusComponentTestExtension#useDefaultConfigProperties()` or `@QuarkusComponentTest#useDefaultConfigProperties()` might come in useful. + +=== Mocking CDI Interceptors + +If a tested component class declares an interceptor binding then you might need to mock the interception too. +There are two ways to accomplish this task. +First, you can define an interceptor class as a static nested class of the test class. + +[source, java] +---- +import static org.junit.jupiter.api.Assertions.assertEquals; + +import jakarta.inject.Inject; +import io.quarkus.test.component.QuarkusComponentTest; +import org.junit.jupiter.api.Test; + +@QuarkusComponentTest +public class FooTest { + + @Inject + Foo foo; + + @Test + public void testPing() { + assertEquals("OK", foo.ping()); + } + + @ApplicationScoped + static class Foo { + + @SimpleBinding <1> + String ping() { + return "ok"; + } + + } + + @SimpleBinding + @Interceptor + static class SimpleInterceptor { <2> + + @AroundInvoke + Object aroundInvoke(InvocationContext context) throws Exception { + return context.proceed().toString().toUpperCase(); + } + + } +} +---- +<1> `@SimpleBinding` is an interceptor binding. +<2> The interceptor class is automatically considered a tested component. + +NOTE: Static nested classed declared on a test class that is annotated with `@QuarkusComponentTest` are excluded from bean discovery when running a `@QuarkusTest` in order to prevent unintentional CDI conflicts. + +Furthermore, you can also declare a "test interceptor method" directly on the test class. +This method is then invoked in the relevant interception phase. + +[source, java] +---- +import static org.junit.jupiter.api.Assertions.assertEquals; + +import jakarta.inject.Inject; +import io.quarkus.test.component.QuarkusComponentTest; +import org.junit.jupiter.api.Test; + +@QuarkusComponentTest +public class FooTest { + + @Inject + Foo foo; + + @Test + public void testPing() { + assertEquals("OK", foo.ping()); + } + + @SimpleBinding <1> + @AroundInvoke <2> + Object aroundInvoke(InvocationContext context) throws Exception { + return context.proceed().toString().toUpperCase(); + } + + @ApplicationScoped + static class Foo { + + @SimpleBinding <1> + String ping() { + return "ok"; + } + + } +} +---- +<1> The interceptor bindings of the resulting interceptor are specified by annotating the method with the interceptor binding types. +<2> Defines the interception type. diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/InjectionPointInfo.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/InjectionPointInfo.java index 7bc44dbce45c8..f98d4bda71cd4 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/InjectionPointInfo.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/InjectionPointInfo.java @@ -409,7 +409,8 @@ public boolean equals(Object obj) { if (other.qualifiers != null) { return false; } - } else if (!qualifiers.equals(other.qualifiers)) { + } else if (!qualifiersAreEqual(qualifiers, other.qualifiers)) { + // We cannot use AnnotationInstance#equals() as it requires the exact same annotationTarget instance return false; } if (type == null) { @@ -422,6 +423,30 @@ public boolean equals(Object obj) { return true; } + private boolean qualifiersAreEqual(Set q1, Set q2) { + if (q1 == q2) { + return true; + } + if (q1.size() != q2.size()) { + return false; + } + for (AnnotationInstance a1 : q1) { + for (AnnotationInstance a2 : q2) { + if (!annotationsAreEqual(a1, a2)) { + return false; + } + } + } + return true; + } + + private boolean annotationsAreEqual(AnnotationInstance a1, AnnotationInstance a2) { + if (a1 == a2) { + return true; + } + return a1.name().equals(a2.name()) && a1.values().equals(a2.values()); + } + } } diff --git a/test-framework/junit5-component/src/main/java/io/quarkus/test/component/InterceptorMethodCreator.java b/test-framework/junit5-component/src/main/java/io/quarkus/test/component/InterceptorMethodCreator.java new file mode 100644 index 0000000000000..b82442f1a6e56 --- /dev/null +++ b/test-framework/junit5-component/src/main/java/io/quarkus/test/component/InterceptorMethodCreator.java @@ -0,0 +1,36 @@ +package io.quarkus.test.component; + +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + +import io.quarkus.arc.InterceptorCreator; +import io.quarkus.arc.SyntheticCreationalContext; + +public class InterceptorMethodCreator implements InterceptorCreator { + + static final String CREATE_KEY = "createKey"; + + private static final Map, InterceptFunction>> createFunctions = new HashMap<>(); + + @Override + public InterceptFunction create(SyntheticCreationalContext context) { + Object createKey = context.getParams().get(CREATE_KEY); + if (createKey != null) { + Function, InterceptFunction> createFun = createFunctions.get(createKey); + if (createFun != null) { + return createFun.apply(context); + } + } + throw new IllegalStateException("Create function not found: " + createKey); + } + + static void registerCreate(String key, Function, InterceptFunction> create) { + createFunctions.put(key, create); + } + + static void clear() { + createFunctions.clear(); + } + +} diff --git a/test-framework/junit5-component/src/main/java/io/quarkus/test/component/MockBeanCreator.java b/test-framework/junit5-component/src/main/java/io/quarkus/test/component/MockBeanCreator.java index cce7bfaa6e07b..4b888a3d74945 100644 --- a/test-framework/junit5-component/src/main/java/io/quarkus/test/component/MockBeanCreator.java +++ b/test-framework/junit5-component/src/main/java/io/quarkus/test/component/MockBeanCreator.java @@ -12,15 +12,17 @@ public class MockBeanCreator implements BeanCreator { + static final String CREATE_KEY = "createKey"; + private static final Logger LOG = Logger.getLogger(MockBeanCreator.class); private static final Map, ?>> createFunctions = new HashMap<>(); @Override public Object create(SyntheticCreationalContext context) { - Object createKey = context.getParams().get("createKey"); + Object createKey = context.getParams().get(CREATE_KEY); if (createKey != null) { - Function, ?> createFun = createFunctions.get(createKey.toString()); + Function, ?> createFun = createFunctions.get(createKey); if (createFun != null) { return createFun.apply(context); } else { diff --git a/test-framework/junit5-component/src/main/java/io/quarkus/test/component/QuarkusComponentTestExtension.java b/test-framework/junit5-component/src/main/java/io/quarkus/test/component/QuarkusComponentTestExtension.java index 902f8ab857dd8..c4e1c642ac853 100644 --- a/test-framework/junit5-component/src/main/java/io/quarkus/test/component/QuarkusComponentTestExtension.java +++ b/test-framework/junit5-component/src/main/java/io/quarkus/test/component/QuarkusComponentTestExtension.java @@ -32,13 +32,20 @@ import java.util.function.Function; import java.util.stream.Collectors; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import jakarta.annotation.Priority; import jakarta.enterprise.context.Dependent; import jakarta.enterprise.event.Event; import jakarta.enterprise.inject.Instance; import jakarta.enterprise.inject.spi.BeanManager; import jakarta.enterprise.inject.spi.InjectionPoint; +import jakarta.enterprise.inject.spi.InterceptionType; import jakarta.inject.Inject; import jakarta.inject.Singleton; +import jakarta.interceptor.AroundConstruct; +import jakarta.interceptor.AroundInvoke; +import jakarta.interceptor.InvocationContext; import org.eclipse.microprofile.config.Config; import org.eclipse.microprofile.config.inject.ConfigProperty; @@ -145,6 +152,7 @@ public class QuarkusComponentTestExtension private static final String KEY_OLD_CONFIG_PROVIDER_RESOLVER = "oldConfigProviderResolver"; private static final String KEY_GENERATED_RESOURCES = "generatedResources"; private static final String KEY_INJECTED_FIELDS = "injectedFields"; + private static final String KEY_TEST_INSTANCE = "testInstance"; private static final String KEY_CONFIG = "config"; private static final String TARGET_TEST_CLASSES = "target/test-classes"; @@ -232,6 +240,7 @@ public void postProcessTestInstance(Object testInstance, ExtensionContext contex // Inject test class fields context.getRoot().getStore(NAMESPACE).put(KEY_INJECTED_FIELDS, injectFields(context.getRequiredTestClass(), testInstance)); + context.getRoot().getStore(NAMESPACE).put(KEY_TEST_INSTANCE, testInstance); LOG.debugf("postProcessTestInstance: %s ms", TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start)); } @@ -328,6 +337,7 @@ public void afterAll(ExtensionContext context) throws Exception { } MockBeanCreator.clear(); ConfigBeanCreator.clear(); + InterceptorMethodCreator.clear(); SmallRyeConfig config = context.getRoot().getStore(NAMESPACE).get(KEY_CONFIG, SmallRyeConfig.class); ConfigProviderResolver.instance().releaseConfig(config); @@ -417,7 +427,7 @@ public void register(RegistrationContext context) { } String key = UUID.randomUUID().toString(); MockBeanCreator.registerCreate(key, cast(mock.create)); - configurator.creator(MockBeanCreator.class).param("createKey", key).done(); + configurator.creator(MockBeanCreator.class).param(MockBeanCreator.CREATE_KEY, key).done(); } }; } @@ -444,15 +454,10 @@ private static Set getQualifiers(Field field, Collection> componentClasses) { - Class testClass = context.getRequiredTestClass(); + private ClassLoader initArcContainer(ExtensionContext extensionContext, Collection> componentClasses) { + Class testClass = extensionContext.getRequiredTestClass(); // Collect all test class injection points to define a bean removal exclusion - List testClassInjectionPoints = new ArrayList<>(); - for (Field field : testClass.getDeclaredFields()) { - if (field.isAnnotationPresent(Inject.class)) { - testClassInjectionPoints.add(field); - } - } + List testClassInjectionPoints = findInjectFields(testClass); if (componentClasses.isEmpty()) { throw new IllegalStateException("No component classes to test"); @@ -488,6 +493,7 @@ private ClassLoader initArcContainer(ExtensionContext context, Collection qualifiers = new ArrayList<>(); + Set interceptorBindings = new HashSet<>(); AtomicReference beanResolver = new AtomicReference<>(); BeanProcessor.Builder builder = BeanProcessor.builder() @@ -549,7 +555,6 @@ public void writeResource(Resource resource) throws IOException { String testPath = testClass.getClassLoader().getResource(testClass.getName().replace(".", "/") + ".class") .getFile(); int targetClassesIndex = testPath.indexOf(TARGET_TEST_CLASSES); - // NOTE: continuous testing is not supported at the moment if (targetClassesIndex == -1) { throw new IllegalStateException("Invalid test path: " + testPath); } @@ -580,7 +585,7 @@ public void writeResource(Resource resource) throws IOException { }); } - context.getRoot().getStore(NAMESPACE).put(KEY_GENERATED_RESOURCES, generatedResources); + extensionContext.getRoot().getStore(NAMESPACE).put(KEY_GENERATED_RESOURCES, generatedResources); builder.addAnnotationTransformer(AnnotationsTransformer.appliedToField().whenContainsAny(qualifiers) .whenContainsNone(DotName.createSimple(Inject.class)).thenTransform(t -> t.add(Inject.class))); @@ -591,10 +596,10 @@ public void writeResource(Resource resource) throws IOException { builder.addBeanRegistrar(new BeanRegistrar() { @Override - public void register(RegistrationContext context) { + public void register(RegistrationContext registrationContext) { long start = System.nanoTime(); - List beans = context.beans().collect(); - BeanDeployment beanDeployment = context.get(Key.DEPLOYMENT); + List beans = registrationContext.beans().collect(); + BeanDeployment beanDeployment = registrationContext.get(Key.DEPLOYMENT); Set unsatisfiedInjectionPoints = new HashSet<>(); boolean configInjectionPoint = false; Set configPropertyInjectionPoints = new HashSet<>(); @@ -604,7 +609,7 @@ public void register(RegistrationContext context) { // Analyze injection points // - find Config and @ConfigProperty injection points // - find unsatisfied injection points - for (InjectionPointInfo injectionPoint : context.getInjectionPoints()) { + for (InjectionPointInfo injectionPoint : registrationContext.getInjectionPoints()) { BuiltinBean builtin = BuiltinBean.resolve(injectionPoint); if (builtin != null && builtin != BuiltinBean.INSTANCE && builtin != BuiltinBean.LIST) { continue; @@ -620,27 +625,41 @@ public void register(RegistrationContext context) { continue; } Type requiredType = injectionPoint.getRequiredType(); - Set qualifiers = injectionPoint.getRequiredQualifiers(); + Set requiredQualifiers = injectionPoint.getRequiredQualifiers(); if (builtin == BuiltinBean.LIST) { // @All List -> Delta requiredType = requiredType.asParameterizedType().arguments().get(0); - qualifiers = new HashSet<>(qualifiers); - qualifiers.removeIf(q -> q.name().equals(DotNames.ALL)); + requiredQualifiers = new HashSet<>(requiredQualifiers); + requiredQualifiers.removeIf(q -> q.name().equals(DotNames.ALL)); + if (requiredQualifiers.isEmpty()) { + requiredQualifiers.add(AnnotationInstance.builder(DotNames.DEFAULT).build()); + } } - if (isSatisfied(requiredType, qualifiers, injectionPoint, beans, beanDeployment)) { + if (isSatisfied(requiredType, requiredQualifiers, injectionPoint, beans, beanDeployment)) { continue; } if (requiredType.kind() == Kind.PRIMITIVE || requiredType.kind() == Kind.ARRAY) { throw new IllegalStateException( "Found an unmockable unsatisfied injection point: " + injectionPoint.getTargetInfo()); } - unsatisfiedInjectionPoints.add(new TypeAndQualifiers(requiredType, qualifiers)); + unsatisfiedInjectionPoints.add(new TypeAndQualifiers(requiredType, requiredQualifiers)); LOG.debugf("Unsatisfied injection point found: %s", injectionPoint.getTargetInfo()); } + // Make sure that all @InjectMock fields are also considered unsatisfied dependencies + // This means that a mock is created even if no component declares this dependency + for (Field field : findFields(testClass, List.of(InjectMock.class))) { + Set requiredQualifiers = getQualifiers(field, qualifiers); + if (requiredQualifiers.isEmpty()) { + requiredQualifiers = Set.of(AnnotationInstance.builder(DotNames.DEFAULT).build()); + } + unsatisfiedInjectionPoints + .add(new TypeAndQualifiers(Types.jandexType(field.getGenericType()), requiredQualifiers)); + } + for (TypeAndQualifiers unsatisfied : unsatisfiedInjectionPoints) { ClassInfo implementationClass = computingIndex.getClassByName(unsatisfied.type.name()); - BeanConfigurator configurator = context.configure(implementationClass.name()) + BeanConfigurator configurator = registrationContext.configure(implementationClass.name()) .scope(Singleton.class) .addType(unsatisfied.type); unsatisfied.qualifiers.forEach(configurator::addQualifier); @@ -652,14 +671,14 @@ public void register(RegistrationContext context) { } if (configInjectionPoint) { - context.configure(Config.class) + registrationContext.configure(Config.class) .addType(Config.class) .creator(ConfigBeanCreator.class) .done(); } if (!configPropertyInjectionPoints.isEmpty()) { - BeanConfigurator configPropertyConfigurator = context.configure(Object.class) + BeanConfigurator configPropertyConfigurator = registrationContext.configure(Object.class) .identifier("configProperty") .addQualifier(ConfigProperty.class) .param("useDefaultConfigProperties", useDefaultConfigProperties.get()) @@ -672,8 +691,12 @@ public void register(RegistrationContext context) { } LOG.debugf("Test injection points analyzed in %s ms [found: %s, mocked: %s]", - TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start), context.getInjectionPoints().size(), + TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start), + registrationContext.getInjectionPoints().size(), unsatisfiedInjectionPoints.size()); + + // Find all methods annotated with interceptor annotations and register them as synthetic interceptors + processTestInterceptorMethods(testClass, extensionContext, registrationContext, interceptorBindings); } }); @@ -694,6 +717,9 @@ public void accept(BytecodeTransformer transformer) { // Populate the list of qualifiers used to simulate quarkus auto injection ContextRegistrar.RegistrationContext registrationContext = beanProcessor.registerCustomContexts(); qualifiers.addAll(registrationContext.get(Key.QUALIFIERS).keySet()); + for (DotName binding : registrationContext.get(Key.INTERCEPTOR_BINDINGS).keySet()) { + interceptorBindings.add(binding.toString()); + } beanResolver.set(registrationContext.get(Key.DEPLOYMENT).getBeanResolver()); beanProcessor.registerScopes(); beanProcessor.registerBeans(); @@ -729,6 +755,76 @@ public void accept(BytecodeTransformer transformer) { return oldTccl; } + private void processTestInterceptorMethods(Class testClass, ExtensionContext extensionContext, + BeanRegistrar.RegistrationContext registrationContext, Set interceptorBindings) { + for (Method method : findMethods(testClass, + List.of(AroundInvoke.class, PostConstruct.class, PreDestroy.class, AroundConstruct.class))) { + Set bindings = findBindings(method, interceptorBindings); + if (bindings.isEmpty()) { + throw new IllegalStateException("No bindings declared on a test interceptor method: " + method); + } + validateTestInterceptorMethod(method); + String key = UUID.randomUUID().toString(); + InterceptorMethodCreator.registerCreate(key, ctx -> { + return ic -> { + Object instance = null; + if (!Modifier.isStatic(method.getModifiers())) { + // ExtentionContext.getTestInstance() does not work + Object testInstance = extensionContext.getRoot().getStore(NAMESPACE).get(KEY_TEST_INSTANCE, + Object.class); + if (testInstance == null) { + throw new IllegalStateException("Test instance not available"); + } + instance = testInstance; + if (!method.canAccess(instance)) { + method.setAccessible(true); + } + } + return method.invoke(instance, ic); + }; + }); + InterceptionType interceptionType; + if (method.isAnnotationPresent(AroundInvoke.class)) { + interceptionType = InterceptionType.AROUND_INVOKE; + } else if (method.isAnnotationPresent(PostConstruct.class)) { + interceptionType = InterceptionType.POST_CONSTRUCT; + } else if (method.isAnnotationPresent(PreDestroy.class)) { + interceptionType = InterceptionType.PRE_DESTROY; + } else if (method.isAnnotationPresent(AroundConstruct.class)) { + interceptionType = InterceptionType.AROUND_CONSTRUCT; + } else { + // This should never happen + throw new IllegalStateException("No interceptor annotation declared on: " + method); + } + int priority = 1; + Priority priorityAnnotation = method.getAnnotation(Priority.class); + if (priorityAnnotation != null) { + priority = priorityAnnotation.value(); + } + registrationContext.configureInterceptor(interceptionType) + .identifier(key) + .priority(priority) + .bindings(bindings.stream().map(Annotations::jandexAnnotation) + .toArray(AnnotationInstance[]::new)) + .param(InterceptorMethodCreator.CREATE_KEY, key) + .creator(InterceptorMethodCreator.class); + } + } + + private void validateTestInterceptorMethod(Method method) { + Parameter[] params = method.getParameters(); + if (params.length != 1 || !InvocationContext.class.isAssignableFrom(params[0].getType())) { + throw new IllegalStateException("A test interceptor method must declare exactly one InvocationContext parameter:" + + Arrays.toString(params)); + } + + } + + private Set findBindings(Method method, Set bindings) { + return Arrays.stream(method.getAnnotations()).filter(a -> bindings.contains(a.annotationType().getName())) + .collect(Collectors.toSet()); + } + private void indexComponentClass(Indexer indexer, Class componentClass) { try { while (componentClass != null) { @@ -791,6 +887,14 @@ static T cast(Object obj) { } private List injectFields(Class testClass, Object testInstance) throws Exception { + List injectedFields = new ArrayList<>(); + for (Field field : findInjectFields(testClass)) { + injectedFields.add(new FieldInjector(field, testInstance)); + } + return injectedFields; + } + + private List findInjectFields(Class testClass) { List> injectAnnotations; Class deprecatedInjectMock = loadDeprecatedInjectMock(); if (deprecatedInjectMock != null) { @@ -798,20 +902,41 @@ private List injectFields(Class testClass, Object testInstance } else { injectAnnotations = List.of(Inject.class, InjectMock.class); } - List injectedFields = new ArrayList<>(); + return findFields(testClass, injectAnnotations); + } + + private List findFields(Class testClass, List> annotations) { + List fields = new ArrayList<>(); Class current = testClass; while (current.getSuperclass() != null) { for (Field field : current.getDeclaredFields()) { - for (Class annotation : injectAnnotations) { + for (Class annotation : annotations) { if (field.isAnnotationPresent(annotation)) { - injectedFields.add(new FieldInjector(field, testInstance)); + fields.add(field); break; } } } current = current.getSuperclass(); } - return injectedFields; + return fields; + } + + private List findMethods(Class testClass, List> annotations) { + List methods = new ArrayList<>(); + Class current = testClass; + while (current.getSuperclass() != null) { + for (Method method : current.getDeclaredMethods()) { + for (Class annotation : annotations) { + if (method.isAnnotationPresent(annotation)) { + methods.add(method); + break; + } + } + } + current = current.getSuperclass(); + } + return methods; } static class FieldInjector { @@ -851,7 +976,10 @@ public FieldInjector(Field field, Object testInstance) throws Exception { handle.getBean())); } } else { - if (handle.getBean().getKind() != io.quarkus.arc.InjectableBean.Kind.SYNTHETIC) { + if (!handle.isAvailable()) { + throw new IllegalStateException(String + .format("The injected field %s expects a mocked bean; but obtained null", field)); + } else if (handle.getBean().getKind() != io.quarkus.arc.InjectableBean.Kind.SYNTHETIC) { throw new IllegalStateException(String .format("The injected field %s expects a mocked bean; but obtained: %s", field, handle.getBean())); @@ -861,7 +989,10 @@ public FieldInjector(Field field, Object testInstance) throws Exception { unsetHandles = List.of(handle); } - field.setAccessible(true); + if (!field.canAccess(testInstance)) { + field.setAccessible(true); + } + field.set(testInstance, injectedInstance); } @@ -875,7 +1006,6 @@ void unset(Object testInstance) throws Exception { } } } - field.setAccessible(true); field.set(testInstance, null); } diff --git a/test-framework/junit5-component/src/test/java/io/quarkus/test/component/declarative/InterceptorMethodsTest.java b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/declarative/InterceptorMethodsTest.java new file mode 100644 index 0000000000000..c74fc86e3cba6 --- /dev/null +++ b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/declarative/InterceptorMethodsTest.java @@ -0,0 +1,122 @@ +package io.quarkus.test.component.declarative; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; +import jakarta.annotation.Priority; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.inject.spi.Bean; +import jakarta.inject.Inject; +import jakarta.interceptor.AroundConstruct; +import jakarta.interceptor.AroundInvoke; +import jakarta.interceptor.InterceptorBinding; +import jakarta.interceptor.InvocationContext; + +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import io.quarkus.arc.Arc; +import io.quarkus.arc.ArcInvocationContext; +import io.quarkus.arc.NoClassInterceptors; +import io.quarkus.test.InjectMock; +import io.quarkus.test.component.QuarkusComponentTest; +import io.quarkus.test.component.beans.Charlie; + +@QuarkusComponentTest +public class InterceptorMethodsTest { + + static final List EVENTS = new CopyOnWriteArrayList<>(); + + @Inject + TheComponent theComponent; + + // Charlie is mocked even if it's not a dependency of a tested component + @InjectMock + Charlie charlie; + + @Test + public void testPing() { + EVENTS.clear(); + Mockito.when(charlie.ping()).thenReturn("ok"); + assertEquals("OK", theComponent.ping()); + Arc.container().getActiveContext(ApplicationScoped.class).destroy(theComponent.getBean()); + assertEquals(5, EVENTS.size()); + assertEquals("ac", EVENTS.get(0)); + assertEquals("pc", EVENTS.get(1)); + assertEquals("ai2", EVENTS.get(2)); + assertEquals("ai1", EVENTS.get(3)); + assertEquals("pd", EVENTS.get(4)); + } + + @SimpleBinding + @ApplicationScoped + static class TheComponent { + + @Inject + Bean bean; + + String ping() { + return "true"; + } + + @NoClassInterceptors + public Bean getBean() { + return bean; + } + + } + + @Target({ TYPE, METHOD }) + @Retention(RUNTIME) + @InterceptorBinding + public @interface SimpleBinding { + + } + + @Priority(20) + @SimpleBinding + @AroundInvoke + Object aroundInvoke1(InvocationContext context) throws Exception { + EVENTS.add("ai1"); + return Boolean.parseBoolean(context.proceed().toString()) ? charlie.ping() : "false"; + } + + // default priority is 1 + @SimpleBinding + @AroundInvoke + Object aroundInvoke2(InvocationContext context) throws Exception { + EVENTS.add("ai2"); + return context.proceed().toString().toUpperCase(); + } + + @SimpleBinding + @PostConstruct + void postConstruct(ArcInvocationContext context) throws Exception { + EVENTS.add("pc"); + context.proceed(); + } + + @SimpleBinding + @PreDestroy + void preDestroy(ArcInvocationContext context) throws Exception { + EVENTS.add("pd"); + context.proceed(); + } + + @SimpleBinding + @AroundConstruct + void aroundConstruct(ArcInvocationContext context) throws Exception { + EVENTS.add("ac"); + context.proceed(); + } + +}