Skip to content

Commit

Permalink
Merge pull request #35768 from mkouba/qct-per-method-lifecycle-support
Browse files Browse the repository at this point in the history
QuarkusComponentTest: refactorings and API changes
  • Loading branch information
mkouba committed Sep 7, 2023
2 parents 86d200e + 1a2de65 commit 2c25d2f
Show file tree
Hide file tree
Showing 21 changed files with 712 additions and 327 deletions.
19 changes: 11 additions & 8 deletions docs/src/main/asciidoc/getting-started-testing.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -1601,7 +1601,7 @@ import org.mockito.Mockito;
public class FooTest {
@RegisterExtension <1>
static final QuarkusComponentTestExtension extension = new QuarkusComponentTestExtension().configProperty("bar","true");
static final QuarkusComponentTestExtension extension = QuarkusComponentTestExtension.builder().configProperty("bar","true").build();
@Inject
Foo foo;
Expand All @@ -1621,9 +1621,10 @@ public class FooTest {
=== Lifecycle

So what exactly does the `QuarkusComponentTest` do?
It starts the CDI container and registers a dedicated xref:config-reference.adoc[configuration object] during the `before all` test phase.
The container is stopped and the config is released during the `after all` test phase.
The fields annotated with `@Inject` and `@InjectMock` are injected after a test instance is created and unset before a test instance is destroyed.
It starts the CDI container and registers a dedicated xref:config-reference.adoc[configuration object].
If the test instance lifecycle is `Lifecycle#PER_METHOD` (default) then the container is started during the `before each` test phase and stopped during the `after each` test phase.
However, if the test instance lifecycle is `Lifecycle#PER_CLASS` then the container is started during the `before all` test phase and stopped during the `after all` test phase.
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.

=== Auto Mocking Unsatisfied Dependencies
Expand All @@ -1637,13 +1638,15 @@ You can inject the mock in your test and leverage the Mockito API to configure t
=== 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 `QuarkusComponentTestExtension#mock()` method.
You can use the mock configurator API via the `QuarkusComponentTestExtensionBuilder#mock()` method.

=== Configuration

A dedicated `SmallRyeConfig` is registered during the `before all` test phase.
Moreover, it's possible to set the configuration properties via the `QuarkusComponentTestExtension#configProperty(String, String)` method or the `@TestConfigProperty` annotation.
If you only need to use the default values for missing config properties, then the `QuarkusComponentTestExtension#useDefaultConfigProperties()` or `@QuarkusComponentTest#useDefaultConfigProperties()` might come in useful.
You can set the configuration properties for a test with the `@io.quarkus.test.component.TestConfigProperty` annotation or with the `QuarkusComponentTestExtensionBuilder#configProperty(String, String)` method.
If you only need to use the default values for missing config properties, then the `@QuarkusComponentTest#useDefaultConfigProperties()` or `QuarkusComponentTestExtensionBuilder#useDefaultConfigProperties()` might come in useful.

It is also possible to set configuration properties for a test method with the `@io.quarkus.test.component.TestConfigProperty` annotation.
However, if the test instance lifecycle is `Lifecycle#_PER_CLASS` this annotation can only be used on the test class and is ignored on test methods.

=== Mocking CDI Interceptors

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,20 +36,20 @@ public interface MockBeanConfigurator<T> {
* @param create
* @return the test extension
*/
QuarkusComponentTestExtension create(Function<SyntheticCreationalContext<T>, T> create);
QuarkusComponentTestExtensionBuilder create(Function<SyntheticCreationalContext<T>, T> create);

/**
* A Mockito mock object created from the bean class is used as a bean instance.
*
* @return the test extension
*/
QuarkusComponentTestExtension createMockitoMock();
QuarkusComponentTestExtensionBuilder createMockitoMock();

/**
* A Mockito mock object created from the bean class is used as a bean instance.
*
* @return the test extension
*/
QuarkusComponentTestExtension createMockitoMock(Consumer<T> mockInitializer);
QuarkusComponentTestExtensionBuilder createMockitoMock(Consumer<T> mockInitializer);

}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@

class MockBeanConfiguratorImpl<T> implements MockBeanConfigurator<T> {

final QuarkusComponentTestExtension test;
final QuarkusComponentTestExtensionBuilder builder;
final Class<?> beanClass;
Set<Type> types;
Set<Annotation> qualifiers;
Expand All @@ -44,8 +44,8 @@ class MockBeanConfiguratorImpl<T> implements MockBeanConfigurator<T> {
Set<org.jboss.jandex.Type> jandexTypes;
Set<AnnotationInstance> jandexQualifiers;

public MockBeanConfiguratorImpl(QuarkusComponentTestExtension test, Class<?> beanClass) {
this.test = test;
public MockBeanConfiguratorImpl(QuarkusComponentTestExtensionBuilder builder, Class<?> beanClass) {
this.builder = builder;
this.beanClass = beanClass;
this.types = new HierarchyDiscovery(beanClass).getTypeClosure();

Expand Down Expand Up @@ -142,19 +142,19 @@ public MockBeanConfigurator<T> defaultBean(boolean defaultBean) {
}

@Override
public QuarkusComponentTestExtension create(Function<SyntheticCreationalContext<T>, T> create) {
public QuarkusComponentTestExtensionBuilder create(Function<SyntheticCreationalContext<T>, T> create) {
this.create = create;
return register();
}

@Override
public QuarkusComponentTestExtension createMockitoMock() {
public QuarkusComponentTestExtensionBuilder createMockitoMock() {
this.create = c -> QuarkusComponentTestExtension.cast(Mockito.mock(beanClass));
return register();
}

@Override
public QuarkusComponentTestExtension createMockitoMock(Consumer<T> mockInitializer) {
public QuarkusComponentTestExtensionBuilder createMockitoMock(Consumer<T> mockInitializer) {
this.create = c -> {
T mock = QuarkusComponentTestExtension.cast(Mockito.mock(beanClass));
mockInitializer.accept(mock);
Expand All @@ -163,9 +163,9 @@ public QuarkusComponentTestExtension createMockitoMock(Consumer<T> mockInitializ
return register();
}

public QuarkusComponentTestExtension register() {
test.registerMockBean(this);
return test;
public QuarkusComponentTestExtensionBuilder register() {
builder.registerMockBean(this);
return builder;
}

boolean matches(BeanResolver beanResolver, org.jboss.jandex.Type requiredType, Set<AnnotationInstance> qualifiers) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
* <p>
* For primitives the default values as defined in the JLS are used. For any other type {@code null} is injected.
*
* @see QuarkusComponentTestExtension#useDefaultConfigProperties()
* @see QuarkusComponentTestExtensionBuilder#useDefaultConfigProperties()
*/
boolean useDefaultConfigProperties() default false;

Expand All @@ -55,17 +55,17 @@
/**
* The ordinal of the config source used for all test config properties.
*
* @see QuarkusComponentTestExtension#setConfigSourceOrdinal(int)
* @see QuarkusComponentTestExtensionBuilder#setConfigSourceOrdinal(int)
*/
int configSourceOrdinal() default QuarkusComponentTestExtension.DEFAULT_CONFIG_SOURCE_ORDINAL;
int configSourceOrdinal() default QuarkusComponentTestExtensionBuilder.DEFAULT_CONFIG_SOURCE_ORDINAL;

/**
* The additional annotation transformers.
* <p>
* The initial set includes the {@link JaxrsSingletonTransformer}.
*
* @see AnnotationsTransformer
* @see QuarkusComponentTestExtension#addAnnotationsTransformer(AnnotationsTransformer)
* @see QuarkusComponentTestExtensionBuilder#addAnnotationsTransformer(AnnotationsTransformer)
*/
Class<? extends AnnotationsTransformer>[] annotationsTransformers() default {};

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

import java.util.Map;
import java.util.Set;

import org.eclipse.microprofile.config.spi.ConfigSource;

class QuarkusComponentTestConfigSource implements ConfigSource {

private final Map<String, String> configProperties;
private final int ordinal;

QuarkusComponentTestConfigSource(Map<String, String> configProperties, int ordinal) {
this.configProperties = configProperties;
this.ordinal = ordinal;
}

@Override
public Set<String> 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() {
return ordinal;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
package io.quarkus.test.component;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import jakarta.enterprise.event.Event;
import jakarta.enterprise.inject.Instance;
import jakarta.enterprise.inject.spi.BeanContainer;
import jakarta.enterprise.inject.spi.BeanManager;
import jakarta.inject.Inject;
import jakarta.inject.Provider;

import org.jboss.logging.Logger;

import io.quarkus.arc.InjectableInstance;
import io.quarkus.arc.processor.AnnotationsTransformer;

class QuarkusComponentTestConfiguration {

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

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

final Map<String, String> configProperties;
final List<Class<?>> componentClasses;
final List<MockBeanConfiguratorImpl<?>> mockConfigurators;
final boolean useDefaultConfigProperties;
final boolean addNestedClassesAsComponents;
final int configSourceOrdinal;
final List<AnnotationsTransformer> annotationsTransformers;

QuarkusComponentTestConfiguration(Map<String, String> configProperties, List<Class<?>> componentClasses,
List<MockBeanConfiguratorImpl<?>> mockConfigurators, boolean useDefaultConfigProperties,
boolean addNestedClassesAsComponents, int configSourceOrdinal,
List<AnnotationsTransformer> annotationsTransformers) {
this.configProperties = configProperties;
this.componentClasses = componentClasses;
this.mockConfigurators = mockConfigurators;
this.useDefaultConfigProperties = useDefaultConfigProperties;
this.addNestedClassesAsComponents = addNestedClassesAsComponents;
this.configSourceOrdinal = configSourceOrdinal;
this.annotationsTransformers = annotationsTransformers;
}

QuarkusComponentTestConfiguration update(Class<?> testClass) {
Map<String, String> configProperties = new HashMap<>(this.configProperties);
List<Class<?>> componentClasses = new ArrayList<>(this.componentClasses);
boolean useDefaultConfigProperties = this.useDefaultConfigProperties;
boolean addNestedClassesAsComponents = this.addNestedClassesAsComponents;
int configSourceOrdinal = this.configSourceOrdinal;
List<AnnotationsTransformer> annotationsTransformers = new ArrayList<>(this.annotationsTransformers);

QuarkusComponentTest testAnnotation = testClass.getAnnotation(QuarkusComponentTest.class);
if (testAnnotation != null) {
Collections.addAll(componentClasses, testAnnotation.value());
useDefaultConfigProperties = testAnnotation.useDefaultConfigProperties();
addNestedClassesAsComponents = testAnnotation.addNestedClassesAsComponents();
configSourceOrdinal = testAnnotation.configSourceOrdinal();
Class<? extends AnnotationsTransformer>[] transformers = testAnnotation.annotationsTransformers();
if (transformers.length > 0) {
for (Class<? extends AnnotationsTransformer> transformerClass : transformers) {
try {
annotationsTransformers.add(transformerClass.getDeclaredConstructor().newInstance());
} catch (Exception e) {
LOG.errorf("Unable to instantiate %s", transformerClass);
}
}
}
}
// All fields annotated with @Inject represent component classes
Class<?> current = testClass;
while (current != null) {
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);
}
}
}

List<TestConfigProperty> testConfigProperties = new ArrayList<>();
Collections.addAll(testConfigProperties, testClass.getAnnotationsByType(TestConfigProperty.class));
for (TestConfigProperty testConfigProperty : testConfigProperties) {
configProperties.put(testConfigProperty.key(), testConfigProperty.value());
}

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

QuarkusComponentTestConfiguration update(Method testMethod) {
Map<String, String> configProperties = new HashMap<>(this.configProperties);
List<TestConfigProperty> testConfigProperties = new ArrayList<>();
Collections.addAll(testConfigProperties, testMethod.getAnnotationsByType(TestConfigProperty.class));
for (TestConfigProperty testConfigProperty : testConfigProperties) {
configProperties.put(testConfigProperty.key(), testConfigProperty.value());
}
return new QuarkusComponentTestConfiguration(configProperties, componentClasses,
mockConfigurators, useDefaultConfigProperties, addNestedClassesAsComponents, configSourceOrdinal,
annotationsTransformers);
}

private static boolean resolvesToBuiltinBean(Class<?> rawType) {
return Provider.class.equals(rawType)
|| Instance.class.equals(rawType)
|| InjectableInstance.class.equals(rawType)
|| Event.class.equals(rawType)
|| BeanContainer.class.equals(rawType)
|| BeanManager.class.equals(rawType);
}

}
Loading

0 comments on commit 2c25d2f

Please sign in to comment.