Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

QuarkusComponentTest: initial support for ConfigMapping #36389

Merged
merged 1 commit into from
Oct 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/src/main/asciidoc/getting-started-testing.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -1648,6 +1648,8 @@ If you only need to use the default values for missing config properties, then t
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.

CDI beans are also automatically registered for all injected https://smallrye.io/smallrye-config/Main/config/mappings/[Config Mappings]. The mappings are populated with the test configuration properties.

=== Mocking CDI Interceptors

If a tested component class declares an interceptor binding then you might need to mock the interception too.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package io.quarkus.test.component;

import io.quarkus.arc.BeanCreator;
import io.quarkus.arc.SyntheticCreationalContext;
import io.smallrye.config.SmallRyeConfig;

public class ConfigMappingBeanCreator implements BeanCreator<Object> {

@Override
public Object create(SyntheticCreationalContext<Object> context) {
String prefix = context.getParams().get("prefix").toString();
Class<?> mappingClass = tryLoad(context.getParams().get("mappingClass").toString());
SmallRyeConfig config = ConfigBeanCreator.getConfig().unwrap(SmallRyeConfig.class);
return config.getConfigMapping(mappingClass, prefix);
}

static Class<?> tryLoad(String name) {
try {
return Thread.currentThread().getContextClassLoader().loadClass(name);
} catch (ClassNotFoundException e) {
throw new IllegalStateException("Unable to load type: " + name, e);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
Expand Down Expand Up @@ -48,6 +49,7 @@
import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.eclipse.microprofile.config.spi.ConfigProviderResolver;
import org.jboss.jandex.AnnotationInstance;
import org.jboss.jandex.AnnotationValue;
import org.jboss.jandex.ClassInfo;
import org.jboss.jandex.ClassType;
import org.jboss.jandex.DotName;
Expand Down Expand Up @@ -95,6 +97,8 @@
import io.quarkus.runtime.configuration.ApplicationPropertiesConfigSourceLoader;
import io.quarkus.test.InjectMock;
import io.smallrye.common.annotation.Experimental;
import io.smallrye.config.ConfigMapping;
import io.smallrye.config.ConfigMappings.ConfigClassWithPrefix;
import io.smallrye.config.SmallRyeConfig;
import io.smallrye.config.SmallRyeConfigBuilder;
import io.smallrye.config.SmallRyeConfigProviderResolver;
Expand Down Expand Up @@ -150,6 +154,7 @@ public static QuarkusComponentTestExtensionBuilder builder() {
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 KEY_CONFIG_MAPPINGS = "configMappings";

private static final String QUARKUS_TEST_COMPONENT_OUTPUT_DIRECTORY = "quarkus.test.component.output-directory";

Expand Down Expand Up @@ -229,7 +234,7 @@ private void buildContainer(ExtensionContext context) {
private void cleanup(ExtensionContext context) {
ClassLoader oldTccl = context.getRoot().getStore(NAMESPACE).get(KEY_OLD_TCCL, ClassLoader.class);
Thread.currentThread().setContextClassLoader(oldTccl);

context.getRoot().getStore(NAMESPACE).remove(KEY_CONFIG_MAPPINGS);
Set<Path> generatedResources = context.getRoot().getStore(NAMESPACE).get(KEY_GENERATED_RESOURCES, Set.class);
for (Path path : generatedResources) {
try {
Expand Down Expand Up @@ -285,15 +290,24 @@ private void startContainer(ExtensionContext context, Lifecycle testInstanceLife

// TCCL is now the QuarkusComponentTestClassLoader set during initialization
ClassLoader tccl = Thread.currentThread().getContextClassLoader();
SmallRyeConfig config = new SmallRyeConfigBuilder().forClassLoader(tccl)
SmallRyeConfigBuilder configBuilder = new SmallRyeConfigBuilder().forClassLoader(tccl)
.addDefaultInterceptors()
.addDefaultSources()
.withSources(new ApplicationPropertiesConfigSourceLoader.InFileSystem())
.withSources(new ApplicationPropertiesConfigSourceLoader.InClassPath())
.withSources(
new QuarkusComponentTestConfigSource(configuration.configProperties,
configuration.configSourceOrdinal))
.build();
configuration.configSourceOrdinal));
@SuppressWarnings("unchecked")
Set<ConfigClassWithPrefix> configMappings = context.getRoot().getStore(NAMESPACE).get(KEY_CONFIG_MAPPINGS,
Set.class);
if (configMappings != null) {
// Register the mappings found during bean discovery
for (ConfigClassWithPrefix mapping : configMappings) {
configBuilder.withMapping(mapping.getKlass(), mapping.getPrefix());
}
}
SmallRyeConfig config = configBuilder.build();
smallRyeConfigProviderResolver.registerConfig(config, tccl);
context.getRoot().getStore(NAMESPACE).put(KEY_CONFIG, config);
ConfigBeanCreator.setClassLoader(tccl);
Expand Down Expand Up @@ -503,17 +517,15 @@ public void register(RegistrationContext registrationContext) {
Set<TypeAndQualifiers> unsatisfiedInjectionPoints = new HashSet<>();
boolean configInjectionPoint = false;
Set<TypeAndQualifiers> configPropertyInjectionPoints = new HashSet<>();
Map<String, Set<String>> prefixToConfigMappings = new HashMap<>();
DotName configDotName = DotName.createSimple(Config.class);
DotName configPropertyDotName = DotName.createSimple(ConfigProperty.class);
DotName configMappingDotName = DotName.createSimple(ConfigMapping.class);

// Analyze injection points
// - find Config and @ConfigProperty injection points
// - find Config, @ConfigProperty and config mappings injection points
// - find unsatisfied injection points
for (InjectionPointInfo injectionPoint : registrationContext.getInjectionPoints()) {
BuiltinBean builtin = BuiltinBean.resolve(injectionPoint);
if (builtin != null && builtin != BuiltinBean.INSTANCE && builtin != BuiltinBean.LIST) {
continue;
}
if (injectionPoint.getRequiredType().name().equals(configDotName)
&& injectionPoint.hasDefaultedQualifier()) {
configInjectionPoint = true;
Expand All @@ -524,6 +536,10 @@ public void register(RegistrationContext registrationContext) {
injectionPoint.getRequiredQualifiers()));
continue;
}
BuiltinBean builtin = BuiltinBean.resolve(injectionPoint);
if (builtin != null && builtin != BuiltinBean.INSTANCE && builtin != BuiltinBean.LIST) {
continue;
}
Type requiredType = injectionPoint.getRequiredType();
Set<AnnotationInstance> requiredQualifiers = injectionPoint.getRequiredQualifiers();
if (builtin == BuiltinBean.LIST) {
Expand All @@ -535,6 +551,19 @@ public void register(RegistrationContext registrationContext) {
requiredQualifiers.add(AnnotationInstance.builder(DotNames.DEFAULT).build());
}
}
if (requiredType.kind() == Kind.CLASS) {
ClassInfo clazz = computingIndex.getClassByName(requiredType.name());
if (clazz != null && clazz.isInterface()) {
AnnotationInstance configMapping = clazz.declaredAnnotation(configMappingDotName);
if (configMapping != null) {
AnnotationValue prefixValue = configMapping.value("prefix");
String prefix = prefixValue == null ? "" : prefixValue.asString();
Set<String> mappingClasses = prefixToConfigMappings.computeIfAbsent(prefix,
k -> new HashSet<>());
mappingClasses.add(clazz.name().toString());
}
}
}
if (isSatisfied(requiredType, requiredQualifiers, injectionPoint, beans, beanDeployment,
configuration)) {
continue;
Expand Down Expand Up @@ -591,6 +620,24 @@ public void register(RegistrationContext registrationContext) {
configPropertyConfigurator.done();
}

if (!prefixToConfigMappings.isEmpty()) {
Set<ConfigClassWithPrefix> configMappings = new HashSet<>();
for (Entry<String, Set<String>> e : prefixToConfigMappings.entrySet()) {
for (String mapping : e.getValue()) {
DotName mappingName = DotName.createSimple(mapping);
registrationContext.configure(mappingName)
.addType(mappingName)
.creator(ConfigMappingBeanCreator.class)
.param("mappingClass", mapping)
.param("prefix", e.getKey())
.done();
configMappings.add(ConfigClassWithPrefix
.configClassWithPrefix(ConfigMappingBeanCreator.tryLoad(mapping), e.getKey()));
}
}
extensionContext.getRoot().getStore(NAMESPACE).put(KEY_CONFIG_MAPPINGS, configMappings);
}

LOG.debugf("Test injection points analyzed in %s ms [found: %s, mocked: %s]",
TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start),
registrationContext.getInjectionPoints().size(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package io.quarkus.test.component.config;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

import jakarta.inject.Inject;
import jakarta.inject.Singleton;

import org.junit.jupiter.api.Test;

import io.quarkus.test.component.QuarkusComponentTest;
import io.quarkus.test.component.TestConfigProperty;
import io.smallrye.config.ConfigMapping;
import io.smallrye.config.WithDefault;

@QuarkusComponentTest
@TestConfigProperty(key = "foo.bar", value = "true")
@TestConfigProperty(key = "foo.log.rotate", value = "true")
public class ConfigMappingTest {

@Inject
Foo foo;

@TestConfigProperty(key = "foo.oof", value = "boom")
@Test
public void testMapping() {
assertTrue(foo.config.bar());
assertTrue(foo.config.log().rotate());
assertEquals("loom", foo.config.baz());
assertEquals("boom", foo.config.oof());
}

@TestConfigProperty(key = "foo.oof", value = "boomboom")
@Test
public void testAnotherMapping() {
assertTrue(foo.config.bar());
assertTrue(foo.config.log().rotate());
assertEquals("loom", foo.config.baz());
assertEquals("boomboom", foo.config.oof());
}

@Singleton
public static class Foo {

@Inject
FooConfig config;
}

@ConfigMapping(prefix = "foo")
interface FooConfig {

boolean bar();

@WithDefault("loom")
String baz();

String oof();

Log log();

// nested mapping
interface Log {
boolean rotate();
}
}
}
Loading