Skip to content

Commit

Permalink
Merge pull request #46487 from mkouba/issue-46479
Browse files Browse the repository at this point in the history
QuarkusComponentTest: add basic support for nested test classes
  • Loading branch information
mkouba authored Feb 26, 2025
2 parents 1ba5ca1 + 7cda115 commit 0d8bf11
Show file tree
Hide file tree
Showing 10 changed files with 526 additions and 105 deletions.
55 changes: 55 additions & 0 deletions docs/src/main/asciidoc/testing-components.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,59 @@ public class FooTest {
Sometimes you need the full control over the bean attributes and maybe even configure the default mock behavior.
You can use the mock configurator API via the `QuarkusComponentTestExtensionBuilder#mock()` method.

== Nested Tests

JUnit 5 https://junit.org/junit5/docs/current/user-guide/#writing-tests-nested[@Nested tests] may help to structure more complex test scenarios.
However, only basic use cases are tested with `@QuarkusComponentTest`.

.Nested test
[source, java]
----
import static org.junit.jupiter.api.Assertions.assertEquals;
import jakarta.inject.Inject;
import io.quarkus.test.InjectMock;
import io.quarkus.test.component.TestConfigProperty;
import io.quarkus.test.component.QuarkusComponentTest;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
@QuarkusComponentTest <1>
@TestConfigProperty(key = "bar", value = "true") <2>
public class FooTest {
@Inject
Foo foo; <3>
@InjectMock
Charlie charlieMock; <4>
@Nested
class PingTest {
@Test
public void testPing() {
Mockito.when(charlieMock.ping()).thenReturn("OK");
assertEquals("OK", foo.ping());
}
}
@Nested
class PongTest {
@Test
public void testPong() {
Mockito.when(charlieMock.pong()).thenReturn("NOK");
assertEquals("NOK", foo.pong());
}
}
}
----
<1> The `QuarkusComponentTest` annotation registers the JUnit extension.
<2> Sets a configuration property for the test.
<3> The test injects the component under the test. `Foo` injects `Charlie`.
<4> The test also injects a mock for `Charlie`. The injected reference is an "unconfigured" Mockito mock.

== Configuration

You can set the configuration properties for a test with the `@io.quarkus.test.component.TestConfigProperty` annotation or with the `QuarkusComponentTestExtensionBuilder#configProperty(String, String)` method.
Expand All @@ -240,6 +293,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.

NOTE: `@io.quarkus.test.component.TestConfigProperty` declared on a `@Nested` test class is always ignored.

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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@

import org.eclipse.microprofile.config.spi.Converter;
import org.jboss.logging.Logger;
import org.junit.jupiter.api.Nested;
import org.mockito.Mock;

import io.quarkus.arc.InjectableInstance;
Expand Down Expand Up @@ -101,6 +102,12 @@ QuarkusComponentTestConfiguration update(Class<?> testClass) {
List<AnnotationsTransformer> annotationsTransformers = new ArrayList<>(this.annotationsTransformers);
List<Converter<?>> configConverters = new ArrayList<>(this.configConverters);

if (testClass.isAnnotationPresent(Nested.class)) {
while (testClass.getEnclosingClass() != null) {
testClass = testClass.getEnclosingClass();
}
}

QuarkusComponentTest testAnnotation = testClass.getAnnotation(QuarkusComponentTest.class);
if (testAnnotation != null) {
Collections.addAll(componentClasses, testAnnotation.value());
Expand Down Expand Up @@ -130,67 +137,78 @@ QuarkusComponentTestConfiguration update(Class<?> testClass) {
}
Class<?> current = testClass;
while (current != null && current != Object.class) {
// All fields annotated with @Inject represent component classes
for (Field field : current.getDeclaredFields()) {
if (field.isAnnotationPresent(Inject.class)) {
if (Instance.class.isAssignableFrom(field.getType())
|| QuarkusComponentTestExtension.isListAllInjectionPoint(field.getGenericType(),
field.getAnnotations(),
field)) {
// Special handling for Instance<Foo> and @All List<Foo>
componentClasses
.add(getRawType(
QuarkusComponentTestExtension.getFirstActualTypeArgument(field.getGenericType())));
} else if (!resolvesToBuiltinBean(field.getType())) {
componentClasses.add(field.getType());
}
collectComponents(current, addNestedClassesAsComponents, componentClasses);
current = current.getSuperclass();
}

// @TestConfigProperty annotations
for (TestConfigProperty testConfigProperty : testClass.getAnnotationsByType(TestConfigProperty.class)) {
configProperties.put(testConfigProperty.key(), testConfigProperty.value());
}

return new QuarkusComponentTestConfiguration(Map.copyOf(configProperties), Set.copyOf(componentClasses),
this.mockConfigurators, useDefaultConfigProperties, addNestedClassesAsComponents, configSourceOrdinal,
List.copyOf(annotationsTransformers), List.copyOf(configConverters), configBuilderCustomizer);
}

private static void collectComponents(Class<?> testClass, boolean addNestedClassesAsComponents,
List<Class<?>> componentClasses) {
// All fields annotated with @Inject represent component classes
for (Field field : testClass.getDeclaredFields()) {
if (field.isAnnotationPresent(Inject.class)) {
if (Instance.class.isAssignableFrom(field.getType())
|| QuarkusComponentTestExtension.isListAllInjectionPoint(field.getGenericType(),
field.getAnnotations(),
field)) {
// Special handling for Instance<Foo> and @All List<Foo>
componentClasses
.add(getRawType(
QuarkusComponentTestExtension.getFirstActualTypeArgument(field.getGenericType())));
} else if (!resolvesToBuiltinBean(field.getType())) {
componentClasses.add(field.getType());
}
}
// All static nested classes declared on the test class are components
if (addNestedClassesAsComponents) {
for (Class<?> declaredClass : current.getDeclaredClasses()) {
if (Modifier.isStatic(declaredClass.getModifiers())) {
componentClasses.add(declaredClass);
}
}
// All static nested classes declared on the test class are components
if (addNestedClassesAsComponents) {
for (Class<?> declaredClass : testClass.getDeclaredClasses()) {
if (Modifier.isStatic(declaredClass.getModifiers())) {
componentClasses.add(declaredClass);
}
}
// All params of test methods but:
// - not covered by built-in extensions
// - not annotated with @InjectMock, @SkipInject, @org.mockito.Mock
for (Method method : current.getDeclaredMethods()) {
if (QuarkusComponentTestExtension.isTestMethod(method)) {
for (Parameter param : method.getParameters()) {
if (QuarkusComponentTestExtension.BUILTIN_PARAMETER.test(param)
|| param.isAnnotationPresent(InjectMock.class)
|| param.isAnnotationPresent(SkipInject.class)
|| param.isAnnotationPresent(Mock.class)) {
continue;
}
if (Instance.class.isAssignableFrom(param.getType())
|| QuarkusComponentTestExtension.isListAllInjectionPoint(param.getParameterizedType(),
param.getAnnotations(),
param)) {
// Special handling for Instance<Foo> and @All List<Foo>
componentClasses.add(getRawType(
QuarkusComponentTestExtension.getFirstActualTypeArgument(param.getParameterizedType())));
} else {
componentClasses.add(param.getType());
}
}
// All params of test methods but:
// - not covered by built-in extensions
// - not annotated with @InjectMock, @SkipInject, @org.mockito.Mock
for (Method method : testClass.getDeclaredMethods()) {
if (QuarkusComponentTestExtension.isTestMethod(method)) {
for (Parameter param : method.getParameters()) {
if (QuarkusComponentTestExtension.BUILTIN_PARAMETER.test(param)
|| param.isAnnotationPresent(InjectMock.class)
|| param.isAnnotationPresent(SkipInject.class)
|| param.isAnnotationPresent(Mock.class)) {
continue;
}
if (Instance.class.isAssignableFrom(param.getType())
|| QuarkusComponentTestExtension.isListAllInjectionPoint(param.getParameterizedType(),
param.getAnnotations(),
param)) {
// Special handling for Instance<Foo> and @All List<Foo>
componentClasses.add(getRawType(
QuarkusComponentTestExtension.getFirstActualTypeArgument(param.getParameterizedType())));
} else {
componentClasses.add(param.getType());
}
}
}
current = current.getSuperclass();
}

List<TestConfigProperty> testConfigProperties = new ArrayList<>();
Collections.addAll(testConfigProperties, testClass.getAnnotationsByType(TestConfigProperty.class));
for (TestConfigProperty testConfigProperty : testConfigProperties) {
configProperties.put(testConfigProperty.key(), testConfigProperty.value());
// All @Nested inner classes
for (Class<?> nested : testClass.getDeclaredClasses()) {
if (nested.isAnnotationPresent(Nested.class) && !Modifier.isStatic(nested.getModifiers())) {
collectComponents(nested, addNestedClassesAsComponents, componentClasses);
}
}

return new QuarkusComponentTestConfiguration(Map.copyOf(configProperties), Set.copyOf(componentClasses),
this.mockConfigurators, useDefaultConfigProperties, addNestedClassesAsComponents, configSourceOrdinal,
List.copyOf(annotationsTransformers), List.copyOf(configConverters), configBuilderCustomizer);
}

QuarkusComponentTestConfiguration update(Method testMethod) {
Expand Down
Loading

0 comments on commit 0d8bf11

Please sign in to comment.