From 6f2db4b2c464aab65a445f36b6d92a6e217829e0 Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Thu, 19 Oct 2023 14:13:20 +0200 Subject: [PATCH] QuarkusTest: handle beans declared on test profile specifically - beans declared on a test profile implementation are only taken into account if the test profile is used - resolves #36554 --- .../builditem/TestProfileBuildItem.java | 21 ++++ .../asciidoc/getting-started-testing.adoc | 8 ++ .../arc/deployment/TestsAsBeansProcessor.java | 103 ++++++++++++++++++ .../it/main/SharedProfileTestCase.java | 32 ++++++ .../AbstractJvmQuarkusTestExtension.java | 4 + .../test/junit/QuarkusTestExtension.java | 13 ++- .../test/junit/QuarkusTestProfile.java | 5 +- 7 files changed, 183 insertions(+), 3 deletions(-) create mode 100644 core/deployment/src/main/java/io/quarkus/deployment/builditem/TestProfileBuildItem.java diff --git a/core/deployment/src/main/java/io/quarkus/deployment/builditem/TestProfileBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/builditem/TestProfileBuildItem.java new file mode 100644 index 0000000000000..33949f2e51760 --- /dev/null +++ b/core/deployment/src/main/java/io/quarkus/deployment/builditem/TestProfileBuildItem.java @@ -0,0 +1,21 @@ +package io.quarkus.deployment.builditem; + +import io.quarkus.builder.item.SimpleBuildItem; + +/** + * This is an optional build item that represents the current test profile. + *

