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: convenient handling of nested classes #34127

Merged
merged 1 commit into from
Jun 20, 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
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import static io.quarkus.deployment.annotations.ExecutionTime.RUNTIME_INIT;
import static io.quarkus.deployment.annotations.ExecutionTime.STATIC_INIT;

import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
Expand All @@ -24,6 +25,7 @@

import org.jboss.jandex.AnnotationTarget;
import org.jboss.jandex.ClassInfo;
import org.jboss.jandex.ClassInfo.NestingType;
import org.jboss.jandex.DotName;
import org.jboss.jandex.FieldInfo;
import org.jboss.jandex.IndexView;
Expand Down Expand Up @@ -383,6 +385,9 @@ public Integer compute(AnnotationTarget target, Collection<StereotypeInfo> stere
builder.addExcludeType(predicate);
}
}
if (launchModeBuildItem.getLaunchMode() == LaunchMode.TEST) {
builder.addExcludeType(createQuarkusComponentTestExcludePredicate(index));
}

for (SuppressConditionGeneratorBuildItem generator : suppressConditionGenerators) {
builder.addSuppressConditionGenerator(generator.getGenerator());
Expand Down Expand Up @@ -731,6 +736,39 @@ void registerContextPropagation(ArcConfig config, BuildProducer<ThreadContextPro
}
}

Predicate<ClassInfo> createQuarkusComponentTestExcludePredicate(IndexView index) {
// Exlude static nested classed declared on a QuarkusComponentTest:
// 1. Test class annotated with @QuarkusComponentTest
// 2. Test class with a static field of a type QuarkusComponentTestExtension
DotName quarkusComponentTest = DotName.createSimple("io.quarkus.test.component.QuarkusComponentTest");
DotName quarkusComponentTestExtension = DotName.createSimple("io.quarkus.test.component.QuarkusComponentTestExtension");
return new Predicate<ClassInfo>() {

@Override
public boolean test(ClassInfo clazz) {
if (clazz.nestingType() == NestingType.INNER
&& Modifier.isStatic(clazz.flags())) {
DotName enclosingClassName = clazz.enclosingClass();
ClassInfo enclosingClass = index.getClassByName(enclosingClassName);
if (enclosingClass != null) {
if (enclosingClass.hasDeclaredAnnotation(quarkusComponentTest)) {
return true;
} else {
for (FieldInfo field : enclosingClass.fields()) {
if (!field.isSynthetic()
&& Modifier.isStatic(field.flags())
&& field.type().name().equals(quarkusComponentTestExtension)) {
return true;
}
}
}
}
}
return false;
}
};
}

