Skip to content

Commit

Permalink
QuarkusComponentTest: convenient handling of nested classes
Browse files Browse the repository at this point in the history
- add static nested classes declared on test class to the set of
  components under test by default
- exclude static nested classes declated on a QuarkusComponentTest from
  discovery during `@QuarkusTest`
  • Loading branch information
mkouba committed Jun 19, 2023
1 parent 27e381b commit f651231
Show file tree
Hide file tree
Showing 9 changed files with 234 additions and 3 deletions.
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";
}

}

}

0 comments on commit f651231

Please sign in to comment.