, ?> create) {
+ createFunctions.put(key, create);
+ }
+
+ static void clear() {
+ createFunctions.clear();
+ }
+
+}
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
new file mode 100644
index 0000000000000..5b26337f7966d
--- /dev/null
+++ b/test-framework/junit5-component/src/main/java/io/quarkus/test/component/QuarkusComponentTest.java
@@ -0,0 +1,42 @@
+package io.quarkus.test.component;
+
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import io.smallrye.common.annotation.Experimental;
+
+/**
+ * Registers the {@link QuarkusComponentTestExtension} that makes it easy to test Quarkus components.
+ */
+@Experimental("This feature is experimental and the API may change in the future")
+@ExtendWith(QuarkusComponentTestExtension.class)
+@Retention(RUNTIME)
+@Target({ TYPE })
+public @interface QuarkusComponentTest {
+
+ /**
+ * The set of additional components under test.
+ *
+ * The initial set of components is derived from the test class. The types of all fields annotated with
+ * {@link jakarta.inject.Inject} are considered the component types.
+ *
+ * @return the components under test
+ */
+ Class>[] value() default {};
+
+ /**
+ * Indicates that the default values should be used for missing config properties.
+ *
+ * If not used then a missing config property results in a test failure.
+ *
+ * For primitives the default values as defined in the JLS are used. For any other type {@code null} is injected.
+ *
+ * @see QuarkusComponentTestExtension#useDefaultConfigProperties()
+ */
+ boolean useDefaultConfigProperties() default false;
+}
diff --git a/test-framework/junit5-component/src/main/java/io/quarkus/test/component/QuarkusComponentTestClassLoader.java b/test-framework/junit5-component/src/main/java/io/quarkus/test/component/QuarkusComponentTestClassLoader.java
new file mode 100644
index 0000000000000..211be976cafbf
--- /dev/null
+++ b/test-framework/junit5-component/src/main/java/io/quarkus/test/component/QuarkusComponentTestClassLoader.java
@@ -0,0 +1,39 @@
+package io.quarkus.test.component;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URL;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.Objects;
+
+import io.quarkus.arc.ComponentsProvider;
+import io.quarkus.arc.ResourceReferenceProvider;
+
+class QuarkusComponentTestClassLoader extends ClassLoader {
+
+ private final File componentsProviderFile;
+ private final File resourceReferenceProviderFile;
+
+ public QuarkusComponentTestClassLoader(ClassLoader parent, File componentsProviderFile,
+ File resourceReferenceProviderFile) {
+ super(parent);
+ this.componentsProviderFile = Objects.requireNonNull(componentsProviderFile);
+ this.resourceReferenceProviderFile = resourceReferenceProviderFile;
+ }
+
+ @Override
+ public Enumeration getResources(String name) throws IOException {
+ if (("META-INF/services/" + ComponentsProvider.class.getName()).equals(name)) {
+ // return URL that points to the correct components provider
+ return Collections.enumeration(Collections.singleton(componentsProviderFile.toURI()
+ .toURL()));
+ } else if (resourceReferenceProviderFile != null
+ && ("META-INF/services/" + ResourceReferenceProvider.class.getName()).equals(name)) {
+ return Collections.enumeration(Collections.singleton(resourceReferenceProviderFile.toURI()
+ .toURL()));
+ }
+ return super.getResources(name);
+ }
+
+}
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
new file mode 100644
index 0000000000000..4abdad8044d14
--- /dev/null
+++ b/test-framework/junit5-component/src/main/java/io/quarkus/test/component/QuarkusComponentTestExtension.java
@@ -0,0 +1,831 @@
+package io.quarkus.test.component;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.AnnotatedElement;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.lang.reflect.Parameter;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+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.inject.Inject;
+import jakarta.inject.Singleton;
+
+import org.eclipse.microprofile.config.Config;
+import org.eclipse.microprofile.config.inject.ConfigProperty;
+import org.eclipse.microprofile.config.spi.ConfigProviderResolver;
+import org.eclipse.microprofile.config.spi.ConfigSource;
+import org.jboss.jandex.AnnotationInstance;
+import org.jboss.jandex.ClassInfo;
+import org.jboss.jandex.ClassType;
+import org.jboss.jandex.DotName;
+import org.jboss.jandex.IndexView;
+import org.jboss.jandex.Indexer;
+import org.jboss.jandex.Type;
+import org.jboss.jandex.Type.Kind;
+import org.jboss.logging.Logger;
+import org.junit.jupiter.api.extension.AfterAllCallback;
+import org.junit.jupiter.api.extension.AfterEachCallback;
+import org.junit.jupiter.api.extension.BeforeAllCallback;
+import org.junit.jupiter.api.extension.BeforeEachCallback;
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.junit.jupiter.api.extension.TestInstancePostProcessor;
+import org.junit.jupiter.api.extension.TestInstancePreDestroyCallback;
+
+import io.quarkus.arc.Arc;
+import io.quarkus.arc.ArcContainer;
+import io.quarkus.arc.ComponentsProvider;
+import io.quarkus.arc.InstanceHandle;
+import io.quarkus.arc.processor.Annotations;
+import io.quarkus.arc.processor.AnnotationsTransformer;
+import io.quarkus.arc.processor.BeanArchives;
+import io.quarkus.arc.processor.BeanConfigurator;
+import io.quarkus.arc.processor.BeanDeployment;
+import io.quarkus.arc.processor.BeanDeploymentValidator.ValidationContext;
+import io.quarkus.arc.processor.BeanInfo;
+import io.quarkus.arc.processor.BeanProcessor;
+import io.quarkus.arc.processor.BeanRegistrar;
+import io.quarkus.arc.processor.BeanResolver;
+import io.quarkus.arc.processor.Beans;
+import io.quarkus.arc.processor.BuildExtension.Key;
+import io.quarkus.arc.processor.BuiltinBean;
+import io.quarkus.arc.processor.BytecodeTransformer;
+import io.quarkus.arc.processor.ContextRegistrar;
+import io.quarkus.arc.processor.InjectionPointInfo;
+import io.quarkus.arc.processor.InjectionPointInfo.TypeAndQualifiers;
+import io.quarkus.arc.processor.ResourceOutput;
+import io.quarkus.arc.processor.Types;
+import io.quarkus.bootstrap.classloading.QuarkusClassLoader;
+import io.quarkus.runtime.configuration.ApplicationPropertiesConfigSourceLoader;
+import io.smallrye.common.annotation.Experimental;
+import io.smallrye.config.SmallRyeConfig;
+import io.smallrye.config.SmallRyeConfigBuilder;
+import io.smallrye.config.SmallRyeConfigProviderResolver;
+
+/**
+ * JUnit extension that makes it easy to test Quarkus components, aka the CDI beans.
+ *
+ * Lifecycle
+ *
+ * The CDI container is started and a dedicated SmallRyeConfig is registered during the {@code before all} test phase. The
+ * container is stopped and the config is released during the {@code after all} test phase. The fields annotated with
+ * {@code jakarta.inject.Inject} are injected after a test instance is created and unset before a test instance is destroyed.
+ * Moreover, the dependent beans injected into fields annotated with {@code jakarta.inject.Inject} are correctly destroyed
+ * before a test instance is destroyed. Finally, the CDI request context is activated and terminated per
+ * each test method.
+ *
+ *
Auto Mocking Unsatisfied Dependencies
+ *
+ * Unlike in regular CDI environments the test does not fail if a component injects an unsatisfied dependency. Instead, a
+ * synthetic bean is registered automatically for each combination of required type and qualifiers of an injection point that
+ * resolves to an unsatisfied dependency. The bean has the {@link Singleton} scope so it's shared across all injection points
+ * with the same required type and qualifiers. The injected reference is an unconfigured Mockito mock. You can inject the mock
+ * in your test and leverage the Mockito API to configure the behavior.
+ *
+ *
Custom Mocks For Unsatisfied Dependencies
+ *
+ * Sometimes you need the full control over the bean attributes and maybe even configure the default mock behavior. You can use
+ * the mock configurator API via the {@link #mock(Class)} method.
+ *
+ *
Configuration
+ *
+ * A dedicated {@link SmallRyeConfig} is registered during the {@code before all} test phase. Moreover, it's possible to set the
+ * configuration properties via the {@link #configProperty(String, String)} method. If you only need to use the default values
+ * for missing config properties, then the {@link #useDefaultConfigProperties()}
+ * might come in useful.
+ */
+@Experimental("This feature is experimental and the API may change in the future")
+public class QuarkusComponentTestExtension
+ implements BeforeAllCallback, AfterAllCallback, BeforeEachCallback, AfterEachCallback, TestInstancePostProcessor,
+ TestInstancePreDestroyCallback, ConfigSource {
+
+ private static final Logger LOG = Logger.getLogger(QuarkusComponentTestExtension.class);
+
+ private static final ExtensionContext.Namespace NAMESPACE = ExtensionContext.Namespace
+ .create(QuarkusComponentTestExtension.class);
+
+ // Strings used as keys in ExtensionContext.Store
+ private static final String KEY_OLD_TCCL = "oldTccl";
+ 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_CONFIG = "config";
+
+ private static final String TARGET_TEST_CLASSES = "target/test-classes";
+
+ private final Map configProperties;
+ private final List> additionalComponentClasses;
+ private final List> mockConfigurators;
+ private final AtomicBoolean useDefaultConfigProperties = new AtomicBoolean();
+
+ // Used for declarative registration
+ public QuarkusComponentTestExtension() {
+ this.additionalComponentClasses = List.of();
+ this.configProperties = new HashMap<>();
+ this.mockConfigurators = new ArrayList<>();
+ }
+
+ /**
+ * The initial set of components under test is derived from the test class. The types of all fields annotated with
+ * {@link jakarta.inject.Inject} are considered the component types.
+ *
+ * @param additionalComponentClasses
+ */
+ public QuarkusComponentTestExtension(Class>... additionalComponentClasses) {
+ this.additionalComponentClasses = List.of(additionalComponentClasses);
+ this.configProperties = new HashMap<>();
+ this.mockConfigurators = new ArrayList<>();
+ }
+
+ /**
+ * Configure a new mock of a bean.
+ *
+ * Note that a mock is created automatically for all unsatisfied dependencies in the test. This API provides full control
+ * over the bean attributes. The default values are derived from the bean class.
+ *
+ * @param beanClass
+ * @return a new mock bean configurator
+ * @see MockBeanConfigurator#create(Function)
+ */
+ public MockBeanConfigurator mock(Class beanClass) {
+ return new MockBeanConfiguratorImpl<>(this, beanClass);
+ }
+
+ /**
+ * Set a configuration property for the test.
+ *
+ * @param key
+ * @param value
+ * @return the extension
+ */
+ public QuarkusComponentTestExtension configProperty(String key, String value) {
+ this.configProperties.put(key, value);
+ return this;
+ }
+
+ /**
+ * Use the default values for missing config properties. By default, if missing config property results in a test failure.
+ *
+ * For primitives the default values as defined in the JLS are used. For any other type {@code null} is injected.
+ *
+ * @return the extension
+ */
+ public QuarkusComponentTestExtension useDefaultConfigProperties() {
+ this.useDefaultConfigProperties.set(true);
+ return this;
+ }
+
+ @Override
+ public void postProcessTestInstance(Object testInstance, ExtensionContext context) throws Exception {
+ long start = System.nanoTime();
+
+ // Inject test class fields
+ context.getRoot().getStore(NAMESPACE).put(KEY_INJECTED_FIELDS,
+ injectFields(context.getRequiredTestClass(), testInstance));
+
+ LOG.debugf("postProcessTestInstance: %s ms", TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start));
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public void preDestroyTestInstance(ExtensionContext context) throws Exception {
+ long start = System.nanoTime();
+
+ for (FieldInjector fieldInjector : (List) context.getRoot().getStore(NAMESPACE)
+ .get(KEY_INJECTED_FIELDS, List.class)) {
+ fieldInjector.unset(context.getRequiredTestInstance());
+ }
+
+ LOG.debugf("preDestroyTestInstance: %s ms", TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start));
+ }
+
+ @Override
+ public void beforeAll(ExtensionContext context) throws Exception {
+ long start = System.nanoTime();
+
+ Class> testClass = context.getRequiredTestClass();
+
+ // Extension may be registered declaratively
+ Set> componentClasses = new HashSet<>(this.additionalComponentClasses);
+ QuarkusComponentTest testAnnotation = testClass.getAnnotation(QuarkusComponentTest.class);
+ if (testAnnotation != null) {
+ Collections.addAll(componentClasses, testAnnotation.value());
+ if (testAnnotation.useDefaultConfigProperties()) {
+ this.useDefaultConfigProperties.set(true);
+ }
+ }
+ // All fields annotated with @Inject represent component classes
+ for (Field field : testClass.getDeclaredFields()) {
+ if (field.isAnnotationPresent(Inject.class) && !resolvesToBuiltinBean(field.getType())) {
+ componentClasses.add(field.getType());
+ }
+ }
+
+ TestConfigProperty[] testConfigProperties = testClass.getAnnotationsByType(TestConfigProperty.class);
+ for (TestConfigProperty testConfigProperty : testConfigProperties) {
+ this.configProperties.put(testConfigProperty.key(), testConfigProperty.value());
+ }
+
+ ClassLoader oldTccl = initArcContainer(context, componentClasses);
+ context.getRoot().getStore(NAMESPACE).put(KEY_OLD_TCCL, oldTccl);
+
+ ConfigProviderResolver oldConfigProviderResolver = ConfigProviderResolver.instance();
+ context.getRoot().getStore(NAMESPACE).put(KEY_OLD_CONFIG_PROVIDER_RESOLVER, oldConfigProviderResolver);
+
+ SmallRyeConfigProviderResolver smallRyeConfigProviderResolver = new SmallRyeConfigProviderResolver();
+ ConfigProviderResolver.setInstance(smallRyeConfigProviderResolver);
+
+ // TCCL is now the QuarkusComponentTestClassLoader set during initialization
+ ClassLoader tccl = Thread.currentThread().getContextClassLoader();
+ SmallRyeConfig config = new SmallRyeConfigBuilder().forClassLoader(tccl)
+ .addDefaultInterceptors()
+ .addDefaultSources()
+ .withSources(new ApplicationPropertiesConfigSourceLoader.InFileSystem())
+ .withSources(new ApplicationPropertiesConfigSourceLoader.InClassPath())
+ .withSources(this)
+ .build();
+ smallRyeConfigProviderResolver.registerConfig(config, tccl);
+ context.getRoot().getStore(NAMESPACE).put(KEY_CONFIG, config);
+ ConfigBeanCreator.setClassLoader(tccl);
+
+ LOG.debugf("beforeAll: %s ms", TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start));
+ }
+
+ @Override
+ public void afterAll(ExtensionContext context) throws Exception {
+ long start = System.nanoTime();
+
+ ClassLoader oldTccl = context.getRoot().getStore(NAMESPACE).get(KEY_OLD_TCCL, ClassLoader.class);
+ Thread.currentThread().setContextClassLoader(oldTccl);
+
+ try {
+ Arc.shutdown();
+ } catch (Exception e) {
+ LOG.error("An error occured during ArC shutdown: " + e);
+ }
+ MockBeanCreator.clear();
+ ConfigBeanCreator.clear();
+
+ SmallRyeConfig config = context.getRoot().getStore(NAMESPACE).get(KEY_CONFIG, SmallRyeConfig.class);
+ ConfigProviderResolver.instance().releaseConfig(config);
+ ConfigProviderResolver
+ .setInstance(context.getRoot().getStore(NAMESPACE).get(KEY_OLD_CONFIG_PROVIDER_RESOLVER,
+ ConfigProviderResolver.class));
+
+ @SuppressWarnings("unchecked")
+ Set generatedResources = context.getRoot().getStore(NAMESPACE).get(KEY_GENERATED_RESOURCES, Set.class);
+ for (Path path : generatedResources) {
+ try {
+ LOG.debugf("Delete generated %s", path);
+ Files.deleteIfExists(path);
+ } catch (IOException e) {
+ LOG.errorf("Unable to delete the generated resource %s: ", path, e.getMessage());
+ }
+ }
+
+ LOG.debugf("afterAll: %s ms", TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start));
+ }
+
+ @Override
+ public void beforeEach(ExtensionContext context) throws Exception {
+ long start = System.nanoTime();
+
+ // Activate the request context
+ ArcContainer container = Arc.container();
+ container.requestContext().activate();
+
+ LOG.debugf("beforeEach: %s ms", TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start));
+ }
+
+ @Override
+ public void afterEach(ExtensionContext context) throws Exception {
+ long start = System.nanoTime();
+
+ // Terminate the request context
+ ArcContainer container = Arc.container();
+ container.requestContext().terminate();
+
+ LOG.debugf("afterEach: %s ms", TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start));
+ }
+
+ @Override
+ public Set getPropertyNames() {
+ return configProperties.keySet();
+ }
+
+ @Override
+ public String getValue(String propertyName) {
+ return configProperties.get(propertyName);
+ }
+
+ @Override
+ public String getName() {
+ return QuarkusComponentTestExtension.class.getName();
+ }
+
+ @Override
+ public int getOrdinal() {
+ // System properties (400) and ENV variables (300) take precedence but application.properties has lower priority (250)
+ return 275;
+ }
+
+ void registerMockBean(MockBeanConfiguratorImpl> mock) {
+ this.mockConfigurators.add(mock);
+ }
+
+ private BeanRegistrar registrarForMock(MockBeanConfiguratorImpl> mock) {
+ return new BeanRegistrar() {
+
+ @Override
+ public void register(RegistrationContext context) {
+ BeanConfigurator