Skip to content

Commit

Permalink
Merge pull request #34239 from mkouba/componenttest-interceptor-methods
Browse files Browse the repository at this point in the history
QuarkusComponentTest: convenient way of mocking interceptors
  • Loading branch information
mkouba authored Jun 26, 2023
2 parents 07347f8 + 6245f00 commit 4c4623c
Show file tree
Hide file tree
Showing 6 changed files with 447 additions and 38 deletions.
102 changes: 98 additions & 4 deletions docs/src/main/asciidoc/getting-started-testing.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -1573,8 +1573,8 @@ public class FooTest {
}
----
<1> The `QuarkusComponentTest` annotation registers the JUnit extension.
<2> Set a configuration property for the test.
<3> The test injects the component under the test. The types of all fields annotated with `@Inject` are considered the component types under test. You can also specify additional component classes via `@QuarkusComponentTest#value()`.
<2> Sets a configuration property for the test.
<3> The test injects the component under the test. The types of all fields annotated with `@Inject` are considered the component types under test. You can also specify additional component classes via `@QuarkusComponentTest#value()`. Furthermore, the static nested classes declared on the test class are components too.
<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.

Expand Down Expand Up @@ -1609,7 +1609,7 @@ public class FooTest {
}
}
----
<1> The `QuarkusComponentTestExtension` is configured in a static field.
<1> The `QuarkusComponentTestExtension` is configured in a static field of the test class.

=== Lifecycle

Expand Down Expand Up @@ -1638,4 +1638,98 @@ You can use the mock configurator API via the `QuarkusComponentTestExtension#moc

A dedicated `SmallRyeConfig` is registered during the `before all` test phase.
Moreover, it's possible to set the configuration properties via the `QuarkusComponentTestExtension#configProperty(String, String)` method or the `@TestConfigProperty` annotation.
If you only need to use the default values for missing config properties, then the `QuarkusComponentTestExtension#useDefaultConfigProperties()` or `@QuarkusComponentTest#useDefaultConfigProperties()` might come in useful.
If you only need to use the default values for missing config properties, then the `QuarkusComponentTestExtension#useDefaultConfigProperties()` or `@QuarkusComponentTest#useDefaultConfigProperties()` might come in useful.

=== Mocking CDI Interceptors

If a tested component class declares an interceptor binding then you might need to mock the interception too.
There are two ways to accomplish this task.
First, you can define an interceptor class as a static nested class of the test class.

[source, java]
----
import static org.junit.jupiter.api.Assertions.assertEquals;
import jakarta.inject.Inject;
import io.quarkus.test.component.QuarkusComponentTest;
import org.junit.jupiter.api.Test;
@QuarkusComponentTest
public class FooTest {
@Inject
Foo foo;
@Test
public void testPing() {
assertEquals("OK", foo.ping());
}
@ApplicationScoped
static class Foo {
@SimpleBinding <1>
String ping() {
return "ok";
}
}
@SimpleBinding
@Interceptor
static class SimpleInterceptor { <2>
@AroundInvoke
Object aroundInvoke(InvocationContext context) throws Exception {
return context.proceed().toString().toUpperCase();
}
}
}
----
<1> `@SimpleBinding` is an interceptor binding.
<2> The interceptor class is automatically considered a tested component.

NOTE: Static nested classed declared on a test class that is annotated with `@QuarkusComponentTest` are excluded from bean discovery when running a `@QuarkusTest` in order to prevent unintentional CDI conflicts.

Furthermore, you can also declare a "test interceptor method" directly on the test class.
This method is then invoked in the relevant interception phase.

[source, java]
----
import static org.junit.jupiter.api.Assertions.assertEquals;
import jakarta.inject.Inject;
import io.quarkus.test.component.QuarkusComponentTest;
import org.junit.jupiter.api.Test;
@QuarkusComponentTest
public class FooTest {
@Inject
Foo foo;
@Test
public void testPing() {
assertEquals("OK", foo.ping());
}
@SimpleBinding <1>
@AroundInvoke <2>
Object aroundInvoke(InvocationContext context) throws Exception {
return context.proceed().toString().toUpperCase();
}
@ApplicationScoped
static class Foo {
@SimpleBinding <1>
String ping() {
return "ok";
}
}
}
----
<1> The interceptor bindings of the resulting interceptor are specified by annotating the method with the interceptor binding types.
<2> Defines the interception type.
Original file line number Diff line number Diff line change
Expand Up @@ -409,7 +409,8 @@ public boolean equals(Object obj) {
if (other.qualifiers != null) {
return false;
}
} else if (!qualifiers.equals(other.qualifiers)) {
} else if (!qualifiersAreEqual(qualifiers, other.qualifiers)) {
// We cannot use AnnotationInstance#equals() as it requires the exact same annotationTarget instance
return false;
}
if (type == null) {
Expand All @@ -422,6 +423,30 @@ public boolean equals(Object obj) {
return true;
}

private boolean qualifiersAreEqual(Set<AnnotationInstance> q1, Set<AnnotationInstance> q2) {
if (q1 == q2) {
return true;
}
if (q1.size() != q2.size()) {
return false;
}
for (AnnotationInstance a1 : q1) {
for (AnnotationInstance a2 : q2) {
if (!annotationsAreEqual(a1, a2)) {
return false;
}
}
}
return true;
}

private boolean annotationsAreEqual(AnnotationInstance a1, AnnotationInstance a2) {
if (a1 == a2) {
return true;
}
return a1.name().equals(a2.name()) && a1.values().equals(a2.values());
}

}

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

import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;

import io.quarkus.arc.InterceptorCreator;
import io.quarkus.arc.SyntheticCreationalContext;

public class InterceptorMethodCreator implements InterceptorCreator {

static final String CREATE_KEY = "createKey";

private static final Map<String, Function<SyntheticCreationalContext<?>, InterceptFunction>> createFunctions = new HashMap<>();

@Override
public InterceptFunction create(SyntheticCreationalContext<Object> context) {
Object createKey = context.getParams().get(CREATE_KEY);
if (createKey != null) {
Function<SyntheticCreationalContext<?>, InterceptFunction> createFun = createFunctions.get(createKey);
if (createFun != null) {
return createFun.apply(context);
}
}
throw new IllegalStateException("Create function not found: " + createKey);
}

static void registerCreate(String key, Function<SyntheticCreationalContext<?>, InterceptFunction> create) {
createFunctions.put(key, create);
}

static void clear() {
createFunctions.clear();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,17 @@

public class MockBeanCreator implements BeanCreator<Object> {

static final String CREATE_KEY = "createKey";

private static final Logger LOG = Logger.getLogger(MockBeanCreator.class);

private static final Map<String, Function<SyntheticCreationalContext<?>, ?>> createFunctions = new HashMap<>();

@Override
public Object create(SyntheticCreationalContext<Object> context) {
Object createKey = context.getParams().get("createKey");
Object createKey = context.getParams().get(CREATE_KEY);
if (createKey != null) {
Function<SyntheticCreationalContext<?>, ?> createFun = createFunctions.get(createKey.toString());
Function<SyntheticCreationalContext<?>, ?> createFun = createFunctions.get(createKey);
if (createFun != null) {
return createFun.apply(context);
} else {
Expand Down
Loading

0 comments on commit 4c4623c

Please sign in to comment.