Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

QuarkusComponentTest: programmatic lookup improvements #42779

Merged
merged 1 commit into from
Aug 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions docs/src/main/asciidoc/testing-components.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -177,8 +177,21 @@

NOTE: Arguments of a `@ParameterizedTest` method that are provided by an `ArgumentsProvider`, for example with `@org.junit.jupiter.params.provider.ValueArgumentsProvider`, must be annotated with `@SkipInject`.

=== Tested components

The initial set of tested components is derived from the test class:

1. The types of all fields annotated with `@jakarta.inject.Inject` are considered the component types.

Check warning on line 184 in docs/src/main/asciidoc/testing-components.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.HeadingPunctuation] Do not use end punctuation in headings. Raw Output: {"message": "[Quarkus.HeadingPunctuation] Do not use end punctuation in headings.", "location": {"path": "docs/src/main/asciidoc/testing-components.adoc", "range": {"start": {"line": 184, "column": 1}}}, "severity": "INFO"}
2. The types of test methods parameters that are not annotated with `@InjectMock`, `@SkipInject`, or `@org.mockito.Mock` are also considered the component types.

Check warning on line 185 in docs/src/main/asciidoc/testing-components.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.HeadingPunctuation] Do not use end punctuation in headings. Raw Output: {"message": "[Quarkus.HeadingPunctuation] Do not use end punctuation in headings.", "location": {"path": "docs/src/main/asciidoc/testing-components.adoc", "range": {"start": {"line": 185, "column": 1}}}, "severity": "INFO"}
3. If `@QuarkusComponentTest#addNestedClassesAsComponents()` is set to `true` (default) then all static nested classes declared on the test class are components too.

Check warning on line 186 in docs/src/main/asciidoc/testing-components.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.HeadingPunctuation] Do not use end punctuation in headings. Raw Output: {"message": "[Quarkus.HeadingPunctuation] Do not use end punctuation in headings.", "location": {"path": "docs/src/main/asciidoc/testing-components.adoc", "range": {"start": {"line": 186, "column": 1}}}, "severity": "INFO"}

NOTE: `@Inject Instance<T>` and `@Inject @All List<T>` injection points are handled specifically. The actual type argument is registered as a component. However, if the type argument is an interface the implementations _are not registered_ automatically.

Check warning on line 188 in docs/src/main/asciidoc/testing-components.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.TermsSuggestions] Depending on the context, consider using 'because' or 'while' rather than 'as'. Raw Output: {"message": "[Quarkus.TermsSuggestions] Depending on the context, consider using 'because' or 'while' rather than 'as'.", "location": {"path": "docs/src/main/asciidoc/testing-components.adoc", "range": {"start": {"line": 188, "column": 138}}}, "severity": "INFO"}

Additional component classes can be set using `@QuarkusComponentTest#value()` or `QuarkusComponentTestExtensionBuilder#addComponentClasses()`.


[[auto_mocking]]
=== Auto Mocking Unsatisfied Dependencies

Check warning on line 194 in docs/src/main/asciidoc/testing-components.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.Headings] Use sentence-style capitalization in '3.2. Auto Mocking Unsatisfied Dependencies'. Raw Output: {"message": "[Quarkus.Headings] Use sentence-style capitalization in '3.2. Auto Mocking Unsatisfied Dependencies'.", "location": {"path": "docs/src/main/asciidoc/testing-components.adoc", "range": {"start": {"line": 194, "column": 1}}}, "severity": "INFO"}

Check warning on line 194 in docs/src/main/asciidoc/testing-components.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.HeadingPunctuation] Do not use end punctuation in headings. Raw Output: {"message": "[Quarkus.HeadingPunctuation] Do not use end punctuation in headings.", "location": {"path": "docs/src/main/asciidoc/testing-components.adoc", "range": {"start": {"line": 194, "column": 1}}}, "severity": "INFO"}

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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ public T get() {
return new Guard<>(result);
}

