diff --git a/docs/src/main/asciidoc/getting-started-testing.adoc b/docs/src/main/asciidoc/getting-started-testing.adoc
index e24923671bd91d..27b6954d83d5b4 100644
--- a/docs/src/main/asciidoc/getting-started-testing.adoc
+++ b/docs/src/main/asciidoc/getting-started-testing.adoc
@@ -1596,8 +1596,35 @@ public class FooTest {
<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.
-If you need the full control over the `QuarkusComponentTestExtension` configuration then you can use the `@RegisterExtension` annotation and configure the extension programatically.
-The test above could be rewritten like:
+`QuarkusComponentTestExtension` also resolves arguments for parameters of a test method and injects the matching beans.
+So the code snippet above can be rewritten as:
+
+[source, java]
+----
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import jakarta.inject.Inject;
+import io.quarkus.test.InjectMock;
+import io.quarkus.test.component.TestConfigProperty;
+import io.quarkus.test.component.QuarkusComponentTest;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+@QuarkusComponentTest
+@TestConfigProperty(key = "bar", value = "true")
+public class FooTest {
+
+ @Test
+ public void testPing(Foo foo, @InjectMock Charlie charlieMock) { <1>
+ Mockito.when(charlieMock.ping()).thenReturn("OK");
+ assertEquals("OK", foo.ping());
+ }
+}
+----
+<1> Parameters annotated with `@io.quarkus.test.component.SkipInject` are never resolved by this extension.
+
+Furthermore, if you need the full control over the `QuarkusComponentTestExtension` configuration then you can use the `@RegisterExtension` annotation and configure the extension programatically.
+The original test could be rewritten like:
[source, java]
----
@@ -1638,13 +1665,19 @@ However, if the test instance lifecycle is `Lifecycle#PER_CLASS` then the conta
The fields annotated with `@Inject` and `@InjectMock` are injected after a test instance is created.
Finally, the CDI request context is activated and terminated per each test method.
+=== Injection
+Test class fields annotated with `@jakarta.inject.Inject` and `@io.quarkus.test.InjectMock` are injected after a test instance is created.
+Dependent beans injected into these fields are correctly destroyed before a test instance is destroyed.
+Parameters of a test method for which a matching bean exists are resolved unless annotated with `@io.quarkus.test.component.SkipInject`.
+Dependent beans injected into the test method arguments are correctly destroyed after the test method completes.
+
=== 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 `@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.
+You can inject the mock in your test using the `io.quarkus.test.InjectMock` annotation and leverage the Mockito API to configure the behavior.
=== Custom Mocks For Unsatisfied Dependencies
diff --git a/test-framework/common/src/main/java/io/quarkus/test/InjectMock.java b/test-framework/common/src/main/java/io/quarkus/test/InjectMock.java
index af4292f1f853a6..c8daa498f98631 100644
--- a/test-framework/common/src/main/java/io/quarkus/test/InjectMock.java
+++ b/test-framework/common/src/main/java/io/quarkus/test/InjectMock.java
@@ -1,18 +1,19 @@
package io.quarkus.test;
import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
/**
- * Instructs the test engine to inject a mock instance of a bean in the field of a test class.
+ * Instructs the test engine to inject a mock instance of a bean in the target annotated element in a test class.
*
* This annotation is supported:
*
- *
in a {@code io.quarkus.test.component.QuarkusComponentTest},
- *
in a {@code io.quarkus.test.QuarkusTest} if {@code quarkus-junit5-mockito} is present.
+ *
for fields and method parameters in a {@code io.quarkus.test.component.QuarkusComponentTest},
+ *
for fields in a {@code io.quarkus.test.QuarkusTest} if {@code quarkus-junit5-mockito} is present.
*
* The lifecycle and configuration API of the injected mock depends on the type of test being used.
*
@@ -21,7 +22,7 @@
* {@code io.quarkus.test.junit.QuarkusTest}.
*/
@Retention(RUNTIME)
-@Target(FIELD)
+@Target({ FIELD, PARAMETER })
public @interface InjectMock {
}
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 8b5f82d9d5ddbc..5a354087ea06aa 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
@@ -28,7 +28,8 @@
* 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.
+ * {@link jakarta.inject.Inject} are considered the component types. Furthermore, all types of parameters of test methods
+ * that are not annotated with {@link InjectMock} or {@link SkipInject} are also considered the component types.
*
* @return the components under test
*/
diff --git a/test-framework/junit5-component/src/main/java/io/quarkus/test/component/QuarkusComponentTestConfiguration.java b/test-framework/junit5-component/src/main/java/io/quarkus/test/component/QuarkusComponentTestConfiguration.java
index 8d6d624903b2ba..dee50418ce2d9d 100644
--- a/test-framework/junit5-component/src/main/java/io/quarkus/test/component/QuarkusComponentTestConfiguration.java
+++ b/test-framework/junit5-component/src/main/java/io/quarkus/test/component/QuarkusComponentTestConfiguration.java
@@ -3,6 +3,7 @@
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
+import java.lang.reflect.Parameter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
@@ -17,9 +18,12 @@
import jakarta.inject.Provider;
import org.jboss.logging.Logger;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInfo;
import io.quarkus.arc.InjectableInstance;
import io.quarkus.arc.processor.AnnotationsTransformer;
+import io.quarkus.test.InjectMock;
class QuarkusComponentTestConfiguration {
@@ -74,23 +78,36 @@ QuarkusComponentTestConfiguration update(Class> testClass) {
}
}
}
- // All fields annotated with @Inject represent component classes
Class> current = testClass;
- while (current != null) {
+ while (current != null && current != Object.class) {
+ // All fields annotated with @Inject represent component classes
for (Field field : current.getDeclaredFields()) {
if (field.isAnnotationPresent(Inject.class) && !resolvesToBuiltinBean(field.getType())) {
componentClasses.add(field.getType());
}
}
- current = current.getSuperclass();
- }
- // All static nested classes declared on the test class are components
- if (addNestedClassesAsComponents) {
- for (Class> declaredClass : testClass.getDeclaredClasses()) {
- if (Modifier.isStatic(declaredClass.getModifiers())) {
- componentClasses.add(declaredClass);
+ // All static nested classes declared on the test class are components
+ if (addNestedClassesAsComponents) {
+ for (Class> declaredClass : current.getDeclaredClasses()) {
+ if (Modifier.isStatic(declaredClass.getModifiers())) {
+ componentClasses.add(declaredClass);
+ }
}
}
+ // All params of test methods but not TestInfo, not annotated with @InjectMock and not annotated with @SkipInject
+ for (Method method : current.getDeclaredMethods()) {
+ if (method.isAnnotationPresent(Test.class)) {
+ for (Parameter param : method.getParameters()) {
+ if (param.getType() == TestInfo.class
+ || param.isAnnotationPresent(InjectMock.class)
+ || param.isAnnotationPresent(SkipInject.class)) {
+ continue;
+ }
+ componentClasses.add(param.getType());
+ }
+ }
+ }
+ current = current.getSuperclass();
}
List testConfigProperties = new ArrayList<>();
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 15a2f6e2d7fc02..a3e56d0cee25b5 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
@@ -25,17 +25,20 @@
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
+import java.util.function.Predicate;
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.inject.Default;
import jakarta.enterprise.inject.spi.BeanManager;
import jakarta.enterprise.inject.spi.InjectionPoint;
import jakarta.enterprise.inject.spi.InterceptionType;
@@ -58,12 +61,17 @@
import org.jboss.jandex.Type;
import org.jboss.jandex.Type.Kind;
import org.jboss.logging.Logger;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInfo;
import org.junit.jupiter.api.TestInstance.Lifecycle;
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.ParameterContext;
+import org.junit.jupiter.api.extension.ParameterResolutionException;
+import org.junit.jupiter.api.extension.ParameterResolver;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.junit.jupiter.api.extension.TestInstancePostProcessor;
@@ -108,14 +116,23 @@
* programmatically with a static field of type {@link QuarkusComponentTestExtension}, annotated with {@link RegisterExtension}
* and initialized with {@link #QuarkusComponentTestExtension(Class...) simplified constructor} or using the {@link #builder()
* builder}.
+ *
+ *
Container lifecycle
*
* This extension starts the CDI container and registers a dedicated SmallRyeConfig. If {@link Lifecycle#PER_METHOD} is used
* (default) then the container is started during the {@code before each} test phase and stopped during the {@code after each}
* test phase. However, if {@link Lifecycle#PER_CLASS} is used then the container is started during the {@code before all} test
- * phase and stopped 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.
+ * phase and stopped during the {@code after all} test phase. The CDI request context is activated and terminated per each test
+ * method.
+ *
+ *
Injection
+ *
+ * Test class fields annotated with {@link jakarta.inject.Inject} and {@link io.quarkus.test.InjectMock} are injected after a
+ * test instance is created and unset before a test instance is destroyed. Dependent beans injected into these
+ * fields are correctly destroyed before a test instance is destroyed.
+ *
+ * Parameters of a test method for which a matching bean exists are resolved unless annotated with {@link SkipInject}. Dependent
+ * beans injected into the test method arguments are correctly destroyed after the test method completes.
*
*
Auto Mocking Unsatisfied Dependencies
*
@@ -123,7 +140,7 @@
* 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.
+ * in your test using the {@link io.quarkus.test.InjectMock} annotation and leverage the Mockito API to configure the behavior.
*
*
Custom Mocks For Unsatisfied Dependencies
*
@@ -135,7 +152,8 @@
*/
@Experimental("This feature is experimental and the API may change in the future")
public class QuarkusComponentTestExtension
- implements BeforeAllCallback, AfterAllCallback, BeforeEachCallback, AfterEachCallback, TestInstancePostProcessor {
+ implements BeforeAllCallback, AfterAllCallback, BeforeEachCallback, AfterEachCallback, TestInstancePostProcessor,
+ ParameterResolver {
public static QuarkusComponentTestExtensionBuilder builder() {
return new QuarkusComponentTestExtensionBuilder();
@@ -151,6 +169,7 @@ public static QuarkusComponentTestExtensionBuilder builder() {
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_INJECTED_PARAMS = "injectedParams";
private static final String KEY_TEST_INSTANCE = "testInstance";
private static final String KEY_CONFIG = "config";
private static final String KEY_TEST_CLASS_CONFIG = "testClassConfig";
@@ -192,6 +211,7 @@ public void beforeAll(ExtensionContext context) throws Exception {
@Override
public void afterAll(ExtensionContext context) throws Exception {
long start = System.nanoTime();
+ // Stop the container if Lifecycle.PER_CLASS is used
stopContainer(context, Lifecycle.PER_CLASS);
cleanup(context);
LOG.debugf("afterAll: %s ms", TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start));
@@ -211,6 +231,9 @@ public void afterEach(ExtensionContext context) throws Exception {
long start = System.nanoTime();
// Terminate the request context
Arc.container().requestContext().terminate();
+ // Destroy @Dependent beans injected as test method parameters correctly
+ destroyDependentTestMethodParams(context);
+ // Stop the container if Lifecycle.PER_METHOD is used
stopContainer(context, Lifecycle.PER_METHOD);
LOG.debugf("afterEach: %s ms", TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start));
}
@@ -222,6 +245,68 @@ public void postProcessTestInstance(Object testInstance, ExtensionContext contex
LOG.debugf("postProcessTestInstance: %s ms", TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start));
}
+ @Override
+ public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext)
+ throws ParameterResolutionException {
+ // 1. Only test methods are supported
+ // 2. A param annotated with @SkipInject is never supported
+ if (parameterContext.getTarget().isPresent()
+ && !parameterContext.isAnnotated(SkipInject.class)
+ && parameterContext.getDeclaringExecutable().isAnnotationPresent(Test.class)) {
+ java.lang.reflect.Type requiredType = parameterContext.getParameter().getParameterizedType();
+ if (!requiredType.equals(TestInfo.class)) {
+ BeanManager beanManager = Arc.container().beanManager();
+ Annotation[] qualifiers = getQualifiers(parameterContext.getAnnotatedElement(), beanManager);
+ if (qualifiers.length > 0 && Arrays.stream(qualifiers).anyMatch(All.Literal.INSTANCE::equals)) {
+ // @All List<>
+ if (isListRequiredType(requiredType)) {
+ return true;
+ } else {
+ throw new IllegalStateException("Invalid injection point type: " + parameterContext.getParameter());
+ }
+ } else {
+ // Only resolve the parameter if a single matching bean exists
+ return beanManager.resolve(beanManager.getBeans(requiredType, qualifiers)) != null;
+ }
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public Object resolveParameter(ParameterContext parameterContext, ExtensionContext context)
+ throws ParameterResolutionException {
+ @SuppressWarnings("unchecked")
+ List> injectedParams = context.getRoot().getStore(NAMESPACE).get(KEY_INJECTED_PARAMS, List.class);
+ ArcContainer container = Arc.container();
+ BeanManager beanManager = container.beanManager();
+ java.lang.reflect.Type requiredType = parameterContext.getParameter().getParameterizedType();
+ Annotation[] qualifiers = getQualifiers(parameterContext.getAnnotatedElement(), beanManager);
+ if (qualifiers.length > 0 && Arrays.stream(qualifiers).anyMatch(All.Literal.INSTANCE::equals)) {
+ // Special handling for @Injec @All List<>
+ return handleListAll(requiredType, qualifiers, container, injectedParams);
+ } else {
+ InstanceHandle> handle = container.instance(requiredType, qualifiers);
+ injectedParams.add(handle);
+ return handle.get();
+ }
+ }
+
+ private void destroyDependentTestMethodParams(ExtensionContext context) {
+ @SuppressWarnings("unchecked")
+ List> injectedParams = context.getRoot().getStore(NAMESPACE).get(KEY_INJECTED_PARAMS, List.class);
+ for (InstanceHandle> handle : injectedParams) {
+ if (handle.getBean() != null && handle.getBean().getScope().equals(Dependent.class)) {
+ try {
+ handle.destroy();
+ } catch (Exception e) {
+ LOG.errorf(e, "Unable to destroy the injected %s", handle.getBean());
+ }
+ }
+ }
+ injectedParams.clear();
+ }
+
private void buildContainer(ExtensionContext context) {
QuarkusComponentTestConfiguration testClassConfiguration = baseConfiguration
.update(context.getRequiredTestClass());
@@ -316,6 +401,8 @@ private void startContainer(ExtensionContext context, Lifecycle testInstanceLife
Object testInstance = context.getRequiredTestInstance();
context.getRoot().getStore(NAMESPACE).put(KEY_INJECTED_FIELDS,
injectFields(context.getRequiredTestClass(), testInstance));
+ // Injected test method parameters
+ context.getRoot().getStore(NAMESPACE).put(KEY_INJECTED_PARAMS, new CopyOnWriteArrayList<>());
}
}
@@ -345,9 +432,9 @@ public void register(RegistrationContext context) {
};
}
- private static Annotation[] getQualifiers(Field field, BeanManager beanManager) {
+ private static Annotation[] getQualifiers(AnnotatedElement element, BeanManager beanManager) {
List ret = new ArrayList<>();
- Annotation[] annotations = field.getDeclaredAnnotations();
+ Annotation[] annotations = element.getDeclaredAnnotations();
for (Annotation fieldAnnotation : annotations) {
if (beanManager.isQualifier(fieldAnnotation.annotationType())) {
ret.add(fieldAnnotation);
@@ -356,10 +443,10 @@ private static Annotation[] getQualifiers(Field field, BeanManager beanManager)
return ret.toArray(new Annotation[0]);
}
- private static Set getQualifiers(Field field, Collection qualifiers) {
+ private static Set getQualifiers(AnnotatedElement element, Collection qualifiers) {
Set ret = new HashSet<>();
- Annotation[] fieldAnnotations = field.getDeclaredAnnotations();
- for (Annotation annotation : fieldAnnotations) {
+ Annotation[] annotations = element.getDeclaredAnnotations();
+ for (Annotation annotation : annotations) {
if (qualifiers.contains(DotName.createSimple(annotation.annotationType()))) {
ret.add(Annotations.jandexAnnotation(annotation));
}
@@ -369,8 +456,9 @@ private static Set getQualifiers(Field field, Collection testClass = extensionContext.getRequiredTestClass();
- // Collect all test class injection points to define a bean removal exclusion
- List testClassInjectionPoints = findInjectFields(testClass);
+ // Collect all component injection points to define a bean removal exclusion
+ List injectFields = findInjectFields(testClass);
+ List injectParams = findInjectParams(testClass);
if (configuration.componentClasses.isEmpty()) {
throw new IllegalStateException("No component classes to test");
@@ -416,18 +504,24 @@ private ClassLoader initArcContainer(ExtensionContext extensionContext, QuarkusC
.setName(testClass.getName().replace('.', '_'))
.addRemovalExclusion(b -> {
// Do not remove beans:
- // 1. Injected in the test class
+ // 1. Injected in the test class or in a test method parameter
// 2. Annotated with @Unremovable
if (b.getTarget().isPresent()
&& b.getTarget().get().hasDeclaredAnnotation(Unremovable.class)) {
return true;
}
- for (Field injectionPoint : testClassInjectionPoints) {
+ for (Field injectionPoint : injectFields) {
if (beanResolver.get().matches(b, Types.jandexType(injectionPoint.getGenericType()),
getQualifiers(injectionPoint, qualifiers))) {
return true;
}
}
+ for (Parameter param : injectParams) {
+ if (beanResolver.get().matches(b, Types.jandexType(param.getParameterizedType()),
+ getQualifiers(param, qualifiers))) {
+ return true;
+ }
+ }
return false;
})
.setImmutableBeanArchiveIndex(index)
@@ -522,9 +616,10 @@ public void register(RegistrationContext registrationContext) {
DotName configPropertyDotName = DotName.createSimple(ConfigProperty.class);
DotName configMappingDotName = DotName.createSimple(ConfigMapping.class);
- // Analyze injection points
- // - find Config, @ConfigProperty and config mappings injection points
- // - find unsatisfied injection points
+ // We need to analyze all injection points in order to find
+ // Config, @ConfigProperty and config mappings injection points
+ // and all unsatisfied injection points
+ // to register appropriate synthetic beans
for (InjectionPointInfo injectionPoint : registrationContext.getInjectionPoints()) {
if (injectionPoint.getRequiredType().name().equals(configDotName)
&& injectionPoint.hasDefaultedQualifier()) {
@@ -576,7 +671,7 @@ public void register(RegistrationContext registrationContext) {
LOG.debugf("Unsatisfied injection point found: %s", injectionPoint.getTargetInfo());
}
- // Make sure that all @InjectMock fields are also considered unsatisfied dependencies
+ // Make sure that all @InjectMock injection points 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);
@@ -586,6 +681,14 @@ public void register(RegistrationContext registrationContext) {
unsatisfiedInjectionPoints
.add(new TypeAndQualifiers(Types.jandexType(field.getGenericType()), requiredQualifiers));
}
+ for (Parameter param : findInjectMockParams(testClass)) {
+ Set requiredQualifiers = getQualifiers(param, qualifiers);
+ if (requiredQualifiers.isEmpty()) {
+ requiredQualifiers = Set.of(AnnotationInstance.builder(DotNames.DEFAULT).build());
+ }
+ unsatisfiedInjectionPoints
+ .add(new TypeAndQualifiers(Types.jandexType(param.getParameterizedType()), requiredQualifiers));
+ }
for (TypeAndQualifiers unsatisfied : unsatisfiedInjectionPoints) {
ClassInfo implementationClass = computingIndex.getClassByName(unsatisfied.type.name());
@@ -704,8 +807,17 @@ public void accept(BytecodeTransformer transformer) {
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))) {
+ List> annotations = List.of(AroundInvoke.class, PostConstruct.class, PreDestroy.class,
+ AroundConstruct.class);
+ Predicate predicate = m -> {
+ for (Class extends Annotation> annotation : annotations) {
+ if (m.isAnnotationPresent(annotation)) {
+ return true;
+ }
+ }
+ return false;
+ };
+ for (Method method : findMethods(testClass, predicate)) {
Set bindings = findBindings(method, interceptorBindings);
if (bindings.isEmpty()) {
throw new IllegalStateException("No bindings declared on a test interceptor method: " + method);
@@ -849,6 +961,36 @@ private List findInjectFields(Class> testClass) {
return findFields(testClass, injectAnnotations);
}
+ private List findInjectParams(Class> testClass) {
+ List testMethods = findMethods(testClass, m -> m.isAnnotationPresent(Test.class));
+ List ret = new ArrayList<>();
+ for (Method method : testMethods) {
+ for (Parameter param : method.getParameters()) {
+ if (param.getType().equals(TestInfo.class)
+ || param.isAnnotationPresent(InjectMock.class)
+ || param.isAnnotationPresent(SkipInject.class)) {
+ continue;
+ }
+ ret.add(param);
+ }
+ }
+ return ret;
+ }
+
+ private List findInjectMockParams(Class> testClass) {
+ List testMethods = findMethods(testClass, m -> m.isAnnotationPresent(Test.class));
+ List ret = new ArrayList<>();
+ for (Method method : testMethods) {
+ for (Parameter param : method.getParameters()) {
+ if (param.isAnnotationPresent(InjectMock.class)
+ && !param.getType().equals(TestInfo.class)) {
+ ret.add(param);
+ }
+ }
+ }
+ return ret;
+ }
+
private List findFields(Class> testClass, List> annotations) {
List fields = new ArrayList<>();
Class> current = testClass;
@@ -866,16 +1008,13 @@ private List findFields(Class> testClass, List findMethods(Class> testClass, List> annotations) {
+ private List findMethods(Class> testClass, Predicate methodPredicate) {
List methods = new ArrayList<>();
Class> current = testClass;
while (current.getSuperclass() != null) {
for (Method method : current.getDeclaredMethods()) {
- for (Class extends Annotation> annotation : annotations) {
- if (method.isAnnotationPresent(annotation)) {
- methods.add(method);
- break;
- }
+ if (methodPredicate.test(method)) {
+ methods.add(method);
}
}
current = current.getSuperclass();
@@ -901,13 +1040,8 @@ public FieldInjector(Field field, Object testInstance) throws Exception {
if (qualifiers.length > 0 && Arrays.stream(qualifiers).anyMatch(All.Literal.INSTANCE::equals)) {
// Special handling for @Injec @All List
if (isListRequiredType(requiredType)) {
- List> handles = container.listAll(requiredType, qualifiers);
- if (isTypeArgumentInstanceHandle(requiredType)) {
- injectedInstance = handles;
- } else {
- injectedInstance = handles.stream().map(InstanceHandle::get).collect(Collectors.toUnmodifiableList());
- }
- unsetHandles = cast(handles);
+ unsetHandles = new ArrayList<>();
+ injectedInstance = handleListAll(requiredType, qualifiers, container, unsetHandles);
} else {
throw new IllegalStateException("Invalid injection point type: " + field);
}
@@ -955,6 +1089,23 @@ void unset(Object testInstance) throws Exception {
}
+ private static Object handleListAll(java.lang.reflect.Type requiredType, Annotation[] qualifiers, ArcContainer container,
+ Collection> cleanupHandles) {
+ // Remove @All and add @Default if empty
+ Set qualifiersSet = new HashSet<>();
+ Collections.addAll(qualifiersSet, qualifiers);
+ qualifiersSet.remove(All.Literal.INSTANCE);
+ if (qualifiersSet.isEmpty()) {
+ qualifiers = new Annotation[] { Default.Literal.INSTANCE };
+ } else {
+ qualifiers = qualifiersSet.toArray(new Annotation[] {});
+ }
+ List> handles = container.listAll(getListRequiredType(requiredType), qualifiers);
+ cleanupHandles.addAll(handles);
+ return isTypeArgumentInstanceHandle(requiredType) ? handles
+ : handles.stream().map(InstanceHandle::get).collect(Collectors.toUnmodifiableList());
+ }
+
@SuppressWarnings("unchecked")
private Class extends Annotation> loadDeprecatedInjectMock() {
try {
@@ -972,6 +1123,17 @@ private static boolean isListRequiredType(java.lang.reflect.Type type) {
return false;
}
+ private static java.lang.reflect.Type getListRequiredType(java.lang.reflect.Type requiredType) {
+ if (requiredType instanceof ParameterizedType) {
+ final ParameterizedType parameterizedType = (ParameterizedType) requiredType;
+ if (List.class.equals(parameterizedType.getRawType())) {
+ // List -> String
+ return parameterizedType.getActualTypeArguments()[0];
+ }
+ }
+ return null;
+ }
+
private static boolean isTypeArgumentInstanceHandle(java.lang.reflect.Type type) {
// List -> String
java.lang.reflect.Type typeArgument = ((ParameterizedType) type).getActualTypeArguments()[0];
diff --git a/test-framework/junit5-component/src/main/java/io/quarkus/test/component/SkipInject.java b/test-framework/junit5-component/src/main/java/io/quarkus/test/component/SkipInject.java
new file mode 100644
index 00000000000000..6fe5aecc3057bf
--- /dev/null
+++ b/test-framework/junit5-component/src/main/java/io/quarkus/test/component/SkipInject.java
@@ -0,0 +1,16 @@
+package io.quarkus.test.component;
+
+import static java.lang.annotation.ElementType.PARAMETER;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * Skip injection for the annotated test method parameter.
+ */
+@Retention(RUNTIME)
+@Target(PARAMETER)
+public @interface SkipInject {
+
+}
diff --git a/test-framework/junit5-component/src/test/java/io/quarkus/test/component/beans/MyComponent.java b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/beans/MyComponent.java
index fa46c6ebba45b6..4f6c0ceea55ee3 100644
--- a/test-framework/junit5-component/src/test/java/io/quarkus/test/component/beans/MyComponent.java
+++ b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/beans/MyComponent.java
@@ -31,4 +31,8 @@ void onBoolean(@Observes Boolean payload, Delta delta) {
delta.onBoolean();
}
+ public Charlie getCharlie() {
+ return charlie;
+ }
+
}
diff --git a/test-framework/junit5-component/src/test/java/io/quarkus/test/component/declarative/ListAllMockTest.java b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/declarative/ListAllMockTest.java
index 5534cc64c707a9..8e9ca9f4b14565 100644
--- a/test-framework/junit5-component/src/test/java/io/quarkus/test/component/declarative/ListAllMockTest.java
+++ b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/declarative/ListAllMockTest.java
@@ -27,6 +27,10 @@ public class ListAllMockTest {
@InjectMock
Delta delta;
+ @Inject
+ @All
+ List deltas;
+
@InjectMock
@SimpleQualifier
Bravo bravo;
@@ -38,6 +42,7 @@ public void testMock() {
assertFalse(component.ping());
assertEquals(1, component.bravos.size());
assertEquals("ok", component.bravos.get(0).ping());
+ assertEquals(deltas.get(0).ping(), component.ping());
}
@Singleton
diff --git a/test-framework/junit5-component/src/test/java/io/quarkus/test/component/paraminject/ParameterInjectionDependentTest.java b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/paraminject/ParameterInjectionDependentTest.java
new file mode 100644
index 00000000000000..86e134ca95a9a5
--- /dev/null
+++ b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/paraminject/ParameterInjectionDependentTest.java
@@ -0,0 +1,45 @@
+package io.quarkus.test.component.paraminject;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import jakarta.annotation.PreDestroy;
+import jakarta.enterprise.context.Dependent;
+
+import org.junit.jupiter.api.Order;
+import org.junit.jupiter.api.Test;
+
+import io.quarkus.test.component.QuarkusComponentTest;
+
+@QuarkusComponentTest
+public class ParameterInjectionDependentTest {
+
+ @Order(1)
+ @Test
+ public void testParamsInjection(MyDependent myDependent) {
+ assertTrue(myDependent.ping());
+ }
+
+ @Order(2)
+ @Test
+ public void testDependentDestroyed() {
+ assertTrue(MyDependent.DESTROYED.get());
+ }
+
+ @Dependent
+ public static class MyDependent {
+
+ static final AtomicBoolean DESTROYED = new AtomicBoolean();
+
+ boolean ping() {
+ return true;
+ }
+
+ @PreDestroy
+ void destroy() {
+ DESTROYED.set(true);
+ }
+ }
+
+}
diff --git a/test-framework/junit5-component/src/test/java/io/quarkus/test/component/paraminject/ParameterInjectionPerClassLifecycleTest.java b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/paraminject/ParameterInjectionPerClassLifecycleTest.java
new file mode 100644
index 00000000000000..af1edfe148e139
--- /dev/null
+++ b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/paraminject/ParameterInjectionPerClassLifecycleTest.java
@@ -0,0 +1,46 @@
+package io.quarkus.test.component.paraminject;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.util.UUID;
+
+import jakarta.annotation.PostConstruct;
+import jakarta.inject.Singleton;
+
+import org.junit.jupiter.api.Order;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+import org.junit.jupiter.api.TestInstance.Lifecycle;
+
+import io.quarkus.test.component.QuarkusComponentTest;
+
+@TestInstance(Lifecycle.PER_CLASS)
+@QuarkusComponentTest
+public class ParameterInjectionPerClassLifecycleTest {
+
+ static volatile String mySingletonId;
+
+ @Order(1)
+ @Test
+ public void testSingleton1(MySingleton mySingleton) {
+ mySingletonId = mySingleton.id;
+ }
+
+ @Order(2)
+ @Test
+ public void testSingleton2(MySingleton mySingleton) {
+ assertEquals(mySingletonId, mySingleton.id);
+ }
+
+ @Singleton
+ public static class MySingleton {
+
+ String id;
+
+ @PostConstruct
+ void initId() {
+ id = UUID.randomUUID().toString();
+ }
+ }
+
+}
diff --git a/test-framework/junit5-component/src/test/java/io/quarkus/test/component/paraminject/ParameterInjectionPerMethodLifecycleTest.java b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/paraminject/ParameterInjectionPerMethodLifecycleTest.java
new file mode 100644
index 00000000000000..d14287e9578951
--- /dev/null
+++ b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/paraminject/ParameterInjectionPerMethodLifecycleTest.java
@@ -0,0 +1,47 @@
+package io.quarkus.test.component.paraminject;
+
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+
+import java.util.UUID;
+
+import jakarta.annotation.PostConstruct;
+import jakarta.inject.Singleton;
+
+import org.junit.jupiter.api.Order;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInstance;
+import org.junit.jupiter.api.TestInstance.Lifecycle;
+
+import io.quarkus.test.component.QuarkusComponentTest;
+
+@TestInstance(Lifecycle.PER_METHOD)
+@QuarkusComponentTest
+public class ParameterInjectionPerMethodLifecycleTest {
+
+ static volatile String mySingletonId;
+
+ @Order(1)
+ @Test
+ public void testSingleton1(MySingleton mySingleton) {
+ mySingletonId = mySingleton.id;
+ }
+
+ @Order(2)
+ @Test
+ public void testSingleton2(MySingleton mySingleton) {
+ // Each test method starts a different CDI container
+ assertNotEquals(mySingletonId, mySingleton.id);
+ }
+
+ @Singleton
+ public static class MySingleton {
+
+ String id;
+
+ @PostConstruct
+ void initId() {
+ id = UUID.randomUUID().toString();
+ }
+ }
+
+}
diff --git a/test-framework/junit5-component/src/test/java/io/quarkus/test/component/paraminject/ParameterInjectionTest.java b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/paraminject/ParameterInjectionTest.java
new file mode 100644
index 00000000000000..8e81c48b72c00c
--- /dev/null
+++ b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/paraminject/ParameterInjectionTest.java
@@ -0,0 +1,84 @@
+package io.quarkus.test.component.paraminject;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import java.util.List;
+import java.util.function.Supplier;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestInfo;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.junit.jupiter.api.extension.ParameterContext;
+import org.junit.jupiter.api.extension.ParameterResolutionException;
+import org.junit.jupiter.api.extension.ParameterResolver;
+import org.mockito.Mockito;
+
+import io.quarkus.arc.All;
+import io.quarkus.test.InjectMock;
+import io.quarkus.test.component.QuarkusComponentTest;
+import io.quarkus.test.component.SkipInject;
+import io.quarkus.test.component.TestConfigProperty;
+import io.quarkus.test.component.beans.Charlie;
+import io.quarkus.test.component.beans.MyComponent;
+
+@QuarkusComponentTest
+public class ParameterInjectionTest {
+
+ @ExtendWith(MyParamResolver.class)
+ @TestConfigProperty(key = "foo", value = "BAZ")
+ @Test
+ public void testParamsInjection(
+ // TestInfo should be ignored automatically
+ TestInfo testInfo,
+ // MyComponent is automatically a component
+ MyComponent myComponent,
+ // This would be normally resolved by QuarkusComponentTest but is annotated with @SkipInject
+ @SkipInject MyComponent anotherComponent,
+ // Inject unconfigured mock
+ @InjectMock Charlie charlie,
+ // Note that @SkipInject is redundant in this case because the Supplier interface cannot be used as a class-based bean
+ // And so no matching bean exists
+ @SkipInject Supplier