Skip to content

Commit

Permalink
QuarkusTest: handle beans declared on test profile specifically
Browse files Browse the repository at this point in the history
- beans declared on a test profile implementation are only taken into
account if the test profile is used
- resolves quarkusio#36554
  • Loading branch information
mkouba committed Oct 23, 2023
1 parent 4fb9864 commit 6f2db4b
Show file tree
Hide file tree
Showing 7 changed files with 183 additions and 3 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package io.quarkus.deployment.builditem;

import io.quarkus.builder.item.SimpleBuildItem;

/**
* This is an optional build item that represents the current test profile.
* <p>
* It is only available during tests.
*/
public final class TestProfileBuildItem extends SimpleBuildItem {

private final String testProfileClassName;

public TestProfileBuildItem(String testProfileClassName) {
this.testProfileClassName = testProfileClassName;
}

public String getTestProfileClassName() {
return testProfileClassName;
}
}
8 changes: 8 additions & 0 deletions docs/src/main/asciidoc/getting-started-testing.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,8 @@ import java.util.List;
import java.util.Map;
import java.util.Set;
import jakarta.enterprise.inject.Produces;
import io.quarkus.test.junit.QuarkusTestProfile;
import io.quarkus.test.junit.QuarkusTestProfile.TestResourceEntry;
Expand Down Expand Up @@ -601,9 +603,15 @@ public class MockGreetingProfile implements QuarkusTestProfile { <1>
public boolean disableApplicationLifecycleObservers() {
return false;
}
@Produces <2>
public ExternalService mockExternalService() {
return new ExternalService("mock");
}
}
----
<1> All these methods have default implementations so just override the ones you need to override.
<2> If a test profile implementation declares a CDI bean (via producer method/field or nested static class) then this bean is only taken into account if the test profile is used, i.e. it's ignored for any other test profile.

Now we have defined our profile we need to include it on our test class.
We do this by annotating the test class with `@TestProfile(MockGreetingProfile.class)`.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,28 @@
package io.quarkus.arc.deployment;

import java.lang.reflect.Modifier;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;

import org.jboss.jandex.AnnotationTarget;
import org.jboss.jandex.AnnotationTarget.Kind;
import org.jboss.jandex.ClassInfo;
import org.jboss.jandex.ClassInfo.NestingType;
import org.jboss.jandex.DotName;
import org.jboss.jandex.IndexView;

import io.quarkus.arc.processor.Annotations;
import io.quarkus.arc.processor.AnnotationsTransformer;
import io.quarkus.arc.processor.DotNames;
import io.quarkus.deployment.IsTest;
import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.builditem.CombinedIndexBuildItem;
import io.quarkus.deployment.builditem.TestAnnotationBuildItem;
import io.quarkus.deployment.builditem.TestClassBeanBuildItem;
import io.quarkus.deployment.builditem.TestProfileBuildItem;

public class TestsAsBeansProcessor {

Expand All @@ -31,4 +46,92 @@ public void testClassBeans(List<TestClassBeanBuildItem> items, BuildProducer<Add
producer.produce(builder.build());
}

@BuildStep(onlyIf = IsTest.class)
AnnotationsTransformerBuildItem vetoTestProfileBeans(Optional<TestProfileBuildItem> testProfile,
CustomScopeAnnotationsBuildItem customScopes, CombinedIndexBuildItem index) {
if (index.getIndex().getAllKnownImplementors(QUARKUS_TEST_PROFILE).isEmpty()) {
// No test profiles found
return null;
}

Set<DotName> currentTestProfileHierarchy = initTestProfileHierarchy(testProfile, index.getComputingIndex());
return new AnnotationsTransformerBuildItem(new AnnotationsTransformer() {

@Override
public void transform(TransformationContext context) {
AnnotationTarget target = context.getTarget();
if (target.kind() == Kind.METHOD) {
vetoProducerIfNecessary(target.asMethod().declaringClass(), context);
} else if (target.kind() == Kind.FIELD) {
vetoProducerIfNecessary(target.asField().declaringClass(), context);
} else if (target.kind() == Kind.CLASS) {
ClassInfo clazz = target.asClass();
if (clazz.nestingType() == NestingType.INNER && Modifier.isStatic(clazz.flags())) {
ClassInfo enclosing = index.getComputingIndex().getClassByName(clazz.enclosingClass());
if (customScopes.isScopeIn(context.getAnnotations())
&& isTestProfileClass(enclosing, index.getComputingIndex())
&& !currentTestProfileHierarchy.contains(enclosing.name())) {
// Veto static nested class declared on a test profile class
context.transform().add(DotNames.VETOED).done();
}
}
}
}

private void vetoProducerIfNecessary(ClassInfo declaringClass, TransformationContext context) {
if (Annotations.contains(context.getAnnotations(), DotNames.PRODUCES)
&& isTestProfileClass(declaringClass, index.getComputingIndex())
&& !currentTestProfileHierarchy.contains(declaringClass.name())) {
// Veto producer method/field declared on a test profile class
context.transform().add(DotNames.VETOED_PRODUCER).done();
}
}
});
}

private static final DotName QUARKUS_TEST_PROFILE = DotName.createSimple("io.quarkus.test.junit.QuarkusTestProfile");

private static Set<DotName> initTestProfileHierarchy(Optional<TestProfileBuildItem> testProfile, IndexView index) {
Set<DotName> ret = Set.of();
if (testProfile.isPresent()) {
DotName testProfileClassName = DotName.createSimple(testProfile.get().getTestProfileClassName());
ret = Set.of(testProfileClassName);
ClassInfo testProfileClass = index.getClassByName(testProfile.get().getTestProfileClassName());
if (testProfileClass != null && !testProfileClass.superName().equals(DotName.OBJECT_NAME)) {
ret = new HashSet<>();
ret.add(testProfileClassName);
DotName superName = testProfileClass.superName();
while (superName != null && !superName.equals(DotNames.OBJECT)) {
ret.add(superName);
ClassInfo superClass = index.getClassByName(superName);
if (superClass != null) {
superName = superClass.superName();
} else {
superName = null;
}
}
}
}
return ret;
}

private static boolean isTestProfileClass(ClassInfo clazz, IndexView index) {
if (clazz.interfaceNames().contains(QUARKUS_TEST_PROFILE)) {
return true;
}
DotName superName = clazz.superName();
while (superName != null && !superName.equals(DotNames.OBJECT)) {
ClassInfo superClass = index.getClassByName(superName);
if (superClass != null) {
if (superClass.interfaceNames().contains(QUARKUS_TEST_PROFILE)) {
return true;
}
superName = superClass.superName();
} else {
superName = null;
}
}
return false;
}

}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.quarkus.it.main;

