Skip to content

Commit

Permalink
Provide easy way for QuarkusTestResourceLifecycleManager impls to inj…
Browse files Browse the repository at this point in the history
…ect into test

Resolves: #18698
  • Loading branch information
geoand committed Jul 16, 2021
1 parent c5ba24e commit 2be876f
Show file tree
Hide file tree
Showing 8 changed files with 285 additions and 55 deletions.
59 changes: 58 additions & 1 deletion docs/src/main/asciidoc/getting-started-testing.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -993,11 +993,68 @@ A test suite is also free to utilize multiple `@QuarkusTestResource` annotations
NOTE: test resources are global, even if they are defined on a test class or custom profile, which means they will all be activated for all tests, even though we do
remove duplicates. If you want to only enable a test resource on a single test class or test profile, you can use `@QuarkusTestResource(restrictToAnnotatedClass = true)`.

Quarkus provides a few implementations of `QuarkusTestResourceLifecycleManager` out of the box (see `io.quarkus.test.h2.H2DatabaseTestResource` which starts an H2 database, or `io.quarkus.test.kubernetes.client.KubernetesMockServerTestResource` which starts a mock Kubernetes API server),
Quarkus provides a few implementations of `QuarkusTestResourceLifecycleManager` out of the box (see `io.quarkus.test.h2.H2DatabaseTestResource` which starts an H2 database, or `io.quarkus.test.kubernetes.client.KubernetesServerTestResource` which starts a mock Kubernetes API server),
but it is common to create custom implementations to address specific application needs.
Common cases include starting docker containers using https://www.testcontainers.org/[Testcontainers] (an example of which can be found https://github.com/quarkusio/quarkus-quickstarts/blob/main/kafka-quickstart/src/test/java/org/acme/kafka/KafkaResource.java[here]),
or starting a mock HTTP server using http://wiremock.org/[Wiremock] (an example of which can be found https://github.com/geoand/quarkus-test-demo/blob/main/src/test/java/org/acme/getting/started/country/WiremockCountries.java[here]).


=== Altering the test class
When creating a custom `QuarkusTestResourceLifecycleManager` that needs to inject the something into the test class, the `inject` methods can be used.
If for example you have a test like the following:

[source,java]
----
@QuarkusTest
@QuarkusTestResource(MyWireMockResource.class)
public class MyTest {
@InjectWireMock // this a custom annotation you are defining in your own application
WireMockServer wireMockServer;
@Test
public someTest() {
// control wiremock in some way and perform test
}
}
----

Making `MyWireMockResource` inject the `wireMockServer` field can be done as shown in the `inject` method of the following code snippet:

[source,java]
----
public class MyWireMockResource implements QuarkusTestResourceLifecycleManager {
WireMockServer wireMockServer;
@Override
public Map<String, String> start() {
wireMockServer = new WireMockServer(8090);
wireMockServer.start();
// create some stubs
return Map.of("some.service.url", "localhost:" + wireMockServer.port());
}
@Override
public synchronized void stop() {
if (wireMockServer != null) {
wireMockServer.stop();
wireMockServer = null;
}
}
@Override
public void inject(TestInjector testInjector) {
testInjector.injectIntoFields(wireMockServer, new TestInjector.AnnotatedAndMatchesType(InjectWireMock.class, WireMockServer.class));
}
}
----

IMPORTANT: It is worth mentioning that this injection into the test class is not under the control of CDI and happens after CDI has performed
any necessary injections into the test class.

=== Annotation-based test resources

It is possible to write test resources that are enabled and configured using annotations. This is enabled by placing the `@QuarkusTestResource`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@
import static com.github.tomakehurst.wiremock.client.WireMock.any;
import static com.github.tomakehurst.wiremock.client.WireMock.anyUrl;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.Map;

import com.github.tomakehurst.wiremock.WireMockServer;
Expand Down Expand Up @@ -34,27 +32,9 @@ public Map<String, String> start() {
}

