diff --git a/docs/src/main/asciidoc/getting-started-testing.adoc b/docs/src/main/asciidoc/getting-started-testing.adoc index 7989be7187b05..84b5e51d60d3c 100644 --- a/docs/src/main/asciidoc/getting-started-testing.adoc +++ b/docs/src/main/asciidoc/getting-started-testing.adoc @@ -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 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` diff --git a/integration-tests/hibernate-search-orm-elasticsearch-aws/src/test/java/io/quarkus/it/hibernate/search/elasticsearch/aws/WireMockElasticsearchProxyTestResource.java b/integration-tests/hibernate-search-orm-elasticsearch-aws/src/test/java/io/quarkus/it/hibernate/search/elasticsearch/aws/WireMockElasticsearchProxyTestResource.java index b4cdaeb695ee2..d92c205be4909 100644 --- a/integration-tests/hibernate-search-orm-elasticsearch-aws/src/test/java/io/quarkus/it/hibernate/search/elasticsearch/aws/WireMockElasticsearchProxyTestResource.java +++ b/integration-tests/hibernate-search-orm-elasticsearch-aws/src/test/java/io/quarkus/it/hibernate/search/elasticsearch/aws/WireMockElasticsearchProxyTestResource.java @@ -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; @@ -34,27 +32,9 @@ public Map start() { } @Override - public void inject(Object testInstance) { - Class c = testInstance.getClass(); - Class 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 diff --git a/test-framework/common/src/main/java/io/quarkus/test/common/QuarkusTestResourceLifecycleManager.java b/test-framework/common/src/main/java/io/quarkus/test/common/QuarkusTestResourceLifecycleManager.java index 492a558bea593..51553fbc14ec8 100644 --- a/test-framework/common/src/main/java/io/quarkus/test/common/QuarkusTestResourceLifecycleManager.java +++ b/test-framework/common/src/main/java/io/quarkus/test/common/QuarkusTestResourceLifecycleManager.java @@ -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. @@ -47,9 +50,31 @@ default void init(Map 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) { + } /** @@ -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 predicate); + + /** + * Returns {@code true} if the field is annotated with the supplied annotation. + */ + class Annotated implements Predicate { + + private final Class annotationClass; + + public Annotated(Class 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 { + + private final Class annotationClass; + private final Class expectedFieldType; + + public AnnotatedAndMatchesType(Class 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); + } + } + } } diff --git a/test-framework/common/src/main/java/io/quarkus/test/common/TestResourceManager.java b/test-framework/common/src/main/java/io/quarkus/test/common/TestResourceManager.java index 473d0692a1c1d..617a47e2208dc 100644 --- a/test-framework/common/src/main/java/io/quarkus/test/common/TestResourceManager.java +++ b/test-framework/common/src/main/java/io/quarkus/test/common/TestResourceManager.java @@ -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; @@ -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; @@ -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)); } } @@ -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 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() + "'"); + } + + } + } diff --git a/test-framework/common/src/test/java/io/quarkus/test/common/TestResourceManagerInjectorTest.java b/test-framework/common/src/test/java/io/quarkus/test/common/TestResourceManagerInjectorTest.java new file mode 100644 index 0000000000000..24d1e48edf1a5 --- /dev/null +++ b/test-framework/common/src/test/java/io/quarkus/test/common/TestResourceManagerInjectorTest.java @@ -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 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 { + } +} diff --git a/test-framework/common/src/test/java/io/quarkus/test/common/TestResourceManagerTest.java b/test-framework/common/src/test/java/io/quarkus/test/common/TestResourceManagerTest.java index 52890d8bdd7d0..7b6ae8c9e87c9 100644 --- a/test-framework/common/src/test/java/io/quarkus/test/common/TestResourceManagerTest.java +++ b/test-framework/common/src/test/java/io/quarkus/test/common/TestResourceManagerTest.java @@ -53,8 +53,10 @@ public Map start() { } @Override - public void inject(Object testInstance) { - ((AtomicInteger) testInstance).incrementAndGet(); + public void inject(Object instance) { + if (instance instanceof AtomicInteger) { + ((AtomicInteger) instance).incrementAndGet(); + } } @Override @@ -71,8 +73,10 @@ public Map start() { } @Override - public void inject(Object testInstance) { - ((AtomicInteger) testInstance).incrementAndGet(); + public void inject(Object instance) { + if (instance instanceof AtomicInteger) { + ((AtomicInteger) instance).incrementAndGet(); + } } @Override diff --git a/test-framework/kubernetes-client/src/main/java/io/quarkus/test/kubernetes/client/AbstractKubernetesTestResource.java b/test-framework/kubernetes-client/src/main/java/io/quarkus/test/kubernetes/client/AbstractKubernetesTestResource.java index 7d74e3433ada7..14898cf3c4ac7 100644 --- a/test-framework/kubernetes-client/src/main/java/io/quarkus/test/kubernetes/client/AbstractKubernetesTestResource.java +++ b/test-framework/kubernetes-client/src/main/java/io/quarkus/test/kubernetes/client/AbstractKubernetesTestResource.java @@ -1,7 +1,6 @@ package io.quarkus.test.kubernetes.client; import java.lang.annotation.Annotation; -import java.lang.reflect.Field; import java.util.HashMap; import java.util.Map; @@ -57,28 +56,9 @@ protected boolean useHttps() { } @Override - public void inject(Object testInstance) { - Class c = testInstance.getClass(); - Class annotation = getInjectionAnnotation(); - Class injectedClass = getInjectedClass(); - 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, server); - return; - } catch (Exception e) { - throw new RuntimeException(e); - } - } - } - c = c.getSuperclass(); - } + public void inject(TestInjector testInjector) { + testInjector.injectIntoFields(server, + new TestInjector.AnnotatedAndMatchesType(getInjectionAnnotation(), getInjectedClass())); } protected abstract Class getInjectedClass(); diff --git a/test-framework/openshift-client/src/main/java/io/quarkus/test/kubernetes/client/OpenShiftMockServerTestResource.java b/test-framework/openshift-client/src/main/java/io/quarkus/test/kubernetes/client/OpenShiftMockServerTestResource.java index 4a70f9a3d6dcc..e5a2477a6604b 100644 --- a/test-framework/openshift-client/src/main/java/io/quarkus/test/kubernetes/client/OpenShiftMockServerTestResource.java +++ b/test-framework/openshift-client/src/main/java/io/quarkus/test/kubernetes/client/OpenShiftMockServerTestResource.java @@ -1,11 +1,15 @@ package io.quarkus.test.kubernetes.client; -import io.fabric8.kubernetes.client.server.mock.KubernetesMockServer; import io.fabric8.openshift.client.server.mock.OpenShiftMockServer; public class OpenShiftMockServerTestResource extends KubernetesMockServerTestResource { - protected KubernetesMockServer createMockServer() { + protected OpenShiftMockServer createMockServer() { return new OpenShiftMockServer(useHttps()); } + + @Override + protected Class getInjectedClass() { + return OpenShiftMockServer.class; + } }