+ * It is only available during tests. + */ +public final class TestProfileBuildItem extends SimpleBuildItem { + + private final String testProfileClassName; + + public TestProfileBuildItem(String testProfileClassName) { + this.testProfileClassName = testProfileClassName; + } + + public String getTestProfileClassName() { + return testProfileClassName; + } +} diff --git a/docs/src/main/asciidoc/getting-started-testing.adoc b/docs/src/main/asciidoc/getting-started-testing.adoc index 195b12ce3d705..31639cfb3af85 100644 --- a/docs/src/main/asciidoc/getting-started-testing.adoc +++ b/docs/src/main/asciidoc/getting-started-testing.adoc @@ -504,6 +504,8 @@ import java.util.List; import java.util.Map; import java.util.Set; +import jakarta.enterprise.inject.Produces; + import io.quarkus.test.junit.QuarkusTestProfile; import io.quarkus.test.junit.QuarkusTestProfile.TestResourceEntry; @@ -601,9 +603,15 @@ public class MockGreetingProfile implements QuarkusTestProfile { <1> public boolean disableApplicationLifecycleObservers() { return false; } + + @Produces <2> + public ExternalService mockExternalService() { + return new ExternalService("mock"); + } } ---- <1> All these methods have default implementations so just override the ones you need to override. +<2> If a test profile implementation declares a CDI bean (via producer method/field or nested static class) then this bean is only taken into account if the test profile is used, i.e. it's ignored for any other test profile. Now we have defined our profile we need to include it on our test class. We do this by annotating the test class with `@TestProfile(MockGreetingProfile.class)`. diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/TestsAsBeansProcessor.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/TestsAsBeansProcessor.java index aaa6d62d269bf..d22f514216b0e 100644 --- a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/TestsAsBeansProcessor.java +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/TestsAsBeansProcessor.java @@ -1,13 +1,28 @@ package io.quarkus.arc.deployment; +import java.lang.reflect.Modifier; +import java.util.HashSet; import java.util.List; +import java.util.Optional; +import java.util.Set; +import org.jboss.jandex.AnnotationTarget; +import org.jboss.jandex.AnnotationTarget.Kind; +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.ClassInfo.NestingType; import org.jboss.jandex.DotName; +import org.jboss.jandex.IndexView; +import io.quarkus.arc.processor.Annotations; +import io.quarkus.arc.processor.AnnotationsTransformer; +import io.quarkus.arc.processor.DotNames; +import io.quarkus.deployment.IsTest; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.builditem.CombinedIndexBuildItem; import io.quarkus.deployment.builditem.TestAnnotationBuildItem; import io.quarkus.deployment.builditem.TestClassBeanBuildItem; +import io.quarkus.deployment.builditem.TestProfileBuildItem; public class TestsAsBeansProcessor { @@ -31,4 +46,92 @@ public void testClassBeans(List items, BuildProducer testProfile, + CustomScopeAnnotationsBuildItem customScopes, CombinedIndexBuildItem index) { + if (index.getIndex().getAllKnownImplementors(QUARKUS_TEST_PROFILE).isEmpty()) { + // No test profiles found + return null; + } + + Set currentTestProfileHierarchy = initTestProfileHierarchy(testProfile, index.getComputingIndex()); + return new AnnotationsTransformerBuildItem(new AnnotationsTransformer() { + + @Override + public void transform(TransformationContext context) { + AnnotationTarget target = context.getTarget(); + if (target.kind() == Kind.METHOD) { + vetoProducerIfNecessary(target.asMethod().declaringClass(), context); + } else if (target.kind() == Kind.FIELD) { + vetoProducerIfNecessary(target.asField().declaringClass(), context); + } else if (target.kind() == Kind.CLASS) { + ClassInfo clazz = target.asClass(); + if (clazz.nestingType() == NestingType.INNER && Modifier.isStatic(clazz.flags())) { + ClassInfo enclosing = index.getComputingIndex().getClassByName(clazz.enclosingClass()); + if (customScopes.isScopeIn(context.getAnnotations()) + && isTestProfileClass(enclosing, index.getComputingIndex()) + && !currentTestProfileHierarchy.contains(enclosing.name())) { + // Veto static nested class declared on a test profile class + context.transform().add(DotNames.VETOED).done(); + } + } + } + } + + private void vetoProducerIfNecessary(ClassInfo declaringClass, TransformationContext context) { + if (Annotations.contains(context.getAnnotations(), DotNames.PRODUCES) + && isTestProfileClass(declaringClass, index.getComputingIndex()) + && !currentTestProfileHierarchy.contains(declaringClass.name())) { + // Veto producer method/field declared on a test profile class + context.transform().add(DotNames.VETOED_PRODUCER).done(); + } + } + }); + } + + private static final DotName QUARKUS_TEST_PROFILE = DotName.createSimple("io.quarkus.test.junit.QuarkusTestProfile"); + + private static Set initTestProfileHierarchy(Optional testProfile, IndexView index) { + Set ret = Set.of(); + if (testProfile.isPresent()) { + DotName testProfileClassName = DotName.createSimple(testProfile.get().getTestProfileClassName()); + ret = Set.of(testProfileClassName); + ClassInfo testProfileClass = index.getClassByName(testProfile.get().getTestProfileClassName()); + if (testProfileClass != null && !testProfileClass.superName().equals(DotName.OBJECT_NAME)) { + ret = new HashSet<>(); + ret.add(testProfileClassName); + DotName superName = testProfileClass.superName(); + while (superName != null && !superName.equals(DotNames.OBJECT)) { + ret.add(superName); + ClassInfo superClass = index.getClassByName(superName); + if (superClass != null) { + superName = superClass.superName(); + } else { + superName = null; + } + } + } + } + return ret; + } + + private static boolean isTestProfileClass(ClassInfo clazz, IndexView index) { + if (clazz.interfaceNames().contains(QUARKUS_TEST_PROFILE)) { + return true; + } + DotName superName = clazz.superName(); + while (superName != null && !superName.equals(DotNames.OBJECT)) { + ClassInfo superClass = index.getClassByName(superName); + if (superClass != null) { + if (superClass.interfaceNames().contains(QUARKUS_TEST_PROFILE)) { + return true; + } + superName = superClass.superName(); + } else { + superName = null; + } + } + return false; + } + } diff --git a/integration-tests/main/src/test/java/io/quarkus/it/main/SharedProfileTestCase.java b/integration-tests/main/src/test/java/io/quarkus/it/main/SharedProfileTestCase.java index aaa514b500a49..606a3304d57eb 100644 --- a/integration-tests/main/src/test/java/io/quarkus/it/main/SharedProfileTestCase.java +++ b/integration-tests/main/src/test/java/io/quarkus/it/main/SharedProfileTestCase.java @@ -1,6 +1,7 @@ package io.quarkus.it.main; import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertEquals; import java.util.Collections; import java.util.List; @@ -9,9 +10,16 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; +import jakarta.annotation.Priority; +import jakarta.enterprise.inject.Alternative; +import jakarta.enterprise.inject.Produces; +import jakarta.inject.Inject; + import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import io.quarkus.it.rest.ExternalService; +import io.quarkus.it.rest.GreetingService; import io.quarkus.test.common.QuarkusTestResourceLifecycleManager; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.junit.QuarkusTestProfile; @@ -25,6 +33,12 @@ @TestProfile(SharedProfileTestCase.MyProfile.class) public class SharedProfileTestCase { + @Inject + GreetingService greetingService; + + @Inject + ExternalService externalService; + @Test public void included() { RestAssured.when() @@ -50,6 +64,12 @@ public void testContext() { Assertions.assertEquals(MyProfile.class.getName(), DummyTestResource.testProfile.get()); } + @Test + public void testProfileBeans() { + assertEquals("Bonjour Foo", greetingService.greet("Foo")); + assertEquals("profile", externalService.service()); + } + public static class MyProfile implements QuarkusTestProfile { @Override @@ -77,6 +97,18 @@ public String[] commandLineParameters() { public boolean runMainMethod() { return true; } + + @Priority(1000) // Must be higher than priority of MockExternalService + @Alternative + @Produces + public ExternalService externalService() { + return new ExternalService() { + @Override + public String service() { + return "profile"; + } + }; + } } /** diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/AbstractJvmQuarkusTestExtension.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/AbstractJvmQuarkusTestExtension.java index 3c343ab970eaf..516e5ada77552 100644 --- a/test-framework/junit5/src/main/java/io/quarkus/test/junit/AbstractJvmQuarkusTestExtension.java +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/AbstractJvmQuarkusTestExtension.java @@ -46,6 +46,7 @@ public class AbstractJvmQuarkusTestExtension extends AbstractQuarkusTestWithCont protected static final String TEST_LOCATION = "test-location"; protected static final String TEST_CLASS = "test-class"; + protected static final String TEST_PROFILE = "test-profile"; protected ClassLoader originalCl; @@ -202,6 +203,9 @@ protected PrepareResult createAugmentor(ExtensionContext context, Class props = new HashMap<>(); props.put(TEST_LOCATION, testClassLocation); props.put(TEST_CLASS, requiredTestClass); + if (profile != null) { + props.put(TEST_PROFILE, profile.getName()); + } quarkusTestProfile = profile; return new PrepareResult(curatedApplication .createAugmentor(QuarkusTestExtension.TestBuildChainFunction.class.getName(), props), profileInstance, diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestExtension.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestExtension.java index 92844163fd9ba..4002b9139cf36 100644 --- a/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestExtension.java +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestExtension.java @@ -83,6 +83,7 @@ import io.quarkus.deployment.builditem.TestAnnotationBuildItem; import io.quarkus.deployment.builditem.TestClassBeanBuildItem; import io.quarkus.deployment.builditem.TestClassPredicateBuildItem; +import io.quarkus.deployment.builditem.TestProfileBuildItem; import io.quarkus.dev.testing.ExceptionReporting; import io.quarkus.dev.testing.TracingHandler; import io.quarkus.runtime.ApplicationLifecycleManager; @@ -582,8 +583,6 @@ private boolean isNativeOrIntegrationTest(Class clazz) { } private QuarkusTestExtensionState ensureStarted(ExtensionContext extensionContext) { - ExtensionContext.Store store = getStoreFromContext(extensionContext); - QuarkusTestExtensionState state = getState(extensionContext); Class selectedProfile = getQuarkusTestProfile(extensionContext); boolean wrongProfile = !Objects.equals(selectedProfile, quarkusTestProfile); @@ -1356,6 +1355,16 @@ public void execute(BuildContext context) { .build(); } + buildChainBuilder.addBuildStep(new BuildStep() { + @Override + public void execute(BuildContext context) { + Object testProfile = stringObjectMap.get(TEST_PROFILE); + if (testProfile != null) { + context.produce(new TestProfileBuildItem(testProfile.toString())); + } + } + }).produces(TestProfileBuildItem.class).build(); + } }; allCustomizers.add(defaultCustomizer); diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestProfile.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestProfile.java index 18d6c18dcabcf..6b64d87733a4b 100644 --- a/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestProfile.java +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestProfile.java @@ -10,7 +10,10 @@ /** * Defines a 'test profile'. Tests run under a test profile * will have different configuration options to other tests. - * + *

+ * If an implementation of this interface declares CDI beans, via producer methods/fields and nested static classes, then those + * beans are only taken into account if this test profile is used. In other words, the beans are ignored for any other test + * profile. */ public interface QuarkusTestProfile {