@Override
public void inject(Object testInstance) {
Class<?> c = testInstance.getClass();
Class<? extends Annotation> annotation = InjectWireMock.class;
Class<?> injectedClass = WireMockServer.class;
while (c != Object.class) {
for (Field f : c.getDeclaredFields()) {
if (f.getAnnotation(annotation) != null) {
if (!injectedClass.isAssignableFrom(f.getType())) {
throw new RuntimeException(annotation + " can only be used on fields of type " + injectedClass);
}
f.setAccessible(true);
try {
f.set(testInstance, wireMockServer);
return;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
c = c.getSuperclass();
}
public void inject(TestInjector testInjector) {
testInjector.injectIntoFields(wireMockServer,
new TestInjector.AnnotatedAndMatchesType(InjectWireMock.class, WireMockServer.class));
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package io.quarkus.test.common;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.Map;
import java.util.function.Predicate;

/**
* Manage the lifecycle of a test resource, for instance a H2 test server.
Expand Down Expand Up @@ -47,9 +50,31 @@ default void init(Map<String, String> initArgs) {
}

/**
* Allow each resource to provide custom injection of fields of the test class
* Allow each resource to provide custom injection of fields of the test class.
*
* Most implementations will likely use {@link QuarkusTestResourceLifecycleManager#inject(TestInjector)}
* as it provides a simpler way to inject into fields of tests.
*
* It is worth mentioning that this injection into the test class is not under the control of CDI and happens after CDI has
* performed
* any necessary injections into the test class.
*/
default void inject(Object testInstance) {

}

/**
* Simplifies the injection of fields of the test class by providing methods to handle the common injection cases.
*
* In situations not covered by {@link TestInjector}, user can resort to implementing
* {@link QuarkusTestResourceLifecycleManager#inject(Object)}
*
* It is worth mentioning that this injection into the test class is not under the control of CDI and happens after CDI has
* performed
* any necessary injections into the test class.
*/
default void inject(TestInjector testInjector) {

}

/**
Expand All @@ -61,4 +86,58 @@ default void inject(Object testInstance) {
default int order() {
return 0;
}

/**
* Provides methods to handle the common injection cases. See
* {@link QuarkusTestResourceLifecycleManager#inject(TestInjector)}
*/
interface TestInjector {

/**
* @param fieldValue The actual value to inject into a test field
* @param predicate User supplied predicate which can be used to determine whether or not the field should be
* set with with {@code fieldValue}
*/
void injectIntoFields(Object fieldValue, Predicate<Field> predicate);

/**
* Returns {@code true} if the field is annotated with the supplied annotation.
*/
class Annotated implements Predicate<Field> {

private final Class<? extends Annotation> annotationClass;

public Annotated(Class<? extends Annotation> annotationClass) {
this.annotationClass = annotationClass;
}

@Override
public boolean test(Field field) {
return field.getAnnotation(annotationClass) != null;
}
}

/**
* Returns {@code true} if the field is annotated with the supplied annotation and can also be assigned
* to the supplied type.
*/
class AnnotatedAndMatchesType implements Predicate<Field> {

private final Class<? extends Annotation> annotationClass;
private final Class<?> expectedFieldType;

public AnnotatedAndMatchesType(Class<? extends Annotation> annotationClass, Class<?> expectedFieldType) {
this.annotationClass = annotationClass;
this.expectedFieldType = expectedFieldType;
}

@Override
public boolean test(Field field) {
if (field.getAnnotation(annotationClass) == null) {
return false;
}
return field.getType().isAssignableFrom(expectedFieldType);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import java.io.Closeable;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collection;
Expand All @@ -20,6 +21,7 @@
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.function.Predicate;

import org.eclipse.microprofile.config.spi.ConfigProviderResolver;
import org.jboss.jandex.AnnotationInstance;
Expand Down Expand Up @@ -141,7 +143,9 @@ private ExecutorService newExecutor(int poolSize) {

public void inject(Object testInstance) {
for (TestResourceEntry entry : allTestResourceEntries) {
entry.getTestResource().inject(testInstance);
QuarkusTestResourceLifecycleManager quarkusTestResourceLifecycleManager = entry.getTestResource();
quarkusTestResourceLifecycleManager.inject(testInstance);
quarkusTestResourceLifecycleManager.inject(new DefaultTestInjector(testInstance));
}
}

Expand Down Expand Up @@ -459,4 +463,38 @@ public Annotation getConfigAnnotation() {
}
}

// visible for testing
static class DefaultTestInjector implements QuarkusTestResourceLifecycleManager.TestInjector {

// visible for testing
final Object testInstance;

private DefaultTestInjector(Object testInstance) {
this.testInstance = testInstance;
}

@Override
public void injectIntoFields(Object fieldValue, Predicate<Field> predicate) {
Class<?> c = testInstance.getClass();
while (c != Object.class) {
for (Field f : c.getDeclaredFields()) {
if (predicate.test(f)) {
f.setAccessible(true);
try {
f.set(testInstance, fieldValue);
return;
} catch (Exception e) {
throw new RuntimeException("Unable to set field '" + f.getName()
+ "' using 'QuarkusTestResourceLifecycleManager.TestInjector' ", e);
}
}
}
c = c.getSuperclass();
}
System.err.println("Unable to determine matching fields for injection of test instance '"
+ testInstance.getClass().getName() + "'");
}

}

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

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

import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.util.Collections;
import java.util.Map;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

public class TestResourceManagerInjectorTest {

@Test
void testTestInjector() {
TestResourceManager manager = new TestResourceManager(UsingInjectorTest.class);
manager.start();

Foo foo = new Foo();
manager.inject(foo);

Assertions.assertNotNull(foo.bar);
Assertions.assertEquals("bar", foo.bar.value);
Assertions.assertNotNull(foo.dummy);
Assertions.assertEquals("dummy", foo.dummy.value);
}

@QuarkusTestResource(UsingTestInjectorLifecycleManager.class)
public static class UsingInjectorTest {
}

public static class UsingTestInjectorLifecycleManager implements QuarkusTestResourceLifecycleManager {

@Override
public Map<String, String> start() {
return Collections.emptyMap();
}

@Override
public void stop() {

}

@Override
public void inject(TestInjector testInjector) {
TestResourceManager.DefaultTestInjector defaultTestInjector = (TestResourceManager.DefaultTestInjector) testInjector;
if (defaultTestInjector.testInstance instanceof Foo) {
testInjector.injectIntoFields(new Bar("bar"), (f) -> f.getType().isAssignableFrom(BarBase.class));
testInjector.injectIntoFields(new Dummy("dummy"), (f) -> f.getType().equals(Dummy.class));
}
}
}

public static abstract class BarBase {
public final String value;

public BarBase(String value) {
this.value = value;
}
}

public static class Bar extends BarBase {
public Bar(String value) {
super(value);
}
}

public static class Dummy {
public final String value;

public Dummy(String value) {
this.value = value;
}
}

public static class Foo {
BarBase bar;
@InjectDummy
Dummy dummy;
}

@Target({ FIELD })
@Retention(RUNTIME)
public @interface InjectDummy {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,10 @@ public Map<String, String> start() {
}

@Override
public void inject(Object testInstance) {
((AtomicInteger) testInstance).incrementAndGet();
public void inject(Object instance) {
if (instance instanceof AtomicInteger) {
((AtomicInteger) instance).incrementAndGet();
}
}

@Override
Expand All @@ -71,8 +73,10 @@ public Map<String, String> start() {
}

@Override
public void inject(Object testInstance) {
((AtomicInteger) testInstance).incrementAndGet();
public void inject(Object instance) {
if (instance instanceof AtomicInteger) {
((AtomicInteger) instance).incrementAndGet();
}
}

@Override
Expand Down
Loading

0 comments on commit 2be876f

Please sign in to comment.