Skip to content

Commit

Permalink
QuarkusComponentTest: refactorings and API changes
Browse files Browse the repository at this point in the history
- determine the test phase in which the container should be started from the
TestInstance lifecycle
- introduce the QuarkusComponentTestExtensionBuilder to create immutable
extension instance when programmatic API is used
- TestConfigProperty can be declared on test methods

Co-authored-by: Ladislav Thon <ladicek@gmail.com>
  • Loading branch information
mkouba and Ladicek committed Sep 6, 2023
1 parent 3dbd75d commit 1a2de65
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.

Check warning on line 1628 in docs/src/main/asciidoc/getting-started-testing.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/getting-started-testing.adoc", "range": {"start": {"line": 1628, "column": 70}}}, "severity": "INFO"}

Check warning on line 1628 in docs/src/main/asciidoc/getting-started-testing.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

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

=== 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.

Check warning on line 1641 in docs/src/main/asciidoc/getting-started-testing.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.TermsWarnings] Consider using 'through', 'by', 'from', 'on', or 'by using' rather than 'via' unless updating existing content that uses the term. Raw Output: {"message": "[Quarkus.TermsWarnings] Consider using 'through', 'by', 'from', 'on', or 'by using' rather than 'via' unless updating existing content that uses the term.", "location": {"path": "docs/src/main/asciidoc/getting-started-testing.adoc", "range": {"start": {"line": 1641, "column": 24}}}, "severity": "WARNING"}

=== Configuration

Check warning on line 1643 in docs/src/main/asciidoc/getting-started-testing.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/getting-started-testing.adoc", "range": {"start": {"line": 1643, "column": 1}}}, "severity": "INFO"}

Check warning on line 1643 in docs/src/main/asciidoc/getting-started-testing.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.Headings] Use sentence-style capitalization in '20.4. Configuration'. Raw Output: {"message": "[Quarkus.Headings] Use sentence-style capitalization in '20.4. Configuration'.", "location": {"path": "docs/src/main/asciidoc/getting-started-testing.adoc", "range": {"start": {"line": 1643, "column": 1}}}, "severity": "INFO"}

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.

Check warning on line 1645 in docs/src/main/asciidoc/getting-started-testing.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.Fluff] Depending on the context, consider using 'Rewrite the sentence, or use 'must', instead of' rather than 'need to'. Raw Output: {"message": "[Quarkus.Fluff] Depending on the context, consider using 'Rewrite the sentence, or use 'must', instead of' rather than 'need to'.", "location": {"path": "docs/src/main/asciidoc/getting-started-testing.adoc", "range": {"start": {"line": 1645, "column": 208}}}, "severity": "INFO"}
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.

Check warning on line 1649 in docs/src/main/asciidoc/getting-started-testing.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.Headings] Use sentence-style capitalization in '20.5. Mocking CDI Interceptors'. Raw Output: {"message": "[Quarkus.Headings] Use sentence-style capitalization in '20.5. Mocking CDI Interceptors'.", "location": {"path": "docs/src/main/asciidoc/getting-started-testing.adoc", "range": {"start": {"line": 1649, "column": 136}}}, "severity": "INFO"}

Check warning on line 1649 in docs/src/main/asciidoc/getting-started-testing.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/getting-started-testing.adoc", "range": {"start": {"line": 1649, "column": 136}}}, "severity": "INFO"}

=== 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 1a2de65

Please sign in to comment.