From 0be1939860d2ac58d3f2ef894c99d3f8c8e119a2 Mon Sep 17 00:00:00 2001 From: Christoph Deppisch Date: Sun, 1 Dec 2024 19:12:33 +0100 Subject: [PATCH] Add Quarkus Testcontainers test resource - Easily add Testcontainers instance to your Quarkus test using annotation based config - Enable users to access Testcontainers instance after container has been started/stopped via listeners - Enable users to supply application properties to the Quarkus application under test (e.g. set connection settings from Testcontainers instance) --- connectors/citrus-testcontainers/pom.xml | 7 + .../actions/StartTestcontainersAction.java | 45 ++- .../aws2/LocalStackContainer.java | 9 +- .../aws2/StartLocalStackAction.java | 4 - .../quarkus/LocalStackContainerResource.java | 66 ++++ .../quarkus/LocalStackContainerSupport.java | 43 +++ .../kafka/quarkus/KafkaContainerResource.java | 56 ++++ .../kafka/quarkus/KafkaContainerSupport.java | 37 ++ .../quarkus/ContainerLifecycleListener.java | 49 +++ .../quarkus/GenericContainerProvider.java | 25 ++ .../quarkus/GenericContainerResource.java | 51 +++ .../quarkus/TestcontainersResource.java | 97 ++++++ .../quarkus/TestcontainersSupport.java | 42 +++ .../quarkus/RedpandaContainerResource.java | 56 ++++ .../quarkus/RedpandaContainerSupport.java | 37 ++ .../StartGenericTestcontainersIT.java | 2 +- pom.xml | 32 +- .../quarkus/app/DemoApplication.java | 10 +- .../src/main/resources/application.properties | 2 +- .../quarkus/app/DemoApplicationTest.java | 13 +- .../src/test/resources/application.properties | 1 + .../ApplicationPropertiesSupplier.java | 31 ++ .../quarkus/CitrusSupport.java | 7 +- .../quarkus/CitrusTestResource.java | 57 +++- runtime/citrus-quarkus/pom.xml | 35 -- src/manual/runtimes-quarkus.adoc | 317 +++++++++++++++++- 26 files changed, 1050 insertions(+), 81 deletions(-) create mode 100644 connectors/citrus-testcontainers/src/main/java/org/citrusframework/testcontainers/aws2/quarkus/LocalStackContainerResource.java create mode 100644 connectors/citrus-testcontainers/src/main/java/org/citrusframework/testcontainers/aws2/quarkus/LocalStackContainerSupport.java create mode 100644 connectors/citrus-testcontainers/src/main/java/org/citrusframework/testcontainers/kafka/quarkus/KafkaContainerResource.java create mode 100644 connectors/citrus-testcontainers/src/main/java/org/citrusframework/testcontainers/kafka/quarkus/KafkaContainerSupport.java create mode 100644 connectors/citrus-testcontainers/src/main/java/org/citrusframework/testcontainers/quarkus/ContainerLifecycleListener.java create mode 100644 connectors/citrus-testcontainers/src/main/java/org/citrusframework/testcontainers/quarkus/GenericContainerProvider.java create mode 100644 connectors/citrus-testcontainers/src/main/java/org/citrusframework/testcontainers/quarkus/GenericContainerResource.java create mode 100644 connectors/citrus-testcontainers/src/main/java/org/citrusframework/testcontainers/quarkus/TestcontainersResource.java create mode 100644 connectors/citrus-testcontainers/src/main/java/org/citrusframework/testcontainers/quarkus/TestcontainersSupport.java create mode 100644 connectors/citrus-testcontainers/src/main/java/org/citrusframework/testcontainers/redpanda/quarkus/RedpandaContainerResource.java create mode 100644 connectors/citrus-testcontainers/src/main/java/org/citrusframework/testcontainers/redpanda/quarkus/RedpandaContainerSupport.java create mode 100644 runtime/citrus-quarkus/citrus-quarkus-it/src/test/resources/application.properties create mode 100644 runtime/citrus-quarkus/citrus-quarkus-runtime/src/main/java/org/citrusframework/quarkus/ApplicationPropertiesSupplier.java diff --git a/connectors/citrus-testcontainers/pom.xml b/connectors/citrus-testcontainers/pom.xml index eee004cf4d..0ac5961d3e 100644 --- a/connectors/citrus-testcontainers/pom.xml +++ b/connectors/citrus-testcontainers/pom.xml @@ -56,6 +56,13 @@ commons-dbcp2 + + + io.quarkus + quarkus-test-common + provided + + software.amazon.awssdk diff --git a/connectors/citrus-testcontainers/src/main/java/org/citrusframework/testcontainers/actions/StartTestcontainersAction.java b/connectors/citrus-testcontainers/src/main/java/org/citrusframework/testcontainers/actions/StartTestcontainersAction.java index 62f4233846..3e4d65b2e7 100644 --- a/connectors/citrus-testcontainers/src/main/java/org/citrusframework/testcontainers/actions/StartTestcontainersAction.java +++ b/connectors/citrus-testcontainers/src/main/java/org/citrusframework/testcontainers/actions/StartTestcontainersAction.java @@ -96,7 +96,7 @@ protected void exposeConnectionSettings(C container, TestContext context) { } } - protected C getContainer() { + public C getContainer() { return container; } @@ -267,37 +267,46 @@ public B withVolumeMount(Resource mountableFile, String mountPath) { protected void prepareBuild() { } - @Override - public T build() { - prepareBuild(); + protected C buildContainer() { + C container = (C) new GenericContainer<>(image); - if (container == null) { - container = (C) new GenericContainer<>(image); - - if (network != null) { - container.withNetwork(network); - if (serviceName != null) { - container.withNetworkAliases(serviceName); - } else if (containerName != null) { - container.withNetworkAliases(containerName); - } + if (network != null) { + container.withNetwork(network); + if (serviceName != null) { + container.withNetworkAliases(serviceName); + } else if (containerName != null) { + container.withNetworkAliases(containerName); } - - container.withStartupTimeout(startupTimeout); } + container.withStartupTimeout(startupTimeout); + + return container; + } + + protected void configureContainer(C container) { container.withLabels(labels); container.withEnv(env); exposedPorts.forEach(container::addExposedPort); container.setPortBindings(portBindings); - volumeMounts.forEach((mountableFile, containerPath) -> - container.withCopyFileToContainer(mountableFile, containerPath)); + volumeMounts.forEach(container::withCopyFileToContainer); if (!commandLine.isEmpty()) { container.withCommand(commandLine.toArray(String[]::new)); } + } + + @Override + public T build() { + prepareBuild(); + + if (container == null) { + container = buildContainer(); + } + + configureContainer(container); if (containerName == null && image != null) { if (image.contains(":")) { diff --git a/connectors/citrus-testcontainers/src/main/java/org/citrusframework/testcontainers/aws2/LocalStackContainer.java b/connectors/citrus-testcontainers/src/main/java/org/citrusframework/testcontainers/aws2/LocalStackContainer.java index 3d0f715831..d1d411fca7 100644 --- a/connectors/citrus-testcontainers/src/main/java/org/citrusframework/testcontainers/aws2/LocalStackContainer.java +++ b/connectors/citrus-testcontainers/src/main/java/org/citrusframework/testcontainers/aws2/LocalStackContainer.java @@ -42,7 +42,7 @@ public class LocalStackContainer extends GenericContainer { private static final String HOSTNAME_EXTERNAL_ENV = "HOSTNAME_EXTERNAL"; private static final String DOCKER_IMAGE_NAME = LocalStackSettings.getImageName(); - private static final String DOCKER_IMAGE_TAG = LocalStackSettings.VERSION_DEFAULT; + private static final String DOCKER_IMAGE_TAG = LocalStackSettings.getVersion(); private final Set services = new HashSet<>(); private String secretKey = "secretkey"; @@ -211,5 +211,12 @@ public String getServiceName() { public static String serviceName(Service service) { return service.serviceName; } + + public static Service fromServiceName(String serviceName) { + return Arrays.stream(Service.values()) + .filter(service -> service.serviceName.equals(serviceName)) + .findFirst() + .orElseThrow(() -> new CitrusRuntimeException("Unknown AWS LocalStack service name: %s".formatted(serviceName))); + } } } diff --git a/connectors/citrus-testcontainers/src/main/java/org/citrusframework/testcontainers/aws2/StartLocalStackAction.java b/connectors/citrus-testcontainers/src/main/java/org/citrusframework/testcontainers/aws2/StartLocalStackAction.java index a46d3f3756..fce9a94183 100644 --- a/connectors/citrus-testcontainers/src/main/java/org/citrusframework/testcontainers/aws2/StartLocalStackAction.java +++ b/connectors/citrus-testcontainers/src/main/java/org/citrusframework/testcontainers/aws2/StartLocalStackAction.java @@ -27,12 +27,8 @@ public class StartLocalStackAction extends StartTestcontainersAction { - private final Set services; - public StartLocalStackAction(Builder builder) { super(builder); - - this.services = builder.services; } @Override diff --git a/connectors/citrus-testcontainers/src/main/java/org/citrusframework/testcontainers/aws2/quarkus/LocalStackContainerResource.java b/connectors/citrus-testcontainers/src/main/java/org/citrusframework/testcontainers/aws2/quarkus/LocalStackContainerResource.java new file mode 100644 index 0000000000..fe0eaf8c3f --- /dev/null +++ b/connectors/citrus-testcontainers/src/main/java/org/citrusframework/testcontainers/aws2/quarkus/LocalStackContainerResource.java @@ -0,0 +1,66 @@ +/* + * Copyright the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citrusframework.testcontainers.aws2.quarkus; + +import java.lang.reflect.InvocationTargetException; +import java.util.Arrays; +import java.util.Map; + +import io.quarkus.test.common.QuarkusTestResourceConfigurableLifecycleManager; +import org.citrusframework.exceptions.CitrusRuntimeException; +import org.citrusframework.testcontainers.aws2.LocalStackContainer; +import org.citrusframework.testcontainers.aws2.StartLocalStackAction; +import org.citrusframework.testcontainers.quarkus.ContainerLifecycleListener; +import org.citrusframework.testcontainers.quarkus.TestcontainersResource; + +public class LocalStackContainerResource extends TestcontainersResource + implements QuarkusTestResourceConfigurableLifecycleManager { + + public static final String SERVICES_INIT_ARG = "aws.localstack.services"; + + public LocalStackContainerResource() { + super(LocalStackContainer.class); + } + + @Override + public void init(LocalStackContainerSupport config) { + for (Class> lifecycleListenerType : + config.containerLifecycleListener()) { + try { + registerContainerLifecycleListener(lifecycleListenerType.getDeclaredConstructor().newInstance()); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + throw new CitrusRuntimeException("Failed to instantiate container lifecycle listener from type: %s" + .formatted(lifecycleListenerType), e); + } + } + + container = new StartLocalStackAction.Builder() + .withServices(config.services()) + .build().getContainer(); + } + + @Override + protected void doInit(Map initArgs) { + String[] serviceNames = initArgs.getOrDefault(SERVICES_INIT_ARG, "").split(","); + container = new StartLocalStackAction.Builder() + .withServices(Arrays.stream(serviceNames) + .map(String::trim) + .map(LocalStackContainer.Service::fromServiceName) + .toArray(LocalStackContainer.Service[]::new)) + .build().getContainer(); + } +} diff --git a/connectors/citrus-testcontainers/src/main/java/org/citrusframework/testcontainers/aws2/quarkus/LocalStackContainerSupport.java b/connectors/citrus-testcontainers/src/main/java/org/citrusframework/testcontainers/aws2/quarkus/LocalStackContainerSupport.java new file mode 100644 index 0000000000..219c875ec5 --- /dev/null +++ b/connectors/citrus-testcontainers/src/main/java/org/citrusframework/testcontainers/aws2/quarkus/LocalStackContainerSupport.java @@ -0,0 +1,43 @@ +/* + * Copyright the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citrusframework.testcontainers.aws2.quarkus; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import io.quarkus.test.common.QuarkusTestResource; +import org.citrusframework.testcontainers.aws2.LocalStackContainer; +import org.citrusframework.testcontainers.quarkus.ContainerLifecycleListener; + +@QuarkusTestResource(LocalStackContainerResource.class) +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface LocalStackContainerSupport { + + /** + * Enabled services. + * @return + */ + LocalStackContainer.Service[] services() default {}; + + /** + * Container lifecycle listeners + */ + Class>[] containerLifecycleListener() default {}; +} diff --git a/connectors/citrus-testcontainers/src/main/java/org/citrusframework/testcontainers/kafka/quarkus/KafkaContainerResource.java b/connectors/citrus-testcontainers/src/main/java/org/citrusframework/testcontainers/kafka/quarkus/KafkaContainerResource.java new file mode 100644 index 0000000000..fff8e982f2 --- /dev/null +++ b/connectors/citrus-testcontainers/src/main/java/org/citrusframework/testcontainers/kafka/quarkus/KafkaContainerResource.java @@ -0,0 +1,56 @@ +/* + * Copyright the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citrusframework.testcontainers.kafka.quarkus; + +import java.lang.reflect.InvocationTargetException; +import java.util.Collections; +import java.util.Map; + +import io.quarkus.test.common.QuarkusTestResourceConfigurableLifecycleManager; +import org.citrusframework.exceptions.CitrusRuntimeException; +import org.citrusframework.testcontainers.kafka.StartKafkaAction; +import org.citrusframework.testcontainers.quarkus.ContainerLifecycleListener; +import org.citrusframework.testcontainers.quarkus.TestcontainersResource; +import org.testcontainers.containers.KafkaContainer; + +public class KafkaContainerResource extends TestcontainersResource + implements QuarkusTestResourceConfigurableLifecycleManager { + + public KafkaContainerResource() { + super(KafkaContainer.class); + } + + @Override + public void init(KafkaContainerSupport config) { + for (Class> lifecycleListenerType : + config.containerLifecycleListener()) { + try { + registerContainerLifecycleListener(lifecycleListenerType.getDeclaredConstructor().newInstance()); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + throw new CitrusRuntimeException("Failed to instantiate container lifecycle listener from type: %s" + .formatted(lifecycleListenerType), e); + } + } + + doInit(Collections.emptyMap()); + } + + @Override + protected void doInit(Map initArgs) { + container = new StartKafkaAction.Builder().build().getContainer(); + } +} diff --git a/connectors/citrus-testcontainers/src/main/java/org/citrusframework/testcontainers/kafka/quarkus/KafkaContainerSupport.java b/connectors/citrus-testcontainers/src/main/java/org/citrusframework/testcontainers/kafka/quarkus/KafkaContainerSupport.java new file mode 100644 index 0000000000..cd02413dd6 --- /dev/null +++ b/connectors/citrus-testcontainers/src/main/java/org/citrusframework/testcontainers/kafka/quarkus/KafkaContainerSupport.java @@ -0,0 +1,37 @@ +/* + * Copyright the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citrusframework.testcontainers.kafka.quarkus; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import io.quarkus.test.common.QuarkusTestResource; +import org.citrusframework.testcontainers.quarkus.ContainerLifecycleListener; +import org.testcontainers.containers.KafkaContainer; + +@QuarkusTestResource(KafkaContainerResource.class) +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface KafkaContainerSupport { + + /** + * Container lifecycle listeners + */ + Class>[] containerLifecycleListener() default {}; +} diff --git a/connectors/citrus-testcontainers/src/main/java/org/citrusframework/testcontainers/quarkus/ContainerLifecycleListener.java b/connectors/citrus-testcontainers/src/main/java/org/citrusframework/testcontainers/quarkus/ContainerLifecycleListener.java new file mode 100644 index 0000000000..1bc7c1988a --- /dev/null +++ b/connectors/citrus-testcontainers/src/main/java/org/citrusframework/testcontainers/quarkus/ContainerLifecycleListener.java @@ -0,0 +1,49 @@ +/* + * Copyright the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citrusframework.testcontainers.quarkus; + +import java.util.Collections; +import java.util.Map; + +/** + * Listener gets invoked when Testcontainers instance is started or stopped. + * Allows implementations to perform actions with given container instance, + * in particular configuring the application under test with the container exposed connection settings. + * @param the container type + */ +public interface ContainerLifecycleListener { + + String INIT_ARG = "citrus.testcontainers.lifecycle.listener"; + + /** + * Invoked when Testcontainers instance has been started. Returned key-value + * map is used to set application properties on the system under test which is the Quarkus application + * started via QuarkusTest annotation. + * @param container + * @return + */ + default Map started(T container) { + return Collections.emptyMap(); + } + + /** + * Invoked after the Testcontainers instance has been stopped. + * @param container + */ + default void stopped(T container) { + } +} diff --git a/connectors/citrus-testcontainers/src/main/java/org/citrusframework/testcontainers/quarkus/GenericContainerProvider.java b/connectors/citrus-testcontainers/src/main/java/org/citrusframework/testcontainers/quarkus/GenericContainerProvider.java new file mode 100644 index 0000000000..1f94a9d230 --- /dev/null +++ b/connectors/citrus-testcontainers/src/main/java/org/citrusframework/testcontainers/quarkus/GenericContainerProvider.java @@ -0,0 +1,25 @@ +/* + * Copyright the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citrusframework.testcontainers.quarkus; + +import org.testcontainers.containers.GenericContainer; + +@FunctionalInterface +public interface GenericContainerProvider { + + GenericContainer create(); +} diff --git a/connectors/citrus-testcontainers/src/main/java/org/citrusframework/testcontainers/quarkus/GenericContainerResource.java b/connectors/citrus-testcontainers/src/main/java/org/citrusframework/testcontainers/quarkus/GenericContainerResource.java new file mode 100644 index 0000000000..b6f1ca73a0 --- /dev/null +++ b/connectors/citrus-testcontainers/src/main/java/org/citrusframework/testcontainers/quarkus/GenericContainerResource.java @@ -0,0 +1,51 @@ +/* + * Copyright the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citrusframework.testcontainers.quarkus; + +import java.lang.reflect.InvocationTargetException; + +import io.quarkus.test.common.QuarkusTestResourceConfigurableLifecycleManager; +import org.citrusframework.exceptions.CitrusRuntimeException; +import org.testcontainers.containers.GenericContainer; + +public class GenericContainerResource extends TestcontainersResource> + implements QuarkusTestResourceConfigurableLifecycleManager { + + public GenericContainerResource() { + super((Class) GenericContainer.class); + } + + @Override + public void init(TestcontainersSupport config) { + for (Class>> lifecycleListenerType : + config.containerLifecycleListener()) { + try { + registerContainerLifecycleListener((ContainerLifecycleListener>) lifecycleListenerType.getDeclaredConstructor().newInstance()); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + throw new CitrusRuntimeException("Failed to instantiate container lifecycle listener from type: %s" + .formatted(lifecycleListenerType), e); + } + } + + try { + container = config.containerProvider().getDeclaredConstructor().newInstance().create(); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + throw new CitrusRuntimeException("Failed to instantiate container provider from type: %s" + .formatted(config.containerProvider()), e); + } + } +} diff --git a/connectors/citrus-testcontainers/src/main/java/org/citrusframework/testcontainers/quarkus/TestcontainersResource.java b/connectors/citrus-testcontainers/src/main/java/org/citrusframework/testcontainers/quarkus/TestcontainersResource.java new file mode 100644 index 0000000000..8be4e8fe7d --- /dev/null +++ b/connectors/citrus-testcontainers/src/main/java/org/citrusframework/testcontainers/quarkus/TestcontainersResource.java @@ -0,0 +1,97 @@ +/* + * Copyright the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citrusframework.testcontainers.quarkus; + +import java.lang.reflect.InvocationTargetException; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import io.quarkus.test.common.QuarkusTestResourceLifecycleManager; +import org.citrusframework.annotations.CitrusResource; +import org.citrusframework.exceptions.CitrusRuntimeException; +import org.testcontainers.containers.GenericContainer; + +public class TestcontainersResource> implements QuarkusTestResourceLifecycleManager { + + private final Set> containerLifecycleListeners = new HashSet<>(); + + protected T container; + + private final Class containerType; + + public TestcontainersResource(Class containerType) { + this.containerType = containerType; + } + + @Override + public final void init(Map initArgs) { + String[] qualifiedClassNames = initArgs.getOrDefault(ContainerLifecycleListener.INIT_ARG, "").split(","); + for (String qualifiedClassName : qualifiedClassNames) { + try { + Class cls = Class.forName(qualifiedClassName, true, Thread.currentThread().getContextClassLoader()); + Object instance = cls.getDeclaredConstructor().newInstance(); + if (instance instanceof ContainerLifecycleListener containerLifecycleListener) { + registerContainerLifecycleListener((ContainerLifecycleListener) containerLifecycleListener); + } + } catch (ClassNotFoundException | InvocationTargetException | InstantiationException | + IllegalAccessException | NoSuchMethodException e) { + throw new CitrusRuntimeException("Failed to instantiate container lifecycle listener from type: %s".formatted(qualifiedClassName), e); + } + } + + doInit(initArgs); + } + + protected void doInit(Map initArgs) { + } + + @Override + public final Map start() { + Map conf = doStart(); + for (ContainerLifecycleListener listener : containerLifecycleListeners) { + conf.putAll(listener.started(container)); + } + return conf; + } + + protected Map doStart() { + container.start(); + return new HashMap<>(); + } + + @Override + public final void stop() { + doStop(); + containerLifecycleListeners.forEach(listener -> listener.stopped(container)); + } + + protected void doStop() { + container.stop(); + } + + @Override + public void inject(TestInjector testInjector) { + testInjector.injectIntoFields(container, new TestInjector.AnnotatedAndMatchesType(CitrusResource.class, containerType)); + } + + protected void registerContainerLifecycleListener(ContainerLifecycleListener containerLifecycleListener) { + this.containerLifecycleListeners.add(containerLifecycleListener); + } + +} diff --git a/connectors/citrus-testcontainers/src/main/java/org/citrusframework/testcontainers/quarkus/TestcontainersSupport.java b/connectors/citrus-testcontainers/src/main/java/org/citrusframework/testcontainers/quarkus/TestcontainersSupport.java new file mode 100644 index 0000000000..7a95a30007 --- /dev/null +++ b/connectors/citrus-testcontainers/src/main/java/org/citrusframework/testcontainers/quarkus/TestcontainersSupport.java @@ -0,0 +1,42 @@ +/* + * Copyright the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citrusframework.testcontainers.quarkus; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import io.quarkus.test.common.QuarkusTestResource; +import org.testcontainers.containers.GenericContainer; + +@QuarkusTestResource(GenericContainerResource.class) +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface TestcontainersSupport { + + /** + * Container provider capable of creating the container instance. + * @return + */ + Class containerProvider(); + + /** + * Container lifecycle listeners + */ + Class>>[] containerLifecycleListener() default {}; +} diff --git a/connectors/citrus-testcontainers/src/main/java/org/citrusframework/testcontainers/redpanda/quarkus/RedpandaContainerResource.java b/connectors/citrus-testcontainers/src/main/java/org/citrusframework/testcontainers/redpanda/quarkus/RedpandaContainerResource.java new file mode 100644 index 0000000000..3ebb6a38c8 --- /dev/null +++ b/connectors/citrus-testcontainers/src/main/java/org/citrusframework/testcontainers/redpanda/quarkus/RedpandaContainerResource.java @@ -0,0 +1,56 @@ +/* + * Copyright the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citrusframework.testcontainers.redpanda.quarkus; + +import java.lang.reflect.InvocationTargetException; +import java.util.Collections; +import java.util.Map; + +import io.quarkus.test.common.QuarkusTestResourceConfigurableLifecycleManager; +import org.citrusframework.exceptions.CitrusRuntimeException; +import org.citrusframework.testcontainers.quarkus.ContainerLifecycleListener; +import org.citrusframework.testcontainers.quarkus.TestcontainersResource; +import org.citrusframework.testcontainers.redpanda.StartRedpandaAction; +import org.testcontainers.redpanda.RedpandaContainer; + +public class RedpandaContainerResource extends TestcontainersResource + implements QuarkusTestResourceConfigurableLifecycleManager { + + public RedpandaContainerResource() { + super(RedpandaContainer.class); + } + + @Override + public void init(RedpandaContainerSupport config) { + for (Class> lifecycleListenerType : + config.containerLifecycleListener()) { + try { + registerContainerLifecycleListener(lifecycleListenerType.getDeclaredConstructor().newInstance()); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + throw new CitrusRuntimeException("Failed to instantiate container lifecycle listener from type: %s" + .formatted(lifecycleListenerType), e); + } + } + + doInit(Collections.emptyMap()); + } + + @Override + protected void doInit(Map initArgs) { + container = new StartRedpandaAction.Builder().build().getContainer(); + } +} diff --git a/connectors/citrus-testcontainers/src/main/java/org/citrusframework/testcontainers/redpanda/quarkus/RedpandaContainerSupport.java b/connectors/citrus-testcontainers/src/main/java/org/citrusframework/testcontainers/redpanda/quarkus/RedpandaContainerSupport.java new file mode 100644 index 0000000000..28cf4292e8 --- /dev/null +++ b/connectors/citrus-testcontainers/src/main/java/org/citrusframework/testcontainers/redpanda/quarkus/RedpandaContainerSupport.java @@ -0,0 +1,37 @@ +/* + * Copyright the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citrusframework.testcontainers.redpanda.quarkus; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import io.quarkus.test.common.QuarkusTestResource; +import org.citrusframework.testcontainers.quarkus.ContainerLifecycleListener; +import org.testcontainers.redpanda.RedpandaContainer; + +@QuarkusTestResource(RedpandaContainerResource.class) +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface RedpandaContainerSupport { + + /** + * Container lifecycle listeners + */ + Class>[] containerLifecycleListener() default {}; +} diff --git a/connectors/citrus-testcontainers/src/test/java/org/citrusframework/testcontainers/integration/StartGenericTestcontainersIT.java b/connectors/citrus-testcontainers/src/test/java/org/citrusframework/testcontainers/integration/StartGenericTestcontainersIT.java index ac4975ac2c..57321f51c7 100644 --- a/connectors/citrus-testcontainers/src/test/java/org/citrusframework/testcontainers/integration/StartGenericTestcontainersIT.java +++ b/connectors/citrus-testcontainers/src/test/java/org/citrusframework/testcontainers/integration/StartGenericTestcontainersIT.java @@ -41,7 +41,7 @@ public class StartGenericTestcontainersIT extends AbstractTestcontainersIT { @Test @CitrusTest public void shouldStartContainer() { - GenericContainer busyBox = new GenericContainer("busybox:latest") + GenericContainer busyBox = new GenericContainer<>("busybox:latest") .withCommand("echo", "Hello World"); given(doFinally().actions(testcontainers().stop() diff --git a/pom.xml b/pom.xml index e947c96a9c..990f6faa07 100644 --- a/pom.xml +++ b/pom.xml @@ -195,7 +195,7 @@ 1.8.0 3.26.3 4.2.2 - 2.29.21 + 2.27.19 1.79 1.15.10 2.12.0 @@ -244,13 +244,14 @@ 6.13.4 3.9.0 6.13.4 - 2.22.1 + 2.23.1 5.14.2 3.2.0 4.1.105.Final 4.12.0 4.7.6 42.7.4 + 3.16.4 3.0.4 4.27.0 2.0.11 @@ -624,6 +625,33 @@ test + + + io.quarkus + quarkus-arc + ${quarkus.platform.version} + + + io.quarkus + quarkus-arc-deployment + ${quarkus.platform.version} + + + io.quarkus + quarkus-junit5 + ${quarkus.platform.version} + + + io.quarkus + quarkus-test-common + ${quarkus.platform.version} + + + io.quarkus + quarkus-jacoco + ${quarkus.platform.version} + + org.apache.httpcomponents.client5 diff --git a/runtime/citrus-quarkus/citrus-quarkus-it/src/main/java/org/citrusframework/quarkus/app/DemoApplication.java b/runtime/citrus-quarkus/citrus-quarkus-it/src/main/java/org/citrusframework/quarkus/app/DemoApplication.java index 035e797e74..8828d23c78 100644 --- a/runtime/citrus-quarkus/citrus-quarkus-it/src/main/java/org/citrusframework/quarkus/app/DemoApplication.java +++ b/runtime/citrus-quarkus/citrus-quarkus-it/src/main/java/org/citrusframework/quarkus/app/DemoApplication.java @@ -19,14 +19,20 @@ import io.quarkus.runtime.StartupEvent; import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.event.Observes; -import org.jboss.logging.Logger; +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; @ApplicationScoped public class DemoApplication { - private static final Logger logger = Logger.getLogger(DemoApplication.class); + private static final Logger logger = LoggerFactory.getLogger(DemoApplication.class); + + @ConfigProperty(name = "greeting.message") + String message; void onStart(@Observes StartupEvent ev) { logger.info("Demo application started!"); + logger.info(message); } } diff --git a/runtime/citrus-quarkus/citrus-quarkus-it/src/main/resources/application.properties b/runtime/citrus-quarkus/citrus-quarkus-it/src/main/resources/application.properties index b084356b7d..46062f63f7 100644 --- a/runtime/citrus-quarkus/citrus-quarkus-it/src/main/resources/application.properties +++ b/runtime/citrus-quarkus/citrus-quarkus-it/src/main/resources/application.properties @@ -1 +1 @@ -quarkus.log.level=DEBUG +quarkus.log.level=INFO diff --git a/runtime/citrus-quarkus/citrus-quarkus-it/src/test/java/org/citrusframework/quarkus/app/DemoApplicationTest.java b/runtime/citrus-quarkus/citrus-quarkus-it/src/test/java/org/citrusframework/quarkus/app/DemoApplicationTest.java index 0131fc6075..7cd56a4558 100644 --- a/runtime/citrus-quarkus/citrus-quarkus-it/src/test/java/org/citrusframework/quarkus/app/DemoApplicationTest.java +++ b/runtime/citrus-quarkus/citrus-quarkus-it/src/test/java/org/citrusframework/quarkus/app/DemoApplicationTest.java @@ -16,6 +16,9 @@ package org.citrusframework.quarkus.app; +import java.util.Collections; +import java.util.Map; + import io.quarkus.test.junit.QuarkusTest; import org.citrusframework.Citrus; import org.citrusframework.TestCaseRunner; @@ -28,6 +31,7 @@ import org.citrusframework.endpoint.direct.annotation.DirectEndpointConfig; import org.citrusframework.message.DefaultMessageQueue; import org.citrusframework.message.MessageQueue; +import org.citrusframework.quarkus.ApplicationPropertiesSupplier; import org.citrusframework.quarkus.CitrusSupport; import org.citrusframework.spi.BindToRegistry; import org.citrusframework.validation.DefaultTextEqualsMessageValidator; @@ -41,8 +45,8 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; @QuarkusTest -@CitrusSupport -class DemoApplicationTest { +@CitrusSupport(applicationPropertiesSupplier = DemoApplicationTest.class) +public class DemoApplicationTest implements ApplicationPropertiesSupplier { @CitrusFramework private Citrus citrus; @@ -111,4 +115,9 @@ void shouldInjectCitrusResources() { .body("${greeting}") ); } + + @Override + public Map get() { + return Collections.singletonMap("greeting.message", "Hello, Citrus rocks!"); + } } diff --git a/runtime/citrus-quarkus/citrus-quarkus-it/src/test/resources/application.properties b/runtime/citrus-quarkus/citrus-quarkus-it/src/test/resources/application.properties new file mode 100644 index 0000000000..11278229d8 --- /dev/null +++ b/runtime/citrus-quarkus/citrus-quarkus-it/src/test/resources/application.properties @@ -0,0 +1 @@ +quarkus.arc.ignored-split-packages=org.citrusframework.* diff --git a/runtime/citrus-quarkus/citrus-quarkus-runtime/src/main/java/org/citrusframework/quarkus/ApplicationPropertiesSupplier.java b/runtime/citrus-quarkus/citrus-quarkus-runtime/src/main/java/org/citrusframework/quarkus/ApplicationPropertiesSupplier.java new file mode 100644 index 0000000000..2b6d5a9a53 --- /dev/null +++ b/runtime/citrus-quarkus/citrus-quarkus-runtime/src/main/java/org/citrusframework/quarkus/ApplicationPropertiesSupplier.java @@ -0,0 +1,31 @@ +/* + * Copyright the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.citrusframework.quarkus; + +import java.util.Map; +import java.util.function.Supplier; + +/** + * Supplier able to set application properties on the Quarkus application under test. + * Supplier gets called as part of the Quarkus test lifecycle manager before the Quarkus application is started. + * Returned application properties are set as system properties for the Quarkus application. + */ +@FunctionalInterface +public interface ApplicationPropertiesSupplier extends Supplier> { + String INIT_ARG = "citrus.quarkus.application.properties.supplier"; + +} diff --git a/runtime/citrus-quarkus/citrus-quarkus-runtime/src/main/java/org/citrusframework/quarkus/CitrusSupport.java b/runtime/citrus-quarkus/citrus-quarkus-runtime/src/main/java/org/citrusframework/quarkus/CitrusSupport.java index 6ac4212703..7e085bd240 100644 --- a/runtime/citrus-quarkus/citrus-quarkus-runtime/src/main/java/org/citrusframework/quarkus/CitrusSupport.java +++ b/runtime/citrus-quarkus/citrus-quarkus-runtime/src/main/java/org/citrusframework/quarkus/CitrusSupport.java @@ -25,11 +25,14 @@ /** * Special Citrus annotation that enables Citrus support on QuarkusTest framework. - * */ -@QuarkusTestResource(CitrusTestResource.class) +@QuarkusTestResource(value = CitrusTestResource.class, restrictToAnnotatedClass = true) @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface CitrusSupport { + /** + * Supplier capable of setting application properties for the Quarkus application under test. + */ + Class[] applicationPropertiesSupplier() default {}; } diff --git a/runtime/citrus-quarkus/citrus-quarkus-runtime/src/main/java/org/citrusframework/quarkus/CitrusTestResource.java b/runtime/citrus-quarkus/citrus-quarkus-runtime/src/main/java/org/citrusframework/quarkus/CitrusTestResource.java index 5bfafff289..7cdfb706d7 100644 --- a/runtime/citrus-quarkus/citrus-quarkus-runtime/src/main/java/org/citrusframework/quarkus/CitrusTestResource.java +++ b/runtime/citrus-quarkus/citrus-quarkus-runtime/src/main/java/org/citrusframework/quarkus/CitrusTestResource.java @@ -16,10 +16,13 @@ package org.citrusframework.quarkus; -import java.util.Collections; +import java.lang.reflect.InvocationTargetException; +import java.util.HashMap; +import java.util.HashSet; import java.util.Map; +import java.util.Set; -import io.quarkus.test.common.QuarkusTestResourceLifecycleManager; +import io.quarkus.test.common.QuarkusTestResourceConfigurableLifecycleManager; import org.citrusframework.Citrus; import org.citrusframework.CitrusInstanceManager; import org.citrusframework.GherkinTestActionRunner; @@ -30,13 +33,13 @@ import org.citrusframework.annotations.CitrusFramework; import org.citrusframework.annotations.CitrusResource; import org.citrusframework.context.TestContext; +import org.citrusframework.exceptions.CitrusRuntimeException; /** * Quarkus test resource that takes care of injecting Citrus resources * such as TestContext, TestCaseRunner, CitrusEndpoints and many more. - * */ -public class CitrusTestResource implements QuarkusTestResourceLifecycleManager { +public class CitrusTestResource implements QuarkusTestResourceConfigurableLifecycleManager { private Citrus citrus; @@ -44,6 +47,38 @@ public class CitrusTestResource implements QuarkusTestResourceLifecycleManager { private TestContext context; + private final Set applicationPropertiesSupplier = new HashSet<>(); + + @Override + public void init(CitrusSupport config) { + for (Class supplierType : config.applicationPropertiesSupplier()) { + try { + registerApplicationPropertiesSupplier(supplierType.getDeclaredConstructor().newInstance()); + } catch (InvocationTargetException | InstantiationException | IllegalAccessException | NoSuchMethodException e) { + throw new CitrusRuntimeException("Failed to instantiate application properties supplier from type: %s" + .formatted(supplierType), e); + } + } + } + + @Override + public void init(Map initArgs) { + String[] qualifiedClassNames = initArgs.getOrDefault(ApplicationPropertiesSupplier.INIT_ARG, "").split(","); + for (String qualifiedClassName : qualifiedClassNames) { + try { + Class cls = Class.forName(qualifiedClassName, true, Thread.currentThread().getContextClassLoader()); + Object instance = cls.getDeclaredConstructor().newInstance(); + if (instance instanceof ApplicationPropertiesSupplier supplier) { + applicationPropertiesSupplier.add(supplier); + } + } catch (ClassNotFoundException | InvocationTargetException | InstantiationException | + IllegalAccessException | NoSuchMethodException e) { + throw new CitrusRuntimeException("Failed to instantiate application property supplier from type: %s" + .formatted(qualifiedClassName), e); + } + } + } + @Override public Map start() { if (citrus == null) { @@ -51,7 +86,9 @@ public Map start() { citrus.beforeSuite("citrus-quarkus"); } - return Collections.emptyMap(); + Map applicationProperties = new HashMap<>(); + applicationPropertiesSupplier.forEach(supplier -> applicationProperties.putAll(supplier.get())); + return applicationProperties; } @Override @@ -64,6 +101,7 @@ public void stop() { runner = null; context = null; + applicationPropertiesSupplier.clear(); } @Override @@ -91,4 +129,13 @@ public void inject(TestInjector testInjector) { testInjector.injectIntoFields(runner, new TestInjector.AnnotatedAndMatchesType(CitrusResource.class, TestCaseRunner.class)); testInjector.injectIntoFields(context, new TestInjector.AnnotatedAndMatchesType(CitrusResource.class, TestContext.class)); } + + /** + * Add new application properties supplier. + * @param supplier + */ + public void registerApplicationPropertiesSupplier(ApplicationPropertiesSupplier supplier) { + applicationPropertiesSupplier.add(supplier); + } + } diff --git a/runtime/citrus-quarkus/pom.xml b/runtime/citrus-quarkus/pom.xml index d60e9347b2..5f8f20bc84 100644 --- a/runtime/citrus-quarkus/pom.xml +++ b/runtime/citrus-quarkus/pom.xml @@ -14,41 +14,6 @@ Citrus Quarkus Extension pom - - 3.16.0 - - - - - - io.quarkus - quarkus-arc - ${quarkus.platform.version} - - - io.quarkus - quarkus-arc-deployment - ${quarkus.platform.version} - - - io.quarkus - quarkus-junit5 - ${quarkus.platform.version} - - - io.quarkus - quarkus-test-common - ${quarkus.platform.version} - - - - io.quarkus - quarkus-jacoco - ${quarkus.platform.version} - - - - diff --git a/src/manual/runtimes-quarkus.adoc b/src/manual/runtimes-quarkus.adoc index 5dbabbe203..c202c41327 100644 --- a/src/manual/runtimes-quarkus.adoc +++ b/src/manual/runtimes-quarkus.adoc @@ -1,7 +1,10 @@ [[runtime-quarkus]] == QuarkusTest runtime -Quarkus has emerged into a popular enterprise Java framework. For unit and integration testing the Quarkus framework provides a special integrations with JUnit Jupiter. Citrus adds a Quarkus test resource that developers can use to include Citrus capabilities into arbitrary Quarkus tests. +Quarkus has emerged into a popular enterprise Java framework. +For unit and integration testing the Quarkus framework provides integrations with JUnit Jupiter. +Citrus adds a Quarkus test resource implementation that allows developers to combine Citrus with Quarkus during testing. +You can use the Citrus test resource annotations on your Quarkus tests and include Citrus capabilities into arbitrary Quarkus tests. NOTE: The Citrus QuarkusTest extension is shipped in a separate Maven module. You need to include the module as a dependency in your project accordingly. @@ -15,9 +18,11 @@ NOTE: The Citrus QuarkusTest extension is shipped in a separate Maven module. Yo ---- -Usually a Quarkus test is annotated with the `@QuarkusTest` or `QuarkusIntegrationTest` annotation. Users may add an annotation named `@CitrusSupport` in order to also enable Citrus capabilities on the test. +Usually a Quarkus test is annotated with the `@QuarkusTest` or `QuarkusIntegrationTest` annotation. +Users just add an annotation named `@CitrusSupport` to also enable Citrus capabilities on the test. -The Citrus support will automatically hook into the QuarkusTest lifecycle management making sure to call the Citrus before/after suite and before/after test handlers. +The Citrus support will automatically hook into the QuarkusTest lifecycle management to inject Citrus resources with `@CitrusResource` annotation. +Also, the Citrus extension makes sure to start a proper Citrus instance and call before/after suite and before/after test handlers. This way you are able to combine Citrus with `@QuarkusTest` annotated classes very easily. @@ -56,11 +61,15 @@ public class DemoApplicationTest { } ---- -The `@CitrusSupport` annotation enables the Citrus features on the test. First of all users may inject Citrus related resources such as `TestCaseRunner` or `TestContext`. +The `@CitrusSupport` annotation enables the Citrus features on the test. +First of all users may inject Citrus related resources such as `TestCaseRunner` or the `TestContext`. -The `TestCaseRunner` reference runs arbitrary Citrus actions as part of the test. +As usual the `TestCaseRunner` is the entrance to the Citrus domain specific language for running arbitrary Citrus actions as part of the test. -The test is also able to configure Message endpoints. +[[runtime-quarkus-endpoint-config]] +=== Endpoint configuration + +The test is able to configure Message endpoints to connect to different messaging transports as part of the test. .Configure message endpoints [source,java] @@ -96,6 +105,298 @@ public class DemoApplicationTest { } ---- -Creating new message endpoints is very easy. Just use the proper endpoint builder and optionally bind the new endpoint to the Citrus bean registry via `BindToRegistry` annotation. - +Creating new message endpoints is very easy. +Just use the proper endpoint builder and optionally bind the new endpoint to the Citrus bean registry via `@BindToRegistry` annotation. You may then use the message endpoint in all `send` and `receive` test actions in order to exchange messages. + +You may move the endpoint configuration into a separate class and load the endpoints with the configuration class as follows: + +.EndpointConfig.class +[source,java] +---- +public class EndpointConfig { + + @BindToRegistry + public KafkaEndpoint bookings() { + return new KafkaEndpointBuilder() + .topic("bookings") + .build(); + } +} +---- + +The endpoint configuration class uses `@BindToRegistry` members or methods to add beans to the Citrus registry. +The configuration class may be referenced by many tests then using the `@CitrusConfiguration` annotation. + +.Load endpoint config classes +[source,java] +---- +@QuarkusTest +@CitrusSupport +@CitrusConfiguration(classes = EndpointConfig.class) +public class DemoApplicationTest { + + @CitrusResource + private KafkaEndpoint bookings; + + @CitrusResource + private TestCaseRunner t; + + @Test + void shouldVerifyDemoApp() { + t.when( + send() + .endpoint(bookings) + .message() + .body("How about Citrus!?") + ); + + t.when( + receive() + .endpoint(bookings) + .message() + .body("Citrus rocks!") + ); + } +} +---- + +Citrus loads the configuration class and injects the `KafkaEndpoint` instance to the test with `@CitrusResource` annotation. + +[[runtime-quarkus-dynamic-tests]] +=== Load dynamic tests + +Citrus supports many test languages besides writing tests in pure Java. +Users can load tests written in XML, YAML, Groovy and many more via dynamic tests. + +.Load YAML tests +[source,java] +---- +@QuarkusTest +@CitrusSupport +@CitrusConfiguration(classes = EndpointConfig.class) +public class DemoApplicationTest { + + @CitrusTestFactory + public Stream loadYamlTests() { + return CitrusTestFactorySupport.factory(TestLoader.YAML).packageScan("some.package.name"); + } +} +---- + +The example above loads YAML test case definitions and runs those as dynamic tests with JUnit Jupiter. +The package scan loads all files in the given folder and runs the tests via Citrus. +All YAML tests are able to reference the message endpoints configured in the configuration class `EndpointConfig.class`. + +A sample YAML test may look like this: + +.my-test.yaml +[source,yaml] +---- +name: my-test +actions: + - send: + endpoint: bookings + message: + body: + data: How about Citrus!? + - receive: + endpoint: bookings + timeout: 5000 + message: + body: + data: Citrus rocks! +---- + +[[runtime-quarkus-application-properties]] +=== Set application properties + +The `@QuarkusTest` annotation will automatically start the application under test. +Citrus provides the ability to programmatically set application properties before the Quarkus application is started. +This is important when you need to overwrite configuration based on test message endpoints configured in the test. + +The next example shows a Citrus enabled Quarkus test that supplies a set of application properties to configure the application under test. + +.Supply application properties +[source,java] +---- +@QuarkusTest +@CitrusSupport(applicationPropertiesSupplier = DemoAppConfigurationSupplier.class) +@CitrusConfiguration(classes = EndpointConfig.class) +public class DemoApplicationTest { + + // ... +} +---- + +The `DemoAppConfiguration` class implements the `Supplier` interface and set a config property. +This property will be set on the application under test. + +.DemoAppConfigurationSupplier.class +[source,java] +---- +public class DemoAppConfigurationSupplier implements ApplicationPropertiesSupplier { + + @Override + public Map get() { + Map conf = new Hasmap<>(); + conf.put("quarkus.log.level", "INFO"); + conf.put("greeting.message", "Hello, Citrus rocks!"); + return conf; + } +} +---- + +The application properties supplier is able to set Quarkus properties as well as application domain properties. +The example above sets `greeting.message` property which can be referenced in the Quarkus application: + +.DemoApplication +[source,java] +---- +@ApplicationScoped +public class DemoApplication { + + private static final Logger logger = Logger.getLogger(DemoApplication.class); + + @ConfigProperty(name = "greeting.message") + String message; + + void onStart(@Observes StartupEvent ev) { + logger.info(message); + } +} +---- + +[[runtime-quarkus-testcontainers]] +=== Testcontainers support + +Citrus integrates with Testcontainers to easily start/stop Testcontainers instances as part of the test. +You can leverage the Citrus Testcontainers features within a Quarkus test very easily. +Citrus implements Quarkus test resources for each of the supported containers (AWS LocalStack, Kafka, Redpanda, ...). + +The following example starts an AWS LocalStack Testcontainers instance and uses the S3 service on that container to upload a file to the S3 bucket. +The Quarkus application under test should handle this S3 file then. + +.AwsS3SourceTest +[source,java] +---- +@QuarkusTest +@CitrusSupport +@LocalStackContainerSupport(services = LocalStackContainer.Service.S3, containerLifecycleListener = AwsS3SourceTest.class) +public class AwsS3SourceTest implements ContainerLifecycleListener { + + @CitrusResource + private TestCaseRunner tc; + + @CitrusResource + private LocalStackContainer localStackContainer; + + @Test + public void shouldHandleUploadedS3File() { + tc.given(this::uploadS3File); + + // verify that the Quarkus application has handled the S3 file + } + + private void uploadS3File(TestContext context) { + S3Client s3Client = createS3Client(localStackContainer); + + CreateMultipartUploadResponse initResponse = s3Client.createMultipartUpload(b -> b.bucket(s3BucketName).key(s3Key)); + String etag = s3Client.uploadPart(b -> b.bucket(s3BucketName) + .key(s3Key) + .uploadId(initResponse.uploadId()) + .partNumber(1), + RequestBody.fromString(s3Data)).eTag(); + s3Client.completeMultipartUpload(b -> b.bucket(s3BucketName) + .multipartUpload(CompletedMultipartUpload.builder() + .parts(Collections.singletonList(CompletedPart.builder() + .partNumber(1) + .eTag(etag).build())).build()) + .key(s3Key) + .uploadId(initResponse.uploadId())); + } + + @Override + public Map started(LocalStackContainer container) { + S3Client s3Client = createS3Client(container); + + s3Client.createBucket(b -> b.bucket(s3BucketName)); + + Map conf = new HashMap<>(); + conf.put("my.app.aws-s3-source.accessKey", container.getAccessKey()); + conf.put("my.app.aws-s3-source.secretKey", container.getSecretKey()); + conf.put("my.app.aws-s3-source.region", container.getRegion()); + conf.put("my.app.aws-s3-source.bucketNameOrArn", s3BucketName); + conf.put("my.app.aws-s3-source.uriEndpointOverride", container.getServiceEndpoint().toString()); + conf.put("my.app.aws-s3-source.overrideEndpoint", "true"); + conf.put("my.app.aws-s3-source.forcePathStyle", "true"); + + return conf; + } + + private static S3Client createS3Client(LocalStackContainer container) { + return S3Client.builder() + .endpointOverride(container.getServiceEndpoint()) + .credentialsProvider( + StaticCredentialsProvider.create( + AwsBasicCredentials.create(container.getAccessKey(), container.getSecretKey()) + ) + ) + .forcePathStyle(true) + .region(Region.of(container.getRegion())) + .build(); + } +} +---- + +A few things happened in this example and let's explain those features one after another. +First thing to notice is the `@LocalStackContainerSupport` annotation that makes Citrus run the AWS LocalStack Testcontainers instance. +Also, the annotation provides the enabled services on that container (`services = LocalStackContainer.Service.S3`). +This starts the Testcontainers instance as part of the Quarkus test. + +The test also implements the `ContainerLifecycleListener` interface. +This enables the test to handle the container instance after it has been started. +This is a good place to create an S3 client and the bucket for the test. + +.Create S3 client +[source,java] +---- +@Override +public Map started(LocalStackContainer container) { + S3Client s3Client = createS3Client(container); + + s3Client.createBucket(b -> b.bucket(s3BucketName)); + + Map conf = new HashMap<>(); + conf.put("my.app.aws-s3-source.accessKey", container.getAccessKey()); + conf.put("my.app.aws-s3-source.secretKey", container.getSecretKey()); + conf.put("my.app.aws-s3-source.region", container.getRegion()); + conf.put("my.app.aws-s3-source.bucketNameOrArn", s3BucketName); + conf.put("my.app.aws-s3-source.uriEndpointOverride", container.getServiceEndpoint().toString()); + conf.put("my.app.aws-s3-source.overrideEndpoint", "true"); + conf.put("my.app.aws-s3-source.forcePathStyle", "true"); + + return conf; +} +---- + +Also, the started listener may return some application properties that get set for the Quarkus application under test. +This is the opportunity to set the Testcontainers connection settings for the Quarkus application. + +Obviously the Quarkus application uses some property based configuration with the `my.app.*` properties. +The test is able to reference the Testcontainers exposed settings as values for these properties (e.g. `my.app.aws-s3-source.accessKey=container.getAccessKey()`). + +With this configuration in place the test is able to upload and S3 file to the test bucket on the Testcontainers instance with the `uploadS3File()` method. +This should trigger the Quarkus application under test to handle the new file accordingly. +We can add some verification and assertion steps to verify that the Quarkus application has handled the S3 file. + +This is how Citrus is able to start Testcontainers instances as part of a Quarkus test. +The application properties supplier as well as the container lifecycle listener interfaces allow us to connect the Quarkus application with the Testcontainers instance. +The test is able to use the services on the Testcontainers instance to trigger some test data that is consumed by the application under test. + +Please also have a look into the other provided Testcontainers annotations in Citrus: + +* @LocalStackContainerSupport +* @KakfaContainerSupport +* @RedpandaContainerSupport +* @TestcontainersSupport