static <T> InstanceImpl<T> forGlobalEntrypoint(Type requiredType, Set<Annotation> requiredQualifiers) {
public static <T> InstanceImpl<T> forGlobalEntrypoint(Type requiredType, Set<Annotation> requiredQualifiers) {
return new InstanceImpl<>(new CreationalContextImpl<>(null), requiredType, requiredQualifiers,
null, null, Collections.emptySet(), null, -1, false, true);
}
Expand Down Expand Up @@ -309,7 +309,7 @@ private T getInternal() {
return getBeanInstance(bean());
}

void destroy() {
public void destroy() {
creationalContext.release();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,21 @@
package io.quarkus.test.component;

import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Parameter;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.lang.reflect.WildcardType;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;

import jakarta.enterprise.event.Event;
Expand All @@ -20,6 +27,7 @@

import org.eclipse.microprofile.config.spi.Converter;
import org.jboss.logging.Logger;
import org.mockito.Mock;

import io.quarkus.arc.InjectableInstance;
import io.quarkus.arc.processor.AnnotationsTransformer;
Expand Down Expand Up @@ -52,14 +60,14 @@ class QuarkusComponentTestConfiguration {
new ZoneIdConverter(),
new LevelConverter());

static final QuarkusComponentTestConfiguration DEFAULT = new QuarkusComponentTestConfiguration(Map.of(), List.of(),
static final QuarkusComponentTestConfiguration DEFAULT = new QuarkusComponentTestConfiguration(Map.of(), Set.of(),
List.of(), false, true, QuarkusComponentTestExtensionBuilder.DEFAULT_CONFIG_SOURCE_ORDINAL, List.of(),
DEFAULT_CONVERTERS, null);

private static final Logger LOG = Logger.getLogger(QuarkusComponentTestConfiguration.class);

final Map<String, String> configProperties;
final List<Class<?>> componentClasses;
final Set<Class<?>> componentClasses;
final List<MockBeanConfiguratorImpl<?>> mockConfigurators;
final boolean useDefaultConfigProperties;
final boolean addNestedClassesAsComponents;
Expand All @@ -68,7 +76,7 @@ class QuarkusComponentTestConfiguration {
final List<Converter<?>> configConverters;
final Consumer<SmallRyeConfigBuilder> configBuilderCustomizer;

QuarkusComponentTestConfiguration(Map<String, String> configProperties, List<Class<?>> componentClasses,
QuarkusComponentTestConfiguration(Map<String, String> configProperties, Set<Class<?>> componentClasses,
List<MockBeanConfiguratorImpl<?>> mockConfigurators, boolean useDefaultConfigProperties,
boolean addNestedClassesAsComponents, int configSourceOrdinal,
List<AnnotationsTransformer> annotationsTransformers, List<Converter<?>> configConverters,
Expand Down Expand Up @@ -124,8 +132,18 @@ QuarkusComponentTestConfiguration update(Class<?> testClass) {
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());
if (field.isAnnotationPresent(Inject.class)) {
if (Instance.class.isAssignableFrom(field.getType())
|| QuarkusComponentTestExtension.isListAllInjectionPoint(field.getGenericType(),
field.getAnnotations(),
field)) {
// Special handling for Instance<Foo> and @All List<Foo>
componentClasses
.add(getRawType(
QuarkusComponentTestExtension.getFirstActualTypeArgument(field.getGenericType())));
} else if (!resolvesToBuiltinBean(field.getType())) {
componentClasses.add(field.getType());
}
}
}
// All static nested classes declared on the test class are components
Expand All @@ -138,17 +156,26 @@ QuarkusComponentTestConfiguration update(Class<?> testClass) {
}
// All params of test methods but:
// - not covered by built-in extensions
// - not annotated with @InjectMock
// - not annotated with @SkipInject
// - not annotated with @InjectMock, @SkipInject, @org.mockito.Mock
for (Method method : current.getDeclaredMethods()) {
if (QuarkusComponentTestExtension.isTestMethod(method)) {
for (Parameter param : method.getParameters()) {
if (QuarkusComponentTestExtension.BUILTIN_PARAMETER.test(param)
|| param.isAnnotationPresent(InjectMock.class)
|| param.isAnnotationPresent(SkipInject.class)) {
|| param.isAnnotationPresent(SkipInject.class)
|| param.isAnnotationPresent(Mock.class)) {
continue;
}
componentClasses.add(param.getType());
if (Instance.class.isAssignableFrom(param.getType())
|| QuarkusComponentTestExtension.isListAllInjectionPoint(param.getParameterizedType(),
param.getAnnotations(),
param)) {
// Special handling for Instance<Foo> and @All List<Foo>
componentClasses.add(getRawType(
QuarkusComponentTestExtension.getFirstActualTypeArgument(param.getParameterizedType())));
} else {
componentClasses.add(param.getType());
}
}
}
}
Expand All @@ -161,9 +188,8 @@ QuarkusComponentTestConfiguration update(Class<?> testClass) {
configProperties.put(testConfigProperty.key(), testConfigProperty.value());
}

return new QuarkusComponentTestConfiguration(Map.copyOf(configProperties), List.copyOf(componentClasses),
this.mockConfigurators,
useDefaultConfigProperties, addNestedClassesAsComponents, configSourceOrdinal,
return new QuarkusComponentTestConfiguration(Map.copyOf(configProperties), Set.copyOf(componentClasses),
this.mockConfigurators, useDefaultConfigProperties, addNestedClassesAsComponents, configSourceOrdinal,
List.copyOf(annotationsTransformers), List.copyOf(configConverters), configBuilderCustomizer);
}

Expand All @@ -188,4 +214,43 @@ private static boolean resolvesToBuiltinBean(Class<?> rawType) {
|| BeanManager.class.equals(rawType);
}

@SuppressWarnings("unchecked")
static <T> Class<T> getRawType(Type type) {
if (type instanceof Class<?>) {
return (Class<T>) type;
}
if (type instanceof ParameterizedType) {
final ParameterizedType parameterizedType = (ParameterizedType) type;
if (parameterizedType.getRawType() instanceof Class<?>) {
return (Class<T>) parameterizedType.getRawType();
}
}
if (type instanceof TypeVariable<?>) {
TypeVariable<?> variable = (TypeVariable<?>) type;
Type[] bounds = variable.getBounds();
return getBound(bounds);
}
if (type instanceof WildcardType) {
WildcardType wildcard = (WildcardType) type;
return getBound(wildcard.getUpperBounds());
}
if (type instanceof GenericArrayType) {
GenericArrayType genericArrayType = (GenericArrayType) type;
Class<?> rawType = getRawType(genericArrayType.getGenericComponentType());
if (rawType != null) {
return (Class<T>) Array.newInstance(rawType, 0).getClass();
}
}
return null;
}

@SuppressWarnings("unchecked")
private static <T> Class<T> getBound(Type[] bounds) {
if (bounds.length == 0) {
return (Class<T>) Object.class;
} else {
return getRawType(bounds[0]);
}
}

}
Loading
Loading