diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ArcProcessor.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ArcProcessor.java index b1aed7450f0ac..d02186bd837d6 100644 --- a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ArcProcessor.java +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ArcProcessor.java @@ -3,6 +3,7 @@ import static io.quarkus.deployment.annotations.ExecutionTime.RUNTIME_INIT; import static io.quarkus.deployment.annotations.ExecutionTime.STATIC_INIT; +import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -24,6 +25,7 @@ import org.jboss.jandex.AnnotationTarget; import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.ClassInfo.NestingType; import org.jboss.jandex.DotName; import org.jboss.jandex.FieldInfo; import org.jboss.jandex.IndexView; @@ -383,6 +385,9 @@ public Integer compute(AnnotationTarget target, Collection stere builder.addExcludeType(predicate); } } + if (launchModeBuildItem.getLaunchMode() == LaunchMode.TEST) { + builder.addExcludeType(createQuarkusComponentTestExcludePredicate(index)); + } for (SuppressConditionGeneratorBuildItem generator : suppressConditionGenerators) { builder.addSuppressConditionGenerator(generator.getGenerator()); @@ -731,6 +736,39 @@ void registerContextPropagation(ArcConfig config, BuildProducer createQuarkusComponentTestExcludePredicate(IndexView index) { + // Exlude static nested classed declared on a QuarkusComponentTest: + // 1. Test class annotated with @QuarkusComponentTest + // 2. Test class with a static field of a type QuarkusComponentTestExtension + DotName quarkusComponentTest = DotName.createSimple("io.quarkus.test.component.QuarkusComponentTest"); + DotName quarkusComponentTestExtension = DotName.createSimple("io.quarkus.test.component.QuarkusComponentTestExtension"); + return new Predicate() { + + @Override + public boolean test(ClassInfo clazz) { + if (clazz.nestingType() == NestingType.INNER + && Modifier.isStatic(clazz.flags())) { + DotName enclosingClassName = clazz.enclosingClass(); + ClassInfo enclosingClass = index.getClassByName(enclosingClassName); + if (enclosingClass != null) { + if (enclosingClass.hasDeclaredAnnotation(quarkusComponentTest)) { + return true; + } else { + for (FieldInfo field : enclosingClass.fields()) { + if (!field.isSynthetic() + && Modifier.isStatic(field.flags()) + && field.type().name().equals(quarkusComponentTestExtension)) { + return true; + } + } + } + } + } + return false; + } + }; + } + private abstract static class AbstractCompositeApplicationClassesPredicate implements Predicate { private final IndexView applicationClassesIndex; diff --git a/integration-tests/main/pom.xml b/integration-tests/main/pom.xml index 8d1bfc30b8790..0086d9f353c81 100644 --- a/integration-tests/main/pom.xml +++ b/integration-tests/main/pom.xml @@ -154,6 +154,11 @@ quarkus-junit5 test + + io.quarkus + quarkus-junit5-component + test + io.quarkus quarkus-test-h2 diff --git a/integration-tests/main/src/test/java/io/quarkus/it/main/MyComponentTest.java b/integration-tests/main/src/test/java/io/quarkus/it/main/MyComponentTest.java new file mode 100644 index 0000000000000..b81abcb52e8cc --- /dev/null +++ b/integration-tests/main/src/test/java/io/quarkus/it/main/MyComponentTest.java @@ -0,0 +1,39 @@ +package io.quarkus.it.main; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import jakarta.enterprise.inject.Produces; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; + +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.junit.jupiter.api.Test; + +import io.quarkus.test.component.QuarkusComponentTest; + +@QuarkusComponentTest +public class MyComponentTest { + + @Inject + @ConfigProperty // name and default value are nonbinding + String myProperty; + + @Test + public void testProperty() { + assertEquals("foo", myProperty); + } + + @Singleton + static class PropertyProducer { + + // This producer would normally break all @QuarkusTest in the test suite + // However, since it's a static nested class declared on a @QuarkusComponentTest it's excluded from the bean discovery + @Produces + @ConfigProperty + String myString() { + return "foo"; + } + + } + +} diff --git a/test-framework/junit5-component/src/main/java/io/quarkus/test/component/QuarkusComponentTest.java b/test-framework/junit5-component/src/main/java/io/quarkus/test/component/QuarkusComponentTest.java index 1aba39aa6beb5..36a4c4941eeff 100644 --- a/test-framework/junit5-component/src/main/java/io/quarkus/test/component/QuarkusComponentTest.java +++ b/test-framework/junit5-component/src/main/java/io/quarkus/test/component/QuarkusComponentTest.java @@ -43,4 +43,11 @@ * @see QuarkusComponentTestExtension#useDefaultConfigProperties() */ boolean useDefaultConfigProperties() default false; + + /** + * If set to {@code true} then all static nested classes are considered additional components under test. + * + * @see #value() + */ + boolean addNestedClassesAsComponents() default true; } 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 865fb1b31a398..902f8ab857dd8 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 @@ -7,6 +7,7 @@ import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Field; import java.lang.reflect.Method; +import java.lang.reflect.Modifier; import java.lang.reflect.Parameter; import java.lang.reflect.ParameterizedType; import java.nio.file.Files; @@ -65,6 +66,7 @@ import io.quarkus.arc.ArcContainer; import io.quarkus.arc.ComponentsProvider; import io.quarkus.arc.InstanceHandle; +import io.quarkus.arc.Unremovable; import io.quarkus.arc.processor.Annotations; import io.quarkus.arc.processor.AnnotationsTransformer; import io.quarkus.arc.processor.BeanArchives; @@ -151,6 +153,7 @@ public class QuarkusComponentTestExtension private final List> additionalComponentClasses; private final List> mockConfigurators; private final AtomicBoolean useDefaultConfigProperties = new AtomicBoolean(); + private final AtomicBoolean addNestedClassesAsComponents = new AtomicBoolean(true); // Used for declarative registration public QuarkusComponentTestExtension() { @@ -209,6 +212,19 @@ public QuarkusComponentTestExtension useDefaultConfigProperties() { return this; } + /** + * Ignore the static nested classes declared on the test class. + *

+ * By default, all static nested classes declared on the test class are added to the set of additional components under + * test. + * + * @return the extension + */ + public QuarkusComponentTestExtension ignoreNestedClasses() { + this.addNestedClassesAsComponents.set(false); + return this; + } + @Override public void postProcessTestInstance(Object testInstance, ExtensionContext context) throws Exception { long start = System.nanoTime(); @@ -247,6 +263,7 @@ public void beforeAll(ExtensionContext context) throws Exception { if (testAnnotation.useDefaultConfigProperties()) { this.useDefaultConfigProperties.set(true); } + this.addNestedClassesAsComponents.set(testAnnotation.addNestedClassesAsComponents()); } // All fields annotated with @Inject represent component classes Class current = testClass; @@ -258,6 +275,14 @@ public void beforeAll(ExtensionContext context) throws Exception { } current = current.getSuperclass(); } + // All static nested classes declared on the test class are components + if (this.addNestedClassesAsComponents.get()) { + for (Class declaredClass : testClass.getDeclaredClasses()) { + if (Modifier.isStatic(declaredClass.getModifiers())) { + componentClasses.add(declaredClass); + } + } + } TestConfigProperty[] testConfigProperties = testClass.getAnnotationsByType(TestConfigProperty.class); for (TestConfigProperty testConfigProperty : testConfigProperties) { @@ -468,7 +493,13 @@ private ClassLoader initArcContainer(ExtensionContext context, Collection { - // Do not remove beans injected in the test class + // Do not remove beans: + // 1. Injected in the test class + // 2. Annotated with @Unremovable + if (b.getTarget().isPresent() + && b.getTarget().get().hasDeclaredAnnotation(Unremovable.class)) { + return true; + } for (Field injectionPoint : testClassInjectionPoints) { if (beanResolver.get().matches(b, Types.jandexType(injectionPoint.getGenericType()), getQualifiers(injectionPoint, qualifiers))) { diff --git a/test-framework/junit5-component/src/test/java/io/quarkus/test/component/MockNotSharedForClassHierarchyTest.java b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/MockNotSharedForClassHierarchyTest.java index 982250daaa7ed..0e3bd38bc03e1 100644 --- a/test-framework/junit5-component/src/test/java/io/quarkus/test/component/MockNotSharedForClassHierarchyTest.java +++ b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/MockNotSharedForClassHierarchyTest.java @@ -15,7 +15,8 @@ public class MockNotSharedForClassHierarchyTest { @RegisterExtension - static final QuarkusComponentTestExtension extension = new QuarkusComponentTestExtension(Component.class); + static final QuarkusComponentTestExtension extension = new QuarkusComponentTestExtension(Component.class) + .ignoreNestedClasses(); @Inject Component component; diff --git a/test-framework/junit5-component/src/test/java/io/quarkus/test/component/MockSharedForClassHierarchyTest.java b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/MockSharedForClassHierarchyTest.java index 2e523a427a398..4ad331494aba8 100644 --- a/test-framework/junit5-component/src/test/java/io/quarkus/test/component/MockSharedForClassHierarchyTest.java +++ b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/MockSharedForClassHierarchyTest.java @@ -16,7 +16,7 @@ public class MockSharedForClassHierarchyTest { static final QuarkusComponentTestExtension extension = new QuarkusComponentTestExtension(Component.class).mock(Foo.class) .createMockitoMock(foo -> { Mockito.when(foo.ping()).thenReturn(11); - }); + }).ignoreNestedClasses(); @Inject Component component; diff --git a/test-framework/junit5-component/src/test/java/io/quarkus/test/component/declarative/IgnoreNestedClassesTest.java b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/declarative/IgnoreNestedClassesTest.java new file mode 100644 index 0000000000000..f7dbc96da23ca --- /dev/null +++ b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/declarative/IgnoreNestedClassesTest.java @@ -0,0 +1,36 @@ +package io.quarkus.test.component.declarative; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import jakarta.inject.Singleton; + +import org.junit.jupiter.api.Test; + +import io.quarkus.arc.Arc; +import io.quarkus.arc.Unremovable; +import io.quarkus.test.component.QuarkusComponentTest; +import io.quarkus.test.component.declarative.IgnoreNestedClassesTest.Alpha; + +@QuarkusComponentTest(value = Alpha.class, addNestedClassesAsComponents = false) +public class IgnoreNestedClassesTest { + + @Test + public void testComponents() { + assertTrue(Arc.container().instance(Alpha.class).isAvailable()); + assertFalse(Arc.container().instance(Bravo.class).isAvailable()); + } + + @Unremovable + @Singleton + static class Alpha { + + } + + @Unremovable + @Singleton + static class Bravo { + + } + +} diff --git a/test-framework/junit5-component/src/test/java/io/quarkus/test/component/declarative/InterceptorMockingTest.java b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/declarative/InterceptorMockingTest.java new file mode 100644 index 0000000000000..8c509bd71bac0 --- /dev/null +++ b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/declarative/InterceptorMockingTest.java @@ -0,0 +1,74 @@ +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 jakarta.annotation.Priority; +import jakarta.inject.Inject; +import jakarta.inject.Singleton; +import jakarta.interceptor.AroundInvoke; +import jakarta.interceptor.Interceptor; +import jakarta.interceptor.InterceptorBinding; +import jakarta.interceptor.InvocationContext; + +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import io.quarkus.test.InjectMock; +import io.quarkus.test.component.QuarkusComponentTest; +import io.quarkus.test.component.beans.Charlie; + +@QuarkusComponentTest +public class InterceptorMockingTest { + + @Inject + TheComponent theComponent; + + @InjectMock + Charlie charlie; + + @Test + public void testPing() { + Mockito.when(charlie.ping()).thenReturn("ok"); + assertEquals("ok", theComponent.ping()); + } + + @Singleton + static class TheComponent { + + @SimpleBinding + String ping() { + return "true"; + } + + } + + @Target({ TYPE, METHOD }) + @Retention(RUNTIME) + @InterceptorBinding + public @interface SimpleBinding { + + } + + // This interceptor is automatically added as a tested component + @Priority(1) + @SimpleBinding + @Interceptor + static class SimpleInterceptor { + + @Inject + Charlie charlie; + + @AroundInvoke + Object aroundInvoke(InvocationContext context) throws Exception { + return Boolean.parseBoolean(context.proceed().toString()) ? charlie.ping() : "false"; + } + + } + +}