Skip to content

Commit

Permalink
QuarkusComponentTest: support test method parameter injection
Browse files Browse the repository at this point in the history
- params annotated with SkipInject are never injected

Co-authored-by: Ladislav Thon <[email protected]>
  • Loading branch information
mkouba and Ladicek committed Jan 25, 2024
1 parent b0199e8 commit 85bd285
Show file tree
Hide file tree
Showing 12 changed files with 512 additions and 51 deletions.
39 changes: 36 additions & 3 deletions docs/src/main/asciidoc/getting-started-testing.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -1596,8 +1596,35 @@ public class FooTest {
<4> The test also injects `Charlie`, a dependency for which a synthetic `@Singleton` bean is registered automatically. The injected reference is an "unconfigured" Mockito mock.
<5> We can leverage the Mockito API in a test method to configure the behavior.

If you need the full control over the `QuarkusComponentTestExtension` configuration then you can use the `@RegisterExtension` annotation and configure the extension programatically.
The test above could be rewritten like:
`QuarkusComponentTestExtension` also resolves parameters of test methods and injects matching beans.
So the code snippet above can be rewritten as:

[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
@TestConfigProperty(key = "bar", value = "true")
public class FooTest {
@Test
public void testPing(Foo foo, @InjectMock Charlie charlieMock) { <1>
Mockito.when(charlieMock.ping()).thenReturn("OK");
assertEquals("OK", foo.ping());
}
}
----
<1> Parameters annotated with `@io.quarkus.test.component.SkipInject` are never resolved by this extension.

Furthermore, if you need the full control over the `QuarkusComponentTestExtension` configuration then you can use the `@RegisterExtension` annotation and configure the extension programatically.
The original test could be rewritten like:

[source, java]
----
Expand Down Expand Up @@ -1638,13 +1665,19 @@ However, if the test instance lifecycle is `Lifecycle#PER_CLASS` then the conta
The fields annotated with `@Inject` and `@InjectMock` are injected after a test instance is created.
Finally, the CDI request context is activated and terminated per each test method.

=== Injection
Test class fields annotated with `@jakarta.inject.Inject` and `@io.quarkus.test.InjectMock` are injected after a test instance is created.
Dependent beans injected into these fields are correctly destroyed before a test instance is destroyed.
Parameters of a test method for which a matching bean exists are resolved unless annotated with `@io.quarkus.test.component.SkipInject`.
Dependent beans injected into the test method arguments are correctly destroyed after the test method completes.

=== Auto Mocking Unsatisfied Dependencies

Unlike in regular CDI environments the test does not fail if a component injects an unsatisfied dependency.
Instead, a synthetic bean is registered automatically for each combination of required type and qualifiers of an injection point that resolves to an unsatisfied dependency.
The bean has the `@Singleton` scope so it's shared across all injection points with the same required type and qualifiers.
The injected reference is an _unconfigured_ Mockito mock.
You can inject the mock in your test and leverage the Mockito API to configure the behavior.
You can inject the mock in your test using the `io.quarkus.test.InjectMock` annotation and leverage the Mockito API to configure the behavior.

=== Custom Mocks For Unsatisfied Dependencies

Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
package io.quarkus.test;

import static java.lang.annotation.ElementType.FIELD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

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

/**
* Instructs the test engine to inject a mock instance of a bean in the field of a test class.
* Instructs the test engine to inject a mock instance of a bean into the annotated field or parameter.
* <p>
* This annotation is supported:
* <ul>
* <li>in a {@code io.quarkus.test.component.QuarkusComponentTest},</li>
* <li>in a {@code io.quarkus.test.QuarkusTest} if {@code quarkus-junit5-mockito} is present.</li>
* <li>for fields and method parameters in a {@code io.quarkus.test.component.QuarkusComponentTest},</li>
* <li>for fields in a {@code io.quarkus.test.QuarkusTest} if {@code quarkus-junit5-mockito} is present.</li>
* </ul>
* The lifecycle and configuration API of the injected mock depends on the type of test being used.
* <p>
Expand All @@ -21,7 +22,7 @@
* {@code io.quarkus.test.junit.QuarkusTest}.
*/
@Retention(RUNTIME)
@Target(FIELD)
@Target({ FIELD, PARAMETER })
public @interface InjectMock {

}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@
* The set of additional components under test.
* <p>
* The initial set of components is derived from the test class. The types of all fields annotated with
* {@link jakarta.inject.Inject} are considered the component types.
* {@link jakarta.inject.Inject} are considered the component types. Furthermore, all types of parameters of test methods
* that are not annotated with {@link InjectMock} or {@link SkipInject} are also considered the component types.
*
* @return the components under test
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Parameter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
Expand All @@ -17,9 +18,12 @@
import jakarta.inject.Provider;

import org.jboss.logging.Logger;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInfo;

import io.quarkus.arc.InjectableInstance;
import io.quarkus.arc.processor.AnnotationsTransformer;
import io.quarkus.test.InjectMock;

class QuarkusComponentTestConfiguration {

Expand Down Expand Up @@ -74,23 +78,36 @@ QuarkusComponentTestConfiguration update(Class<?> testClass) {
}
}
}
// All fields annotated with @Inject represent component classes
Class<?> current = testClass;
while (current != null) {
while (current != null && current != Object.class) {
// All fields annotated with @Inject represent component classes
for (Field field : current.getDeclaredFields()) {
if (field.isAnnotationPresent(Inject.class) && !resolvesToBuiltinBean(field.getType())) {
componentClasses.add(field.getType());
}
}
current = current.getSuperclass();
}
// 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 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 params of test methods but not TestInfo, not annotated with @InjectMock and not annotated with @SkipInject
for (Method method : current.getDeclaredMethods()) {
if (method.isAnnotationPresent(Test.class)) {
for (Parameter param : method.getParameters()) {
if (param.getType() == TestInfo.class
|| param.isAnnotationPresent(InjectMock.class)
|| param.isAnnotationPresent(SkipInject.class)) {
continue;
}
componentClasses.add(param.getType());
}
}
}
current = current.getSuperclass();
}

List<TestConfigProperty> testConfigProperties = new ArrayList<>();
Expand Down
Loading

0 comments on commit 85bd285

Please sign in to comment.