Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Provide easy way for QuarkusTestResourceLifecycleManager implementations to inject into test #18714

Merged
merged 1 commit into from
Jul 16, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.
FroMage marked this conversation as resolved.
Show resolved Hide resolved
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