import static org.hamcrest.Matchers.is;
import static org.junit.jupiter.api.Assertions.assertEquals;

import java.util.Collections;
import java.util.List;
Expand All @@ -9,9 +10,16 @@
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;

import jakarta.annotation.Priority;
import jakarta.enterprise.inject.Alternative;
import jakarta.enterprise.inject.Produces;
import jakarta.inject.Inject;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

import io.quarkus.it.rest.ExternalService;
import io.quarkus.it.rest.GreetingService;
import io.quarkus.test.common.QuarkusTestResourceLifecycleManager;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.junit.QuarkusTestProfile;
Expand All @@ -25,6 +33,12 @@
@TestProfile(SharedProfileTestCase.MyProfile.class)
public class SharedProfileTestCase {

@Inject
GreetingService greetingService;

@Inject
ExternalService externalService;

@Test
public void included() {
RestAssured.when()
Expand All @@ -50,6 +64,12 @@ public void testContext() {
Assertions.assertEquals(MyProfile.class.getName(), DummyTestResource.testProfile.get());
}

@Test
public void testProfileBeans() {
assertEquals("Bonjour Foo", greetingService.greet("Foo"));
assertEquals("profile", externalService.service());
}

public static class MyProfile implements QuarkusTestProfile {

@Override
Expand Down Expand Up @@ -77,6 +97,18 @@ public String[] commandLineParameters() {
public boolean runMainMethod() {
return true;
}

@Priority(1000) // Must be higher than priority of MockExternalService
@Alternative
@Produces
public ExternalService externalService() {
return new ExternalService() {
@Override
public String service() {
return "profile";
}
};
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ public class AbstractJvmQuarkusTestExtension extends AbstractQuarkusTestWithCont

protected static final String TEST_LOCATION = "test-location";
protected static final String TEST_CLASS = "test-class";
protected static final String TEST_PROFILE = "test-profile";

protected ClassLoader originalCl;

Expand Down Expand Up @@ -202,6 +203,9 @@ protected PrepareResult createAugmentor(ExtensionContext context, Class<? extend
final Map<String, Object> props = new HashMap<>();
props.put(TEST_LOCATION, testClassLocation);
props.put(TEST_CLASS, requiredTestClass);
if (profile != null) {
props.put(TEST_PROFILE, profile.getName());
}
quarkusTestProfile = profile;
return new PrepareResult(curatedApplication
.createAugmentor(QuarkusTestExtension.TestBuildChainFunction.class.getName(), props), profileInstance,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
import io.quarkus.deployment.builditem.TestAnnotationBuildItem;
import io.quarkus.deployment.builditem.TestClassBeanBuildItem;
import io.quarkus.deployment.builditem.TestClassPredicateBuildItem;
import io.quarkus.deployment.builditem.TestProfileBuildItem;
import io.quarkus.dev.testing.ExceptionReporting;
import io.quarkus.dev.testing.TracingHandler;
import io.quarkus.runtime.ApplicationLifecycleManager;
Expand Down Expand Up @@ -582,8 +583,6 @@ private boolean isNativeOrIntegrationTest(Class<?> clazz) {
}

private QuarkusTestExtensionState ensureStarted(ExtensionContext extensionContext) {
ExtensionContext.Store store = getStoreFromContext(extensionContext);

QuarkusTestExtensionState state = getState(extensionContext);
Class<? extends QuarkusTestProfile> selectedProfile = getQuarkusTestProfile(extensionContext);
boolean wrongProfile = !Objects.equals(selectedProfile, quarkusTestProfile);
Expand Down Expand Up @@ -1356,6 +1355,16 @@ public void execute(BuildContext context) {
.build();
}

buildChainBuilder.addBuildStep(new BuildStep() {
@Override
public void execute(BuildContext context) {
Object testProfile = stringObjectMap.get(TEST_PROFILE);
if (testProfile != null) {
context.produce(new TestProfileBuildItem(testProfile.toString()));
}
}
}).produces(TestProfileBuildItem.class).build();

}
};
allCustomizers.add(defaultCustomizer);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@
/**
* Defines a 'test profile'. Tests run under a test profile
* will have different configuration options to other tests.
*
* <p>
* If an implementation of this interface declares CDI beans, via producer methods/fields and nested static classes, then those
* beans are only taken into account if this test profile is used. In other words, the beans are ignored for any other test
* profile.
*/
public interface QuarkusTestProfile {

Expand Down

0 comments on commit 6f2db4b

Please sign in to comment.