Skip to content

Commit

Permalink
QuarkusComponentTest: programmatic lookup improvements
Browse files Browse the repository at this point in the history
- handle programmatic lookup injection points specifically: register
type arguments as tested components and consider these types in the
unused beans removal exclusion
- support programmatic lookup in test method params (`Instance<>`, `@All
List<>`)
- add "Tested components" section in the docs
- related to quarkusio#42643
  • Loading branch information
mkouba committed Aug 28, 2024
1 parent 58c2eb5 commit 04a0211
Show file tree
Hide file tree
Showing 15 changed files with 395 additions and 66 deletions.
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,6 +177,19 @@ Dependent beans injected into the fields and test method arguments are correctly

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.
2. The types of test methods parameters that are not annotated with `@InjectMock`, `@SkipInject`, or `@org.mockito.Mock` are also considered the component types.
3. If `@QuarkusComponentTest#addNestedClassesAsComponents()` is set to `true` (default) then all static nested classes declared on the test class are components too.

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.

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


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

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

0 comments on commit 04a0211

Please sign in to comment.