Skip to content

Commit

Permalink
QuarkusComponentTest: initial support for ConfigMapping
Browse files Browse the repository at this point in the history
- resolves #36373
  • Loading branch information
mkouba committed Oct 11, 2023
1 parent 89a0142 commit 6697d1d
Show file tree
Hide file tree
Showing 4 changed files with 149 additions and 9 deletions.
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();
}
}
}

0 comments on commit 6697d1d

Please sign in to comment.