From 1a2de6596cfa040f0403e6ea245d21864513d5ea Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Wed, 6 Sep 2023 10:59:21 +0200 Subject: [PATCH] QuarkusComponentTest: refactorings and API changes - 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 --- .../asciidoc/getting-started-testing.adoc | 19 +- .../test/component/MockBeanConfigurator.java | 6 +- .../component/MockBeanConfiguratorImpl.java | 18 +- .../test/component/QuarkusComponentTest.java | 8 +- .../QuarkusComponentTestConfigSource.java | 38 ++ .../QuarkusComponentTestConfiguration.java | 129 ++++++ .../QuarkusComponentTestExtension.java | 413 ++++++------------ .../QuarkusComponentTestExtensionBuilder.java | 137 ++++++ .../test/component/TestConfigProperty.java | 17 +- .../component/AnnotationsTransformerTest.java | 5 +- ...ApplicationPropertiesConfigSourceTest.java | 5 +- .../test/component/DependencyMockingTest.java | 6 +- .../test/component/MockConfiguratorTest.java | 5 +- .../MockNotSharedForClassHierarchyTest.java | 5 +- .../MockSharedForClassHierarchyTest.java | 7 +- .../component/ObserverInjectingMockTest.java | 6 +- .../UnsetConfigurationPropertiesTest.java | 5 +- ...rtyDeclaredOnMethodOverridesClassTest.java | 33 ++ .../ConfigPropertyDeclaredOnMethodTest.java | 39 ++ .../lifecycle/PerClassLifecycleTest.java | 69 +++ .../lifecycle/PerMethodLifecycleTest.java | 69 +++ 21 files changed, 712 insertions(+), 327 deletions(-) create mode 100644 test-framework/junit5-component/src/main/java/io/quarkus/test/component/QuarkusComponentTestConfigSource.java create mode 100644 test-framework/junit5-component/src/main/java/io/quarkus/test/component/QuarkusComponentTestConfiguration.java create mode 100644 test-framework/junit5-component/src/main/java/io/quarkus/test/component/QuarkusComponentTestExtensionBuilder.java create mode 100644 test-framework/junit5-component/src/test/java/io/quarkus/test/component/declarative/ConfigPropertyDeclaredOnMethodOverridesClassTest.java create mode 100644 test-framework/junit5-component/src/test/java/io/quarkus/test/component/declarative/ConfigPropertyDeclaredOnMethodTest.java create mode 100644 test-framework/junit5-component/src/test/java/io/quarkus/test/component/lifecycle/PerClassLifecycleTest.java create mode 100644 test-framework/junit5-component/src/test/java/io/quarkus/test/component/lifecycle/PerMethodLifecycleTest.java diff --git a/docs/src/main/asciidoc/getting-started-testing.adoc b/docs/src/main/asciidoc/getting-started-testing.adoc index 244714a058e15..f2536c3b73ec8 100644 --- a/docs/src/main/asciidoc/getting-started-testing.adoc +++ b/docs/src/main/asciidoc/getting-started-testing.adoc @@ -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; @@ -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 @@ -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 diff --git a/test-framework/junit5-component/src/main/java/io/quarkus/test/component/MockBeanConfigurator.java b/test-framework/junit5-component/src/main/java/io/quarkus/test/component/MockBeanConfigurator.java index c2f3850bfd31c..f988b5ab37e0a 100644 --- a/test-framework/junit5-component/src/main/java/io/quarkus/test/component/MockBeanConfigurator.java +++ b/test-framework/junit5-component/src/main/java/io/quarkus/test/component/MockBeanConfigurator.java @@ -36,20 +36,20 @@ public interface MockBeanConfigurator { * @param create * @return the test extension */ - QuarkusComponentTestExtension create(Function, T> create); + QuarkusComponentTestExtensionBuilder create(Function, 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 mockInitializer); + QuarkusComponentTestExtensionBuilder createMockitoMock(Consumer mockInitializer); } \ No newline at end of file diff --git a/test-framework/junit5-component/src/main/java/io/quarkus/test/component/MockBeanConfiguratorImpl.java b/test-framework/junit5-component/src/main/java/io/quarkus/test/component/MockBeanConfiguratorImpl.java index 38191081d5b9a..96096d8c7666f 100644 --- a/test-framework/junit5-component/src/main/java/io/quarkus/test/component/MockBeanConfiguratorImpl.java +++ b/test-framework/junit5-component/src/main/java/io/quarkus/test/component/MockBeanConfiguratorImpl.java @@ -29,7 +29,7 @@ class MockBeanConfiguratorImpl implements MockBeanConfigurator { - final QuarkusComponentTestExtension test; + final QuarkusComponentTestExtensionBuilder builder; final Class beanClass; Set types; Set qualifiers; @@ -44,8 +44,8 @@ class MockBeanConfiguratorImpl implements MockBeanConfigurator { Set jandexTypes; Set 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(); @@ -142,19 +142,19 @@ public MockBeanConfigurator defaultBean(boolean defaultBean) { } @Override - public QuarkusComponentTestExtension create(Function, T> create) { + public QuarkusComponentTestExtensionBuilder create(Function, 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 mockInitializer) { + public QuarkusComponentTestExtensionBuilder createMockitoMock(Consumer mockInitializer) { this.create = c -> { T mock = QuarkusComponentTestExtension.cast(Mockito.mock(beanClass)); mockInitializer.accept(mock); @@ -163,9 +163,9 @@ public QuarkusComponentTestExtension createMockitoMock(Consumer 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 qualifiers) { 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 8a9b0799f3ab8..8b5f82d9d5ddb 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 @@ -41,7 +41,7 @@ *

* 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; @@ -55,9 +55,9 @@ /** * 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. @@ -65,7 +65,7 @@ * The initial set includes the {@link JaxrsSingletonTransformer}. * * @see AnnotationsTransformer - * @see QuarkusComponentTestExtension#addAnnotationsTransformer(AnnotationsTransformer) + * @see QuarkusComponentTestExtensionBuilder#addAnnotationsTransformer(AnnotationsTransformer) */ Class[] annotationsTransformers() default {}; diff --git a/test-framework/junit5-component/src/main/java/io/quarkus/test/component/QuarkusComponentTestConfigSource.java b/test-framework/junit5-component/src/main/java/io/quarkus/test/component/QuarkusComponentTestConfigSource.java new file mode 100644 index 0000000000000..f71cc46be1670 --- /dev/null +++ b/test-framework/junit5-component/src/main/java/io/quarkus/test/component/QuarkusComponentTestConfigSource.java @@ -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 configProperties; + private final int ordinal; + + QuarkusComponentTestConfigSource(Map configProperties, int ordinal) { + this.configProperties = configProperties; + this.ordinal = ordinal; + } + + @Override + public Set 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; + } + +} 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 new file mode 100644 index 0000000000000..8d6d624903b2b --- /dev/null +++ b/test-framework/junit5-component/src/main/java/io/quarkus/test/component/QuarkusComponentTestConfiguration.java @@ -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 configProperties; + final List> componentClasses; + final List> mockConfigurators; + final boolean useDefaultConfigProperties; + final boolean addNestedClassesAsComponents; + final int configSourceOrdinal; + final List annotationsTransformers; + + QuarkusComponentTestConfiguration(Map configProperties, List> componentClasses, + List> mockConfigurators, boolean useDefaultConfigProperties, + boolean addNestedClassesAsComponents, int configSourceOrdinal, + List 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 configProperties = new HashMap<>(this.configProperties); + List> componentClasses = new ArrayList<>(this.componentClasses); + boolean useDefaultConfigProperties = this.useDefaultConfigProperties; + boolean addNestedClassesAsComponents = this.addNestedClassesAsComponents; + int configSourceOrdinal = this.configSourceOrdinal; + List 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[] transformers = testAnnotation.annotationsTransformers(); + if (transformers.length > 0) { + for (Class 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 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 configProperties = new HashMap<>(this.configProperties); + List 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); + } + +} 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 3aa420659512b..1b34ebcd78b81 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 @@ -20,25 +20,21 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Consumer; -import java.util.function.Function; 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.event.Event; -import jakarta.enterprise.inject.Instance; import jakarta.enterprise.inject.spi.BeanManager; import jakarta.enterprise.inject.spi.InjectionPoint; import jakarta.enterprise.inject.spi.InterceptionType; @@ -51,7 +47,6 @@ import org.eclipse.microprofile.config.Config; import org.eclipse.microprofile.config.inject.ConfigProperty; import org.eclipse.microprofile.config.spi.ConfigProviderResolver; -import org.eclipse.microprofile.config.spi.ConfigSource; import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.ClassInfo; import org.jboss.jandex.ClassType; @@ -61,13 +56,14 @@ import org.jboss.jandex.Type; import org.jboss.jandex.Type.Kind; import org.jboss.logging.Logger; +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.RegisterExtension; import org.junit.jupiter.api.extension.TestInstancePostProcessor; -import org.junit.jupiter.api.extension.TestInstancePreDestroyCallback; import io.quarkus.arc.All; import io.quarkus.arc.Arc; @@ -104,16 +100,18 @@ import io.smallrye.config.SmallRyeConfigProviderResolver; /** - * JUnit extension that makes it easy to test Quarkus components, aka the CDI beans. - * - *

Lifecycle

+ * Makes it easy to test Quarkus components. This extension can be registered declaratively with {@link QuarkusComponentTest} or + * 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}. *

- * The CDI container is started and a dedicated SmallRyeConfig is registered during the {@code before all} test phase. The - * container is stopped and the config is released 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. + * 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. * *

Auto Mocking Unsatisfied Dependencies

*

@@ -126,29 +124,18 @@ *

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 {@link #mock(Class)} method. - * - *

Configuration

- *

- * A dedicated {@link SmallRyeConfig} is registered during the {@code before all} test phase. Moreover, it's possible to set the - * configuration properties via the {@link #configProperty(String, String)} method. If you only need to use the default values - * for missing config properties, then the {@link #useDefaultConfigProperties()} - * might come in useful. + * the mock configurator API via the {@link QuarkusComponentTestExtensionBuilder#mock(Class)} method. * * @see InjectMock * @see TestConfigProperty */ @Experimental("This feature is experimental and the API may change in the future") public class QuarkusComponentTestExtension - implements BeforeAllCallback, AfterAllCallback, BeforeEachCallback, AfterEachCallback, TestInstancePostProcessor, - TestInstancePreDestroyCallback, ConfigSource { + implements BeforeAllCallback, AfterAllCallback, BeforeEachCallback, AfterEachCallback, TestInstancePostProcessor { - /** - * By default, test config properties take precedence over system properties (400), ENV variables (300) and - * application.properties (250) - * - */ - public static final int DEFAULT_CONFIG_SOURCE_ORDINAL = 500; + public static QuarkusComponentTestExtensionBuilder builder() { + return new QuarkusComponentTestExtensionBuilder(); + } private static final Logger LOG = Logger.getLogger(QuarkusComponentTestExtension.class); @@ -162,23 +149,15 @@ public class QuarkusComponentTestExtension private static final String KEY_INJECTED_FIELDS = "injectedFields"; private static final String KEY_TEST_INSTANCE = "testInstance"; private static final String KEY_CONFIG = "config"; + private static final String KEY_TEST_CLASS_CONFIG = "testClassConfig"; private static final String QUARKUS_TEST_COMPONENT_OUTPUT_DIRECTORY = "quarkus.test.component.output-directory"; - private final Map configProperties; - private final List> additionalComponentClasses; - private final List> mockConfigurators; - private final AtomicBoolean useDefaultConfigProperties = new AtomicBoolean(); - private final AtomicBoolean addNestedClassesAsComponents = new AtomicBoolean(true); - private final AtomicInteger configSourceOrdinal = new AtomicInteger(DEFAULT_CONFIG_SOURCE_ORDINAL); - private final List additionalAnnotationsTransformers; + private final QuarkusComponentTestConfiguration baseConfiguration; // Used for declarative registration public QuarkusComponentTestExtension() { - this.additionalComponentClasses = List.of(); - this.configProperties = new HashMap<>(); - this.mockConfigurators = new ArrayList<>(); - this.additionalAnnotationsTransformers = new ArrayList<>(); + this(QuarkusComponentTestConfiguration.DEFAULT); } /** @@ -188,210 +167,69 @@ public QuarkusComponentTestExtension() { * @param additionalComponentClasses */ public QuarkusComponentTestExtension(Class... additionalComponentClasses) { - this.additionalComponentClasses = List.of(additionalComponentClasses); - this.configProperties = new HashMap<>(); - this.mockConfigurators = new ArrayList<>(); - this.additionalAnnotationsTransformers = new ArrayList<>(); - } - - /** - * Configure a new mock of a bean. - *

- * Note that a mock is created automatically for all unsatisfied dependencies in the test. This API provides full control - * over the bean attributes. The default values are derived from the bean class. - * - * @param beanClass - * @return a new mock bean configurator - * @see MockBeanConfigurator#create(Function) - */ - public MockBeanConfigurator mock(Class beanClass) { - return new MockBeanConfiguratorImpl<>(this, beanClass); - } - - /** - * Set a configuration property for the test. - * - * @param key - * @param value - * @return self - */ - public QuarkusComponentTestExtension configProperty(String key, String value) { - this.configProperties.put(key, value); - return this; + this(new QuarkusComponentTestConfiguration(Map.of(), List.of(additionalComponentClasses), + List.of(), false, true, QuarkusComponentTestExtensionBuilder.DEFAULT_CONFIG_SOURCE_ORDINAL, + List.of())); } - /** - * Use the default values for missing config properties. By default, a missing config property results in a test failure. - *

- * For primitives the default values as defined in the JLS are used. For any other type {@code null} is injected. - * - * @return self - */ - public QuarkusComponentTestExtension useDefaultConfigProperties() { - this.useDefaultConfigProperties.set(true); - return this; + QuarkusComponentTestExtension(QuarkusComponentTestConfiguration baseConfiguration) { + this.baseConfiguration = baseConfiguration; } - /** - * Ignore the static nested classes declared on the test class. - *

- * By default, all static nested classes declared on the test class are added to the set of additional components under - * test. - * - * @return self - */ - public QuarkusComponentTestExtension ignoreNestedClasses() { - this.addNestedClassesAsComponents.set(false); - return this; - } - - /** - * Set the ordinal of the config source used for all test config properties. By default, - * {@value #DEFAULT_CONFIG_SOURCE_ORDINAL} is used. - * - * @param val - * @return self - */ - public QuarkusComponentTestExtension setConfigSourceOrdinal(int val) { - this.configSourceOrdinal.set(val); - return this; + @Override + public void beforeAll(ExtensionContext context) throws Exception { + long start = System.nanoTime(); + buildContainer(context); + startContainer(context, Lifecycle.PER_CLASS); + LOG.debugf("beforeAll: %s ms", TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start)); } - /** - * Add an additional {@link AnnotationsTransformer}. - * - * @param transformer - * @return self - */ - public QuarkusComponentTestExtension addAnnotationsTransformer(AnnotationsTransformer transformer) { - this.additionalAnnotationsTransformers.add(transformer); - return this; + @Override + public void afterAll(ExtensionContext context) throws Exception { + long start = System.nanoTime(); + stopContainer(context, Lifecycle.PER_CLASS); + cleanup(context); + LOG.debugf("afterAll: %s ms", TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start)); } @Override - public void postProcessTestInstance(Object testInstance, ExtensionContext context) throws Exception { + public void beforeEach(ExtensionContext context) throws Exception { long start = System.nanoTime(); - - // Inject test class fields - context.getRoot().getStore(NAMESPACE).put(KEY_INJECTED_FIELDS, - injectFields(context.getRequiredTestClass(), testInstance)); - context.getRoot().getStore(NAMESPACE).put(KEY_TEST_INSTANCE, testInstance); - - LOG.debugf("postProcessTestInstance: %s ms", TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start)); + startContainer(context, Lifecycle.PER_METHOD); + // Activate the request context + Arc.container().requestContext().activate(); + LOG.debugf("beforeEach: %s ms", TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start)); } - @SuppressWarnings("unchecked") @Override - public void preDestroyTestInstance(ExtensionContext context) throws Exception { + public void afterEach(ExtensionContext context) throws Exception { long start = System.nanoTime(); - - for (FieldInjector fieldInjector : (List) context.getRoot().getStore(NAMESPACE) - .get(KEY_INJECTED_FIELDS, List.class)) { - fieldInjector.unset(context.getRequiredTestInstance()); - } - - LOG.debugf("preDestroyTestInstance: %s ms", TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start)); + // Terminate the request context + Arc.container().requestContext().terminate(); + stopContainer(context, Lifecycle.PER_METHOD); + LOG.debugf("afterEach: %s ms", TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start)); } @Override - public void beforeAll(ExtensionContext context) throws Exception { + public void postProcessTestInstance(Object testInstance, ExtensionContext context) throws Exception { long start = System.nanoTime(); + context.getRoot().getStore(NAMESPACE).put(KEY_TEST_INSTANCE, testInstance); + LOG.debugf("postProcessTestInstance: %s ms", TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start)); + } - Class testClass = context.getRequiredTestClass(); - - // Extension may be registered declaratively - Set> componentClasses = new HashSet<>(this.additionalComponentClasses); - QuarkusComponentTest testAnnotation = testClass.getAnnotation(QuarkusComponentTest.class); - if (testAnnotation != null) { - Collections.addAll(componentClasses, testAnnotation.value()); - if (testAnnotation.useDefaultConfigProperties()) { - this.useDefaultConfigProperties.set(true); - } - this.addNestedClassesAsComponents.set(testAnnotation.addNestedClassesAsComponents()); - this.configSourceOrdinal.set(testAnnotation.configSourceOrdinal()); - Class[] transformers = testAnnotation.annotationsTransformers(); - if (transformers.length > 0) { - for (Class transformerClass : transformers) { - try { - this.additionalAnnotationsTransformers.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 (this.addNestedClassesAsComponents.get()) { - for (Class declaredClass : testClass.getDeclaredClasses()) { - if (Modifier.isStatic(declaredClass.getModifiers())) { - componentClasses.add(declaredClass); - } - } - } - - TestConfigProperty[] testConfigProperties = testClass.getAnnotationsByType(TestConfigProperty.class); - for (TestConfigProperty testConfigProperty : testConfigProperties) { - this.configProperties.put(testConfigProperty.key(), testConfigProperty.value()); - } - - ClassLoader oldTccl = initArcContainer(context, componentClasses); + private void buildContainer(ExtensionContext context) { + QuarkusComponentTestConfiguration testClassConfiguration = baseConfiguration + .update(context.getRequiredTestClass()); + context.getRoot().getStore(NAMESPACE).put(KEY_TEST_CLASS_CONFIG, testClassConfiguration); + ClassLoader oldTccl = initArcContainer(context, testClassConfiguration); context.getRoot().getStore(NAMESPACE).put(KEY_OLD_TCCL, oldTccl); - - ConfigProviderResolver oldConfigProviderResolver = ConfigProviderResolver.instance(); - context.getRoot().getStore(NAMESPACE).put(KEY_OLD_CONFIG_PROVIDER_RESOLVER, oldConfigProviderResolver); - - SmallRyeConfigProviderResolver smallRyeConfigProviderResolver = new SmallRyeConfigProviderResolver(); - ConfigProviderResolver.setInstance(smallRyeConfigProviderResolver); - - // TCCL is now the QuarkusComponentTestClassLoader set during initialization - ClassLoader tccl = Thread.currentThread().getContextClassLoader(); - SmallRyeConfig config = new SmallRyeConfigBuilder().forClassLoader(tccl) - .addDefaultInterceptors() - .addDefaultSources() - .withSources(new ApplicationPropertiesConfigSourceLoader.InFileSystem()) - .withSources(new ApplicationPropertiesConfigSourceLoader.InClassPath()) - .withSources(this) - .build(); - smallRyeConfigProviderResolver.registerConfig(config, tccl); - context.getRoot().getStore(NAMESPACE).put(KEY_CONFIG, config); - ConfigBeanCreator.setClassLoader(tccl); - - LOG.debugf("beforeAll: %s ms", TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start)); } - @Override - public void afterAll(ExtensionContext context) throws Exception { - long start = System.nanoTime(); - + @SuppressWarnings("unchecked") + private void cleanup(ExtensionContext context) { ClassLoader oldTccl = context.getRoot().getStore(NAMESPACE).get(KEY_OLD_TCCL, ClassLoader.class); Thread.currentThread().setContextClassLoader(oldTccl); - try { - Arc.shutdown(); - } catch (Exception e) { - LOG.error("An error occured during ArC shutdown: " + e); - } - MockBeanCreator.clear(); - ConfigBeanCreator.clear(); - InterceptorMethodCreator.clear(); - - SmallRyeConfig config = context.getRoot().getStore(NAMESPACE).get(KEY_CONFIG, SmallRyeConfig.class); - ConfigProviderResolver.instance().releaseConfig(config); - ConfigProviderResolver - .setInstance(context.getRoot().getStore(NAMESPACE).get(KEY_OLD_CONFIG_PROVIDER_RESOLVER, - ConfigProviderResolver.class)); - - @SuppressWarnings("unchecked") Set generatedResources = context.getRoot().getStore(NAMESPACE).get(KEY_GENERATED_RESOURCES, Set.class); for (Path path : generatedResources) { try { @@ -401,54 +239,70 @@ public void afterAll(ExtensionContext context) throws Exception { LOG.errorf("Unable to delete the generated resource %s: ", path, e.getMessage()); } } - - LOG.debugf("afterAll: %s ms", TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start)); - } - - @Override - public void beforeEach(ExtensionContext context) throws Exception { - long start = System.nanoTime(); - - // Activate the request context - ArcContainer container = Arc.container(); - container.requestContext().activate(); - - LOG.debugf("beforeEach: %s ms", TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start)); } - @Override - public void afterEach(ExtensionContext context) throws Exception { - long start = System.nanoTime(); - - // Terminate the request context - ArcContainer container = Arc.container(); - container.requestContext().terminate(); - - LOG.debugf("afterEach: %s ms", TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start)); - } - - @Override - public Set getPropertyNames() { - return configProperties.keySet(); - } - - @Override - public String getValue(String propertyName) { - return configProperties.get(propertyName); + @SuppressWarnings("unchecked") + private void stopContainer(ExtensionContext context, Lifecycle testInstanceLifecycle) throws Exception { + if (testInstanceLifecycle.equals(context.getTestInstanceLifecycle().orElse(Lifecycle.PER_METHOD))) { + for (FieldInjector fieldInjector : (List) context.getRoot().getStore(NAMESPACE) + .get(KEY_INJECTED_FIELDS, List.class)) { + fieldInjector.unset(context.getRequiredTestInstance()); + } + try { + Arc.shutdown(); + } catch (Exception e) { + LOG.error("An error occured during ArC shutdown: " + e); + } + MockBeanCreator.clear(); + ConfigBeanCreator.clear(); + InterceptorMethodCreator.clear(); + + SmallRyeConfig config = context.getRoot().getStore(NAMESPACE).get(KEY_CONFIG, SmallRyeConfig.class); + ConfigProviderResolver.instance().releaseConfig(config); + ConfigProviderResolver + .setInstance(context.getRoot().getStore(NAMESPACE).get(KEY_OLD_CONFIG_PROVIDER_RESOLVER, + ConfigProviderResolver.class)); + } } - @Override - public String getName() { - return QuarkusComponentTestExtension.class.getName(); - } + private void startContainer(ExtensionContext context, Lifecycle testInstanceLifecycle) throws Exception { + if (testInstanceLifecycle.equals(context.getTestInstanceLifecycle().orElse(Lifecycle.PER_METHOD))) { + // Init ArC + Arc.initialize(); - @Override - public int getOrdinal() { - return configSourceOrdinal.get(); - } + QuarkusComponentTestConfiguration configuration = context.getRoot().getStore(NAMESPACE) + .get(KEY_TEST_CLASS_CONFIG, QuarkusComponentTestConfiguration.class); + Optional testMethod = context.getTestMethod(); + if (testMethod.isPresent()) { + configuration = configuration.update(testMethod.get()); + } - void registerMockBean(MockBeanConfiguratorImpl mock) { - this.mockConfigurators.add(mock); + ConfigProviderResolver oldConfigProviderResolver = ConfigProviderResolver.instance(); + context.getRoot().getStore(NAMESPACE).put(KEY_OLD_CONFIG_PROVIDER_RESOLVER, oldConfigProviderResolver); + + SmallRyeConfigProviderResolver smallRyeConfigProviderResolver = new SmallRyeConfigProviderResolver(); + ConfigProviderResolver.setInstance(smallRyeConfigProviderResolver); + + // TCCL is now the QuarkusComponentTestClassLoader set during initialization + ClassLoader tccl = Thread.currentThread().getContextClassLoader(); + SmallRyeConfig config = new SmallRyeConfigBuilder().forClassLoader(tccl) + .addDefaultInterceptors() + .addDefaultSources() + .withSources(new ApplicationPropertiesConfigSourceLoader.InFileSystem()) + .withSources(new ApplicationPropertiesConfigSourceLoader.InClassPath()) + .withSources( + new QuarkusComponentTestConfigSource(configuration.configProperties, + configuration.configSourceOrdinal)) + .build(); + smallRyeConfigProviderResolver.registerConfig(config, tccl); + context.getRoot().getStore(NAMESPACE).put(KEY_CONFIG, config); + ConfigBeanCreator.setClassLoader(tccl); + + // Inject fields declated on the test class + Object testInstance = context.getRequiredTestInstance(); + context.getRoot().getStore(NAMESPACE).put(KEY_INJECTED_FIELDS, + injectFields(context.getRequiredTestClass(), testInstance)); + } } private BeanRegistrar registrarForMock(MockBeanConfiguratorImpl mock) { @@ -499,12 +353,12 @@ private static Set getQualifiers(Field field, Collection> componentClasses) { + private ClassLoader initArcContainer(ExtensionContext extensionContext, QuarkusComponentTestConfiguration configuration) { Class testClass = extensionContext.getRequiredTestClass(); // Collect all test class injection points to define a bean removal exclusion List testClassInjectionPoints = findInjectFields(testClass); - if (componentClasses.isEmpty()) { + if (configuration.componentClasses.isEmpty()) { throw new IllegalStateException("No component classes to test"); } @@ -519,7 +373,7 @@ private ClassLoader initArcContainer(ExtensionContext extensionContext, Collecti IndexView index; try { Indexer indexer = new Indexer(); - for (Class componentClass : componentClasses) { + for (Class componentClass : configuration.componentClasses) { // Make sure that component hierarchy and all annotations present are indexed indexComponentClass(indexer, componentClass); } @@ -632,7 +486,7 @@ public void writeResource(Resource resource) throws IOException { .whenContainsNone(DotName.createSimple(Inject.class)).thenTransform(t -> t.add(Inject.class))); builder.addAnnotationTransformer(new JaxrsSingletonTransformer()); - for (AnnotationsTransformer transformer : additionalAnnotationsTransformers) { + for (AnnotationsTransformer transformer : configuration.annotationsTransformers) { builder.addAnnotationTransformer(transformer); } @@ -681,7 +535,8 @@ public void register(RegistrationContext registrationContext) { requiredQualifiers.add(AnnotationInstance.builder(DotNames.DEFAULT).build()); } } - if (isSatisfied(requiredType, requiredQualifiers, injectionPoint, beans, beanDeployment)) { + if (isSatisfied(requiredType, requiredQualifiers, injectionPoint, beans, beanDeployment, + configuration)) { continue; } if (requiredType.kind() == Kind.PRIMITIVE || requiredType.kind() == Kind.ARRAY) { @@ -727,7 +582,7 @@ public void register(RegistrationContext registrationContext) { BeanConfigurator configPropertyConfigurator = registrationContext.configure(Object.class) .identifier("configProperty") .addQualifier(ConfigProperty.class) - .param("useDefaultConfigProperties", useDefaultConfigProperties.get()) + .param("useDefaultConfigProperties", configuration.useDefaultConfigProperties) .addInjectionPoint(ClassType.create(InjectionPoint.class)) .creator(ConfigPropertyBeanCreator.class); for (TypeAndQualifiers configPropertyInjectionPoint : configPropertyInjectionPoints) { @@ -747,7 +602,7 @@ public void register(RegistrationContext registrationContext) { }); // Register mock beans - for (MockBeanConfiguratorImpl mockConfigurator : mockConfigurators) { + for (MockBeanConfiguratorImpl mockConfigurator : configuration.mockConfigurators) { builder.addBeanRegistrar(registrarForMock(mockConfigurator)); } @@ -790,9 +645,6 @@ public void accept(BytecodeTransformer transformer) { null); Thread.currentThread().setContextClassLoader(testClassLoader); - // Now we are ready to initialize Arc - Arc.initialize(); - } catch (Throwable e) { if (e instanceof RuntimeException) { throw (RuntimeException) e; @@ -817,9 +669,7 @@ private void processTestInterceptorMethods(Class testClass, ExtensionContext return ic -> { Object instance = null; if (!Modifier.isStatic(method.getModifiers())) { - // ExtentionContext.getTestInstance() does not work - Object testInstance = extensionContext.getRoot().getStore(NAMESPACE).get(KEY_TEST_INSTANCE, - Object.class); + Object testInstance = extensionContext.getRoot().getStore(NAMESPACE).get(KEY_TEST_INSTANCE); if (testInstance == null) { throw new IllegalStateException("Test instance not available"); } @@ -906,8 +756,7 @@ private void indexAnnotatedElement(Indexer indexer, AnnotatedElement element) th } private boolean isSatisfied(Type requiredType, Set qualifiers, InjectionPointInfo injectionPoint, - Iterable beans, - BeanDeployment beanDeployment) { + Iterable beans, BeanDeployment beanDeployment, QuarkusComponentTestConfiguration configuration) { for (BeanInfo bean : beans) { if (Beans.matches(bean, requiredType, qualifiers)) { LOG.debugf("Injection point %s satisfied by %s", injectionPoint.getTargetInfo(), @@ -915,7 +764,7 @@ private boolean isSatisfied(Type requiredType, Set qualifier return true; } } - for (MockBeanConfiguratorImpl mock : mockConfigurators) { + for (MockBeanConfiguratorImpl mock : configuration.mockConfigurators) { if (mock.matches(beanDeployment.getBeanResolver(), requiredType, qualifiers)) { LOG.debugf("Injection point %s satisfied by %s", injectionPoint.getTargetInfo(), mock); @@ -1085,10 +934,6 @@ private static boolean isTypeArgumentInstanceHandle(java.lang.reflect.Type type) return false; } - private boolean resolvesToBuiltinBean(Class rawType) { - return Instance.class.isAssignableFrom(rawType) || Event.class.equals(rawType) || BeanManager.class.equals(rawType); - } - private File getTestOutputDirectory(Class testClass) { String outputDirectory = System.getProperty(QUARKUS_TEST_COMPONENT_OUTPUT_DIRECTORY); File testOutputDirectory; diff --git a/test-framework/junit5-component/src/main/java/io/quarkus/test/component/QuarkusComponentTestExtensionBuilder.java b/test-framework/junit5-component/src/main/java/io/quarkus/test/component/QuarkusComponentTestExtensionBuilder.java new file mode 100644 index 0000000000000..e0c267e507065 --- /dev/null +++ b/test-framework/junit5-component/src/main/java/io/quarkus/test/component/QuarkusComponentTestExtensionBuilder.java @@ -0,0 +1,137 @@ +package io.quarkus.test.component; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +import io.quarkus.arc.processor.AnnotationsTransformer; + +/** + * Convenient builder for {@link QuarkusComponentTestExtension}. + */ +public class QuarkusComponentTestExtensionBuilder { + + /** + * By default, test config properties take precedence over system properties (400), ENV variables (300) and + * application.properties (250) + * + * @see #setConfigSourceOrdinal(int) + */ + public static final int DEFAULT_CONFIG_SOURCE_ORDINAL = 500; + + private final Map configProperties = new HashMap<>(); + private final List> componentClasses = new ArrayList<>(); + private final List> mockConfigurators = new ArrayList<>(); + private final List annotationsTransformers = new ArrayList<>(); + private boolean useDefaultConfigProperties = false; + private boolean addNestedClassesAsComponents = true; + private int configSourceOrdinal = QuarkusComponentTestExtensionBuilder.DEFAULT_CONFIG_SOURCE_ORDINAL; + + /** + * The initial set of components under test is derived from the test class. The types of all fields annotated with + * {@link jakarta.inject.Inject} are considered the component types. + * + * + * @param componentClasses + * @return self + * @see #ignoreNestedClasses() + */ + public QuarkusComponentTestExtensionBuilder addComponentClasses(Class... componentClasses) { + Collections.addAll(this.componentClasses, componentClasses); + return this; + } + + /** + * Set a configuration property for the test. + * + * @param key + * @param value + * @return self + */ + public QuarkusComponentTestExtensionBuilder configProperty(String key, String value) { + configProperties.put(key, value); + return this; + } + + /** + * Use the default values for missing config properties. By default, a missing config property results in a test + * failure. + *

+ * For primitives the default values as defined in the JLS are used. For any other type {@code null} is injected. + * + * @return self + */ + public QuarkusComponentTestExtensionBuilder useDefaultConfigProperties() { + useDefaultConfigProperties = true; + return this; + } + + /** + * Ignore the static nested classes declared on the test class. + *

+ * By default, all static nested classes declared on the test class are added to the set of additional components under + * test. + * + * @return self + */ + public QuarkusComponentTestExtensionBuilder ignoreNestedClasses() { + addNestedClassesAsComponents = false; + return this; + } + + /** + * Set the ordinal of the config source used for all test config properties. By default, + * {@value #DEFAULT_CONFIG_SOURCE_ORDINAL} is used. + * + * @param val + * @return self + */ + public QuarkusComponentTestExtensionBuilder setConfigSourceOrdinal(int val) { + configSourceOrdinal = val; + return this; + } + + /** + * Add an additional {@link AnnotationsTransformer}. + * + * @param transformer + * @return self + */ + public QuarkusComponentTestExtensionBuilder addAnnotationsTransformer(AnnotationsTransformer transformer) { + annotationsTransformers.add(transformer); + return this; + } + + /** + * Configure a new mock of a bean. + *

+ * Note that a mock is created automatically for all unsatisfied dependencies in the test. This API provides full control + * over the bean attributes. The default values are derived from the bean class. + * + * @param beanClass + * @return a new mock bean configurator + * @see MockBeanConfigurator#create(Function) + */ + public MockBeanConfigurator mock(Class beanClass) { + return new MockBeanConfiguratorImpl<>(this, beanClass); + } + + /** + * + * @return a new extension instance + */ + public QuarkusComponentTestExtension build() { + return new QuarkusComponentTestExtension(new QuarkusComponentTestConfiguration(Map.copyOf(configProperties), + List.copyOf(componentClasses), + List.copyOf(mockConfigurators), useDefaultConfigProperties, addNestedClassesAsComponents, configSourceOrdinal, + List.copyOf(annotationsTransformers))); + } + + void registerMockBean(MockBeanConfiguratorImpl mock) { + this.mockConfigurators.add(mock); + } + +} \ No newline at end of file diff --git a/test-framework/junit5-component/src/main/java/io/quarkus/test/component/TestConfigProperty.java b/test-framework/junit5-component/src/main/java/io/quarkus/test/component/TestConfigProperty.java index 6bba48ac31209..7b7cfeaa8ef28 100644 --- a/test-framework/junit5-component/src/main/java/io/quarkus/test/component/TestConfigProperty.java +++ b/test-framework/junit5-component/src/main/java/io/quarkus/test/component/TestConfigProperty.java @@ -1,5 +1,6 @@ package io.quarkus.test.component; +import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.RetentionPolicy.RUNTIME; @@ -7,16 +8,26 @@ import java.lang.annotation.Retention; import java.lang.annotation.Target; +import org.junit.jupiter.api.TestInstance.Lifecycle; + import io.quarkus.test.component.TestConfigProperty.TestConfigProperties; /** - * Set the value of a configuration property for a {@code io.quarkus.test.component.QuarkusComponentTest}. + * Set the value of a configuration property. + *

+ * If declared on a class then the configuration property is used for all test methods declared on the test class. + *

+ * If declared on a method then the configuration property is only used for that test method. + * If the test instance lifecycle is {@link Lifecycle#_PER_CLASS}, this annotation can only be used on the test class and is + * ignored on test methods. + *

+ * Configuration properties declared on test methods take precedence over the configuration properties declared on test class. * * @see QuarkusComponentTest - * @see QuarkusComponentTestExtension#configProperty(String, String) + * @see QuarkusComponentTestExtensionBuilder#configProperty(String, String) */ @Retention(RUNTIME) -@Target(TYPE) +@Target({ TYPE, METHOD }) @Repeatable(TestConfigProperties.class) public @interface TestConfigProperty { diff --git a/test-framework/junit5-component/src/test/java/io/quarkus/test/component/AnnotationsTransformerTest.java b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/AnnotationsTransformerTest.java index 54f8d591db162..1d674788418bd 100644 --- a/test-framework/junit5-component/src/test/java/io/quarkus/test/component/AnnotationsTransformerTest.java +++ b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/AnnotationsTransformerTest.java @@ -17,11 +17,12 @@ public class AnnotationsTransformerTest { @RegisterExtension - static final QuarkusComponentTestExtension extension = new QuarkusComponentTestExtension() + static final QuarkusComponentTestExtension extension = QuarkusComponentTestExtension.builder() .addAnnotationsTransformer(AnnotationsTransformer.appliedToClass() .whenClass(c -> c.declaredAnnotations().isEmpty() && c.annotationsMap().containsKey(DotName.createSimple(Inject.class))) - .thenTransform(t -> t.add(Singleton.class))); + .thenTransform(t -> t.add(Singleton.class))) + .build(); @Inject NotABean bean; diff --git a/test-framework/junit5-component/src/test/java/io/quarkus/test/component/ApplicationPropertiesConfigSourceTest.java b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/ApplicationPropertiesConfigSourceTest.java index da6b5d8c36fd5..71d57005685dd 100644 --- a/test-framework/junit5-component/src/test/java/io/quarkus/test/component/ApplicationPropertiesConfigSourceTest.java +++ b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/ApplicationPropertiesConfigSourceTest.java @@ -12,8 +12,9 @@ public class ApplicationPropertiesConfigSourceTest { @RegisterExtension - static final QuarkusComponentTestExtension extension = new QuarkusComponentTestExtension() - .configProperty("org.acme.bar", "GRUT"); + static final QuarkusComponentTestExtension extension = QuarkusComponentTestExtension.builder() + .configProperty("org.acme.bar", "GRUT") + .build(); @Inject Component component; diff --git a/test-framework/junit5-component/src/test/java/io/quarkus/test/component/DependencyMockingTest.java b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/DependencyMockingTest.java index a28f305cd93e3..71d4745e8bdd6 100644 --- a/test-framework/junit5-component/src/test/java/io/quarkus/test/component/DependencyMockingTest.java +++ b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/DependencyMockingTest.java @@ -15,9 +15,11 @@ public class DependencyMockingTest { @RegisterExtension - static final QuarkusComponentTestExtension extension = new QuarkusComponentTestExtension(MyComponent.class) + static final QuarkusComponentTestExtension extension = QuarkusComponentTestExtension.builder() + .addComponentClasses(MyComponent.class) // this config property is injected into MyComponent and the value is used in the ping() method - .configProperty("foo", "BAR"); + .configProperty("foo", "BAR") + .build(); @Inject MyComponent myComponent; diff --git a/test-framework/junit5-component/src/test/java/io/quarkus/test/component/MockConfiguratorTest.java b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/MockConfiguratorTest.java index c18d9e44fb8f5..93eecf3495634 100644 --- a/test-framework/junit5-component/src/test/java/io/quarkus/test/component/MockConfiguratorTest.java +++ b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/MockConfiguratorTest.java @@ -16,11 +16,12 @@ public class MockConfiguratorTest { @RegisterExtension - static final QuarkusComponentTestExtension extension = new QuarkusComponentTestExtension(MyComponent.class) + static final QuarkusComponentTestExtension extension = QuarkusComponentTestExtension.builder() .mock(Charlie.class).createMockitoMock(charlie -> { Mockito.when(charlie.pong()).thenReturn("bar"); }) - .configProperty("foo", "BAR"); + .configProperty("foo", "BAR") + .build(); @Inject MyComponent myComponent; diff --git a/test-framework/junit5-component/src/test/java/io/quarkus/test/component/MockNotSharedForClassHierarchyTest.java b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/MockNotSharedForClassHierarchyTest.java index 0e3bd38bc03e1..6167648bfcfb6 100644 --- a/test-framework/junit5-component/src/test/java/io/quarkus/test/component/MockNotSharedForClassHierarchyTest.java +++ b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/MockNotSharedForClassHierarchyTest.java @@ -15,8 +15,9 @@ public class MockNotSharedForClassHierarchyTest { @RegisterExtension - static final QuarkusComponentTestExtension extension = new QuarkusComponentTestExtension(Component.class) - .ignoreNestedClasses(); + static final QuarkusComponentTestExtension extension = QuarkusComponentTestExtension.builder() + .ignoreNestedClasses() + .build(); @Inject Component component; diff --git a/test-framework/junit5-component/src/test/java/io/quarkus/test/component/MockSharedForClassHierarchyTest.java b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/MockSharedForClassHierarchyTest.java index 4ad331494aba8..91aefca7305d0 100644 --- a/test-framework/junit5-component/src/test/java/io/quarkus/test/component/MockSharedForClassHierarchyTest.java +++ b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/MockSharedForClassHierarchyTest.java @@ -13,10 +13,13 @@ public class MockSharedForClassHierarchyTest { @RegisterExtension - static final QuarkusComponentTestExtension extension = new QuarkusComponentTestExtension(Component.class).mock(Foo.class) + static final QuarkusComponentTestExtension extension = QuarkusComponentTestExtension.builder() + .mock(Foo.class) .createMockitoMock(foo -> { Mockito.when(foo.ping()).thenReturn(11); - }).ignoreNestedClasses(); + }) + .ignoreNestedClasses() + .build(); @Inject Component component; diff --git a/test-framework/junit5-component/src/test/java/io/quarkus/test/component/ObserverInjectingMockTest.java b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/ObserverInjectingMockTest.java index af369b857d0cc..72b79a14628db 100644 --- a/test-framework/junit5-component/src/test/java/io/quarkus/test/component/ObserverInjectingMockTest.java +++ b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/ObserverInjectingMockTest.java @@ -16,8 +16,10 @@ public class ObserverInjectingMockTest { @RegisterExtension - static final QuarkusComponentTestExtension extension = new QuarkusComponentTestExtension(MyComponent.class) - .useDefaultConfigProperties(); + static final QuarkusComponentTestExtension extension = QuarkusComponentTestExtension.builder() + .addComponentClasses(MyComponent.class) + .useDefaultConfigProperties() + .build(); @Inject Event event; diff --git a/test-framework/junit5-component/src/test/java/io/quarkus/test/component/UnsetConfigurationPropertiesTest.java b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/UnsetConfigurationPropertiesTest.java index a8dbc8b8c6226..1e9bcb7dd6991 100644 --- a/test-framework/junit5-component/src/test/java/io/quarkus/test/component/UnsetConfigurationPropertiesTest.java +++ b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/UnsetConfigurationPropertiesTest.java @@ -14,8 +14,9 @@ public class UnsetConfigurationPropertiesTest { @RegisterExtension - static final QuarkusComponentTestExtension extension = new QuarkusComponentTestExtension(Component.class) - .useDefaultConfigProperties(); + static final QuarkusComponentTestExtension extension = QuarkusComponentTestExtension.builder() + .useDefaultConfigProperties() + .build(); @Inject Component component; diff --git a/test-framework/junit5-component/src/test/java/io/quarkus/test/component/declarative/ConfigPropertyDeclaredOnMethodOverridesClassTest.java b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/declarative/ConfigPropertyDeclaredOnMethodOverridesClassTest.java new file mode 100644 index 0000000000000..dbbbc77319d60 --- /dev/null +++ b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/declarative/ConfigPropertyDeclaredOnMethodOverridesClassTest.java @@ -0,0 +1,33 @@ +package io.quarkus.test.component.declarative; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import io.quarkus.test.InjectMock; +import io.quarkus.test.component.QuarkusComponentTest; +import io.quarkus.test.component.TestConfigProperty; +import io.quarkus.test.component.beans.Charlie; +import io.quarkus.test.component.beans.MyComponent; + +@TestConfigProperty(key = "foo", value = "BAZ") +@QuarkusComponentTest +public class ConfigPropertyDeclaredOnMethodOverridesClassTest { + + @Inject + MyComponent myComponent; + + @InjectMock + Charlie charlie; + + @TestConfigProperty(key = "foo", value = "BAR") + @Test + public void testPing() { + Mockito.when(charlie.ping()).thenReturn("1"); + assertEquals("1 and BAR", myComponent.ping()); + } + +} diff --git a/test-framework/junit5-component/src/test/java/io/quarkus/test/component/declarative/ConfigPropertyDeclaredOnMethodTest.java b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/declarative/ConfigPropertyDeclaredOnMethodTest.java new file mode 100644 index 0000000000000..30a8a17bb8386 --- /dev/null +++ b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/declarative/ConfigPropertyDeclaredOnMethodTest.java @@ -0,0 +1,39 @@ +package io.quarkus.test.component.declarative; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import io.quarkus.test.InjectMock; +import io.quarkus.test.component.QuarkusComponentTest; +import io.quarkus.test.component.TestConfigProperty; +import io.quarkus.test.component.beans.Charlie; +import io.quarkus.test.component.beans.MyComponent; + +@QuarkusComponentTest +public class ConfigPropertyDeclaredOnMethodTest { + + @Inject + MyComponent myComponent; + + @InjectMock + Charlie charlie; + + @TestConfigProperty(key = "foo", value = "BAR") + @Test + public void testPing1() { + Mockito.when(charlie.ping()).thenReturn("1"); + assertEquals("1 and BAR", myComponent.ping()); + } + + @TestConfigProperty(key = "foo", value = "BAZ") + @Test + public void testPing2() { + Mockito.when(charlie.ping()).thenReturn("2"); + assertEquals("2 and BAZ", myComponent.ping()); + } + +} diff --git a/test-framework/junit5-component/src/test/java/io/quarkus/test/component/lifecycle/PerClassLifecycleTest.java b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/lifecycle/PerClassLifecycleTest.java new file mode 100644 index 0000000000000..f5be9cb6df7e4 --- /dev/null +++ b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/lifecycle/PerClassLifecycleTest.java @@ -0,0 +1,69 @@ +package io.quarkus.test.component.lifecycle; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.concurrent.atomic.AtomicInteger; + +import jakarta.annotation.PostConstruct; +import jakarta.inject.Inject; +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 org.junit.jupiter.api.extension.RegisterExtension; +import org.mockito.Mockito; + +import io.quarkus.test.InjectMock; +import io.quarkus.test.component.QuarkusComponentTestExtension; +import io.quarkus.test.component.beans.Charlie; + +@TestInstance(Lifecycle.PER_CLASS) +public class PerClassLifecycleTest { + + @RegisterExtension + static final QuarkusComponentTestExtension extension = new QuarkusComponentTestExtension(); + + @Inject + MySingleton mySingleton; + + @InjectMock + Charlie charlie; + + @Order(1) + @Test + public void testPing1() { + Mockito.when(charlie.ping()).thenReturn("foo"); + assertEquals("foo", mySingleton.ping()); + assertEquals(1, MySingleton.COUNTER.get()); + } + + @Order(2) + @Test + public void testPing2() { + Mockito.when(charlie.ping()).thenReturn("baz"); + assertEquals("baz", mySingleton.ping()); + assertEquals(1, MySingleton.COUNTER.get()); + } + + @Singleton + public static class MySingleton { + + static final AtomicInteger COUNTER = new AtomicInteger(); + + @Inject + Charlie charlie; + + @PostConstruct + void init() { + COUNTER.incrementAndGet(); + } + + public String ping() { + return charlie.ping(); + } + + } + +} diff --git a/test-framework/junit5-component/src/test/java/io/quarkus/test/component/lifecycle/PerMethodLifecycleTest.java b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/lifecycle/PerMethodLifecycleTest.java new file mode 100644 index 0000000000000..8b650ba7b045b --- /dev/null +++ b/test-framework/junit5-component/src/test/java/io/quarkus/test/component/lifecycle/PerMethodLifecycleTest.java @@ -0,0 +1,69 @@ +package io.quarkus.test.component.lifecycle; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.concurrent.atomic.AtomicInteger; + +import jakarta.annotation.PostConstruct; +import jakarta.inject.Inject; +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 org.junit.jupiter.api.extension.RegisterExtension; +import org.mockito.Mockito; + +import io.quarkus.test.InjectMock; +import io.quarkus.test.component.QuarkusComponentTestExtension; +import io.quarkus.test.component.beans.Charlie; + +@TestInstance(Lifecycle.PER_METHOD) +public class PerMethodLifecycleTest { + + @RegisterExtension + static final QuarkusComponentTestExtension extension = new QuarkusComponentTestExtension(); + + @Inject + MySingleton mySingleton; + + @InjectMock + Charlie charlie; + + @Order(1) + @Test + public void testPing1() { + Mockito.when(charlie.ping()).thenReturn("foo"); + assertEquals("foo", mySingleton.ping()); + assertEquals(1, MySingleton.COUNTER.get()); + } + + @Order(2) + @Test + public void testPing2() { + Mockito.when(charlie.ping()).thenReturn("baz"); + assertEquals("baz", mySingleton.ping()); + assertEquals(2, MySingleton.COUNTER.get()); + } + + @Singleton + public static class MySingleton { + + static final AtomicInteger COUNTER = new AtomicInteger(); + + @Inject + Charlie charlie; + + @PostConstruct + void init() { + COUNTER.incrementAndGet(); + } + + public String ping() { + return charlie.ping(); + } + + } + +}