private abstract static class AbstractCompositeApplicationClassesPredicate<T> implements Predicate<T> {

private final IndexView applicationClassesIndex;
Expand Down
5 changes: 5 additions & 0 deletions integration-tests/main/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,11 @@
<artifactId>quarkus-junit5</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5-component</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-test-h2</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package io.quarkus.it.main;

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

import jakarta.enterprise.inject.Produces;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;

import org.eclipse.microprofile.config.inject.ConfigProperty;
import org.junit.jupiter.api.Test;

import io.quarkus.test.component.QuarkusComponentTest;

@QuarkusComponentTest
public class MyComponentTest {

@Inject
@ConfigProperty // name and default value are nonbinding
String myProperty;

@Test
public void testProperty() {
assertEquals("foo", myProperty);
}

@Singleton
static class PropertyProducer {

// This producer would normally break all @QuarkusTest in the test suite
// However, since it's a static nested class declared on a @QuarkusComponentTest it's excluded from the bean discovery
@Produces
@ConfigProperty
String myString() {
return "foo";
}

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,11 @@
* @see QuarkusComponentTestExtension#useDefaultConfigProperties()
*/
boolean useDefaultConfigProperties() default false;

/**
* If set to {@code true} then all static nested classes are considered additional components under test.
*
* @see #value()
*/
boolean addNestedClassesAsComponents() default true;
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Parameter;
import java.lang.reflect.ParameterizedType;
import java.nio.file.Files;
Expand Down Expand Up @@ -65,6 +66,7 @@
import io.quarkus.arc.ArcContainer;
import io.quarkus.arc.ComponentsProvider;
import io.quarkus.arc.InstanceHandle;
import io.quarkus.arc.Unremovable;
import io.quarkus.arc.processor.Annotations;
import io.quarkus.arc.processor.AnnotationsTransformer;
import io.quarkus.arc.processor.BeanArchives;
Expand Down Expand Up @@ -151,6 +153,7 @@ public class QuarkusComponentTestExtension
private final List<Class<?>> additionalComponentClasses;
private final List<MockBeanConfiguratorImpl<?>> mockConfigurators;
private final AtomicBoolean useDefaultConfigProperties = new AtomicBoolean();
private final AtomicBoolean addNestedClassesAsComponents = new AtomicBoolean(true);

// Used for declarative registration
public QuarkusComponentTestExtension() {
Expand Down Expand Up @@ -209,6 +212,19 @@ public QuarkusComponentTestExtension useDefaultConfigProperties() {
return this;
}

/**
* Ignore the static nested classes declared on the test class.
* <p>
* By default, all static nested classes declared on the test class are added to the set of additional components under
* test.
*
* @return the extension
*/
public QuarkusComponentTestExtension ignoreNestedClasses() {
this.addNestedClassesAsComponents.set(false);
return this;
}

@Override
public void postProcessTestInstance(Object testInstance, ExtensionContext context) throws Exception {
long start = System.nanoTime();
Expand Down Expand Up @@ -247,6 +263,7 @@ public void beforeAll(ExtensionContext context) throws Exception {
if (testAnnotation.useDefaultConfigProperties()) {
this.useDefaultConfigProperties.set(true);
}
this.addNestedClassesAsComponents.set(testAnnotation.addNestedClassesAsComponents());
}
// All fields annotated with @Inject represent component classes
Class<?> current = testClass;
Expand All @@ -258,6 +275,14 @@ public void beforeAll(ExtensionContext context) throws Exception {
}
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) {
Expand Down Expand Up @@ -468,7 +493,13 @@ private ClassLoader initArcContainer(ExtensionContext context, Collection<Class<
BeanProcessor.Builder builder = BeanProcessor.builder()
.setName(testClass.getName().replace('.', '_'))
.addRemovalExclusion(b -> {
// Do not remove beans injected in the test class
// Do not remove beans:
// 1. Injected in the test class
// 2. Annotated with @Unremovable
if (b.getTarget().isPresent()
&& b.getTarget().get().hasDeclaredAnnotation(Unremovable.class)) {
return true;
}
for (Field injectionPoint : testClassInjectionPoints) {
if (beanResolver.get().matches(b, Types.jandexType(injectionPoint.getGenericType()),
getQualifiers(injectionPoint, qualifiers))) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
public class MockNotSharedForClassHierarchyTest {

@RegisterExtension
static final QuarkusComponentTestExtension extension = new QuarkusComponentTestExtension(Component.class);
static final QuarkusComponentTestExtension extension = new QuarkusComponentTestExtension(Component.class)
.ignoreNestedClasses();

@Inject
Component component;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public class MockSharedForClassHierarchyTest {
static final QuarkusComponentTestExtension extension = new QuarkusComponentTestExtension(Component.class).mock(Foo.class)
.createMockitoMock(foo -> {
Mockito.when(foo.ping()).thenReturn(11);
});
}).ignoreNestedClasses();

@Inject
Component component;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package io.quarkus.test.component.declarative;

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

import jakarta.inject.Singleton;

import org.junit.jupiter.api.Test;

import io.quarkus.arc.Arc;
import io.quarkus.arc.Unremovable;
import io.quarkus.test.component.QuarkusComponentTest;
import io.quarkus.test.component.declarative.IgnoreNestedClassesTest.Alpha;

@QuarkusComponentTest(value = Alpha.class, addNestedClassesAsComponents = false)
public class IgnoreNestedClassesTest {

@Test
public void testComponents() {
assertTrue(Arc.container().instance(Alpha.class).isAvailable());
assertFalse(Arc.container().instance(Bravo.class).isAvailable());
}

@Unremovable
@Singleton
static class Alpha {

}

@Unremovable
@Singleton
static class Bravo {

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package io.quarkus.test.component.declarative;

import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import static org.junit.jupiter.api.Assertions.assertEquals;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import jakarta.annotation.Priority;
import jakarta.inject.Inject;
import jakarta.inject.Singleton;
import jakarta.interceptor.AroundInvoke;
import jakarta.interceptor.Interceptor;
import jakarta.interceptor.InterceptorBinding;
import jakarta.interceptor.InvocationContext;

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.beans.Charlie;

@QuarkusComponentTest
public class InterceptorMockingTest {

@Inject
TheComponent theComponent;

@InjectMock
Charlie charlie;

@Test
public void testPing() {
Mockito.when(charlie.ping()).thenReturn("ok");
assertEquals("ok", theComponent.ping());
}

@Singleton
static class TheComponent {

@SimpleBinding
String ping() {
return "true";
}

}

@Target({ TYPE, METHOD })
@Retention(RUNTIME)
@InterceptorBinding
public @interface SimpleBinding {

}

// This interceptor is automatically added as a tested component
@Priority(1)
@SimpleBinding
@Interceptor
static class SimpleInterceptor {

@Inject
Charlie charlie;

@AroundInvoke
Object aroundInvoke(InvocationContext context) throws Exception {
return Boolean.parseBoolean(context.proceed().toString()) ? charlie.ping() : "false";
}

}

}