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 15, 2021
1 parent df49dd5 commit ec38e0a
Show file tree
Hide file tree
Showing 7 changed files with 259 additions and 53 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, WireMockServer.class, InjectWireMock.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 @@ -33,28 +31,10 @@ public Map<String, String> start() {
"quarkus.hibernate-search-orm.elasticsearch.hosts", "localhost:" + wireMockServer.port());
}

@SuppressWarnings("unchecked")
@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, WireMockServer.class, InjectWireMock.class);
}

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

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

/**
Expand Down Expand Up @@ -47,9 +48,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 +84,19 @@ 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 fieldType The field's required type
* @param annotations Optional annotations that the field must have. If more than one are specified, the field
* must be annotated with all the provided annotations
*/
void injectIntoFields(Object fieldValue, Class<?> fieldType, Class<? extends Annotation>... annotations);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

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.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
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,65 @@ 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, Class<?> fieldType, Class<? extends Annotation>... annotations) {
Class<?> c = testInstance.getClass();
while (c != Object.class) {
for (Field f : c.getDeclaredFields()) {
AnnotationMatchResult annotationMatchResult = matchesAnnotations(f, annotations);
if (annotationMatchResult != AnnotationMatchResult.NOT_MATCHED) {
if (!fieldType.isAssignableFrom(f.getType())) {
if (annotationMatchResult == AnnotationMatchResult.MATCHED) {
// if the annotations match, we throw an exception if the types don't match as it's almost certainly a user error
// if however there were no annotation supplied, then we continue searching for candidate fields
throw new RuntimeException("'" + Arrays.toString(annotations)
+ "' can only be used on fields of type " + fieldType);
} else if (annotationMatchResult == AnnotationMatchResult.NONE_NEEDED) {
continue;
}
}

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();
}
}

private AnnotationMatchResult matchesAnnotations(Field field, Class<? extends Annotation>[] annotations) {
if ((annotations == null) || (annotations.length == 0)) {
return AnnotationMatchResult.NONE_NEEDED;
}
for (Class<? extends Annotation> annotation : annotations) {
if (field.getAnnotation(annotation) == null) {
return AnnotationMatchResult.NOT_MATCHED;
}
}
return AnnotationMatchResult.MATCHED;
}

private enum AnnotationMatchResult {
NONE_NEEDED,
MATCHED,
NOT_MATCHED
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
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"), Bar.class);
testInjector.injectIntoFields(new Dummy("dummy"), Dummy.class);
}
}
}

public static class Bar {
public final String value;

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

public static class Dummy {
public final String value;

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

public static class Foo {
Bar 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 ec38e0a

Please sign in to comment.