Skip to content

Commit

Permalink
Add Quarkus Testcontainers test resource
Browse files Browse the repository at this point in the history
- 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)
  • Loading branch information
christophd committed Dec 2, 2024
1 parent d7a99a5 commit 0be1939
Show file tree
Hide file tree
Showing 26 changed files with 1,050 additions and 81 deletions.
7 changes: 7 additions & 0 deletions connectors/citrus-testcontainers/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,13 @@
<artifactId>commons-dbcp2</artifactId>
</dependency>

<!-- Optional Quarkus Test integration -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-test-common</artifactId>
<scope>provided</scope>
</dependency>

<!-- AWS Localstack -->
<dependency>
<groupId>software.amazon.awssdk</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ protected void exposeConnectionSettings(C container, TestContext context) {
}
}

protected C getContainer() {
public C getContainer() {
return container;
}

Expand Down Expand Up @@ -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(":")) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public class LocalStackContainer extends GenericContainer<LocalStackContainer> {
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<Service> services = new HashSet<>();
private String secretKey = "secretkey";
Expand Down Expand Up @@ -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)));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,8 @@

public class StartLocalStackAction extends StartTestcontainersAction<LocalStackContainer> {

private final Set<LocalStackContainer.Service> services;

public StartLocalStackAction(Builder builder) {
super(builder);

this.services = builder.services;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -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<LocalStackContainer>
implements QuarkusTestResourceConfigurableLifecycleManager<LocalStackContainerSupport> {

public static final String SERVICES_INIT_ARG = "aws.localstack.services";

public LocalStackContainerResource() {
super(LocalStackContainer.class);
}

@Override
public void init(LocalStackContainerSupport config) {
for (Class<? extends ContainerLifecycleListener<LocalStackContainer>> 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<String, String> 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();
}
}
Original file line number Diff line number Diff line change
@@ -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<? extends ContainerLifecycleListener<LocalStackContainer>>[] containerLifecycleListener() default {};
}
Original file line number Diff line number Diff line change
@@ -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<KafkaContainer>
implements QuarkusTestResourceConfigurableLifecycleManager<KafkaContainerSupport> {

public KafkaContainerResource() {
super(KafkaContainer.class);
}

@Override
public void init(KafkaContainerSupport config) {
for (Class<? extends ContainerLifecycleListener<KafkaContainer>> 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<String, String> initArgs) {
container = new StartKafkaAction.Builder().build().getContainer();
}
}
Original file line number Diff line number Diff line change
@@ -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<? extends ContainerLifecycleListener<KafkaContainer>>[] containerLifecycleListener() default {};
}
Original file line number Diff line number Diff line change
@@ -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 <T> the container type
*/
public interface ContainerLifecycleListener<T> {

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<String, String> started(T container) {
return Collections.emptyMap();
}

/**
* Invoked after the Testcontainers instance has been stopped.
* @param container
*/
default void stopped(T container) {
}
}
Loading

0 comments on commit 0be1939

Please sign in to comment.