Improve container test code

Replace `DockerImageNames` with a enum and relocate it from the
`testcontainers` to `container` package. The enum now also
becomes a common location that we can use to apply container
configuration such as timeouts.

Closes gh-41164

Co-authored-by: Phillip Webb <phil.webb@broadcom.com>
This commit is contained in:
Andy Wilkinson
2024-06-19 15:03:12 +01:00
committed by Phillip Webb
parent 36a504b2fb
commit 491f34d25c
143 changed files with 827 additions and 854 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2023 the original author or authors.
* Copyright 2012-2024 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.
@@ -14,30 +14,23 @@
* limitations under the License.
*/
package org.springframework.boot.testsupport.testcontainers;
package org.springframework.boot.testsupport.container;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.utility.DockerImageName;
/**
* A {@link GenericContainer} for ActiveMQ.
*
* @author Stephane Nicoll
*/
public class ActiveMQContainer extends GenericContainer<ActiveMQContainer> {
public final class ActiveMQContainer extends GenericContainer<ActiveMQContainer> {
private static final int DEFAULT_PORT = 61616;
public ActiveMQContainer() {
super(DockerImageNames.activeMq());
public ActiveMQContainer(DockerImageName dockerImageName) {
super(dockerImageName);
addExposedPorts(DEFAULT_PORT);
}
/**
* Return the broker URL to use.
* @return the broker url of the ActiveMQ instance
*/
public String getBrokerUrl() {
return String.format("tcp://" + getHost() + ":" + getMappedPort(DEFAULT_PORT));
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2023 the original author or authors.
* Copyright 2012-2024 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.
@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.boot.testsupport.testcontainers;
package org.springframework.boot.testsupport.container;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2023 the original author or authors.
* Copyright 2012-2024 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.
@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.boot.testsupport.testcontainers;
package org.springframework.boot.testsupport.container;
import org.junit.jupiter.api.extension.ConditionEvaluationResult;
import org.junit.jupiter.api.extension.ExecutionCondition;

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2023 the original author or authors.
* Copyright 2012-2024 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.
@@ -14,9 +14,10 @@
* limitations under the License.
*/
package org.springframework.boot.testsupport.testcontainers;
package org.springframework.boot.testsupport.container;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.utility.DockerImageName;
/**
* A {@link GenericContainer} for Redis.
@@ -26,8 +27,8 @@ import org.testcontainers.containers.GenericContainer;
*/
public class RedisContainer extends GenericContainer<RedisContainer> {
public RedisContainer() {
super(DockerImageNames.redis());
public RedisContainer(DockerImageName dockerImageName) {
super(dockerImageName);
addExposedPorts(6379);
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2023 the original author or authors.
* Copyright 2012-2024 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.
@@ -14,21 +14,22 @@
* limitations under the License.
*/
package org.springframework.boot.testsupport.testcontainers;
package org.springframework.boot.testsupport.container;
import java.time.Duration;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.utility.DockerImageName;
/**
* Custom {@link org.testcontainers.containers.CassandraContainer} tuned for stability in
* heavily contended environments such as CI.
* A {@link GenericContainer} for Docker Registry.
*
* @author Andy Wilkinson
* @author Phillip Webb
*/
public class CassandraContainer extends org.testcontainers.containers.CassandraContainer<CassandraContainer> {
public class RegistryContainer extends GenericContainer<RegistryContainer> {
public CassandraContainer() {
super(DockerImageNames.cassandra());
withStartupTimeout(Duration.ofMinutes(10));
public RegistryContainer(DockerImageName dockerImageName) {
super(dockerImageName);
addExposedPorts(5000);
addEnv("SERVER_NAME", "localhost");
}
}

View File

@@ -0,0 +1,272 @@
/*
* Copyright 2012-2024 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
*
* https://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.springframework.boot.testsupport.container;
import java.lang.reflect.Constructor;
import java.time.Duration;
import java.util.Arrays;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.Supplier;
import org.testcontainers.containers.CassandraContainer;
import org.testcontainers.containers.Container;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.containers.KafkaContainer;
import org.testcontainers.containers.MongoDBContainer;
import org.testcontainers.containers.Neo4jContainer;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.containers.PulsarContainer;
import org.testcontainers.containers.RabbitMQContainer;
import org.testcontainers.couchbase.CouchbaseContainer;
import org.testcontainers.elasticsearch.ElasticsearchContainer;
import org.testcontainers.redpanda.RedpandaContainer;
import org.testcontainers.utility.DockerImageName;
import org.springframework.util.Assert;
/**
* References to container images used for integration tests. This class also acts a a
* central location for tests to {@link #container(Class) create} a correctly configured
* {@link Container testcontainer}.
*
* @author Stephane Nicoll
* @author Eddú Meléndez
* @author Moritz Halbritter
* @author Chris Bono
* @author Phillip Webb
*/
public enum TestImage {
/**
* A container image suitable for testing ActiveMQ.
*/
ACTIVE_MQ("symptoma/activemq", "5.18.0", () -> ActiveMQContainer.class),
/**
* A container image suitable for testing Cassandra.
*/
CASSANDRA("cassandra", "3.11.10", () -> CassandraContainer.class,
(container) -> ((CassandraContainer<?>) container).withStartupTimeout(Duration.ofMinutes(10))),
/**
* A Docker image suitable for running.
*/
COUCHBASE("couchbase/server", "7.1.4", () -> CouchbaseContainer.class,
(container) -> ((CouchbaseContainer) container).withStartupAttempts(5)
.withStartupTimeout(Duration.ofMinutes(10))),
/**
* A Docker image suitable for Elasticsearch 7.
*/
ELASTICSEARCH("docker.elastic.co/elasticsearch/elasticsearch", "7.17.5", () -> ElasticsearchContainer.class,
(container) -> ((ElasticsearchContainer) container).withEnv("ES_JAVA_OPTS", "-Xms32m -Xmx512m")
.withStartupAttempts(5)
.withStartupTimeout(Duration.ofMinutes(10))),
/**
* A container image suitable for testing Elasticsearch 8.
*/
ELASTICSEARCH_8("elasticsearch", "8.6.1"),
/**
* A container image suitable for testing Kafka.
*/
KAFKA("confluentinc/cp-kafka", "7.4.0", () -> KafkaContainer.class),
/**
* A container image suitable for testing MariaDB.
*/
MARIADB("mariadb", "10.10"),
/**
* A Docker image suitable for MongoDB.
*/
MONGODB("mongo", "5.0.17", () -> MongoDBContainer.class,
(container) -> ((MongoDBContainer) container).withStartupAttempts(5)
.withStartupTimeout(Duration.ofMinutes(5))),
/**
* A container image suitable for testing MySQL.
*/
MYSQL("mysql", "8.0"),
/**
* A container image suitable for testing Neo4j.
*/
NEO4J("neo4j", "4.4.11", () -> Neo4jContainer.class,
(container) -> ((Neo4jContainer<?>) container).withStartupAttempts(5)
.withStartupTimeout(Duration.ofMinutes(10))),
/**
* A container image suitable for testing Oracle Free.
*/
ORACLE_FREE("gvenzl/oracle-free", "23.3-slim", () -> org.testcontainers.oracle.OracleContainer.class,
(container) -> ((org.testcontainers.oracle.OracleContainer) container)
.withStartupTimeout(Duration.ofMinutes(2))),
/**
* A container image suitable for testing Oracle XA.
*/
ORACLE_XE("gvenzl/oracle-xe", "18.4.0-slim", () -> org.testcontainers.containers.OracleContainer.class,
(container) -> ((org.testcontainers.containers.OracleContainer) container)
.withStartupTimeout(Duration.ofMinutes(2))),
/**
* A container image suitable for testing Opentelemetry.
*/
OPENTELEMETRY("otel/opentelemetry-collector-contrib", "0.75.0"),
/**
* A container image suitable for testing Postgres.
*/
POSTGRESQL("postgres", "14.0", () -> PostgreSQLContainer.class),
/**
* A container image suitable for testing Pulsar.
*/
PULSAR("apachepulsar/pulsar", "3.1.0", () -> PulsarContainer.class,
(container) -> ((PulsarContainer) container).withStartupAttempts(2)
.withStartupTimeout(Duration.ofMinutes(3))),
/**
* A container image suitable for testing RabbitMQ.
*/
RABBITMQ("rabbitmq", "3.11-alpine", () -> RabbitMQContainer.class,
(container) -> ((RabbitMQContainer) container).withStartupTimeout(Duration.ofMinutes(4))),
/**
* A container image suitable for testing Redis.
*/
REDIS("redis", "7.0.11", () -> RedisContainer.class,
(container) -> ((RedisContainer) container).withStartupAttempts(5)
.withStartupTimeout(Duration.ofMinutes(10))),
/**
* A container image suitable for testing Redpanda.
*/
REDPANDA("redpandadata/redpanda", "v23.1.2", () -> RedpandaContainer.class,
(container) -> ((RedpandaContainer) container).withStartupTimeout(Duration.ofMinutes(5))),
/**
* A container image suitable for testing a Docker registry.
*/
REGISTRY("registry", "2.7.1", () -> RegistryContainer.class,
(container) -> ((RegistryContainer) container).withStartupAttempts(5)
.withStartupTimeout(Duration.ofMinutes(3))),
/**
* A container image suitable for testing MS SQL Server.
*/
SQL_SERVER("mcr.microsoft.com/mssql/server"),
/**
* A container image suitable for testing Zipkin.
*/
ZIPKIN("openzipkin/zipkin", "2.24.1", () -> ZipkinContainer.class);
private final String name;
private final String tag;
private final Class<?> containerClass;
private final Consumer<?> containerSetup;
TestImage(String name) {
this(name, null);
}
TestImage(String name, String tag) {
this(name, tag, null, null);
}
TestImage(String name, String tag, Supplier<Class<?>> containerClass) {
this(name, tag, containerClass, null);
}
TestImage(String name, String tag, Supplier<Class<?>> containerClass, Consumer<?> containerSetup) {
this.name = name;
this.tag = tag;
this.containerClass = getIfPossible(containerClass);
this.containerSetup = containerSetup;
}
static Class<?> getIfPossible(Supplier<Class<?>> supplier) {
try {
return (supplier != null) ? supplier.get() : null;
}
catch (NoClassDefFoundError ex) {
return null;
}
}
private boolean matchesContainerClass(Class<?> containerClass) {
return this.containerClass != null && this.containerClass.isAssignableFrom(containerClass);
}
/**
* Create a {@link GenericContainer} for the given {@link TestImage}.
* @return a generic container for the test image
*/
public GenericContainer<?> genericContainer() {
return createContainer(GenericContainer.class);
}
@SuppressWarnings({ "rawtypes", "unchecked" })
private <C extends Container<?>> C createContainer(Class<C> containerClass) {
DockerImageName dockerImageName = DockerImageName.parse(toString());
try {
Constructor<C> constructor = containerClass.getDeclaredConstructor(DockerImageName.class);
constructor.setAccessible(true);
C container = constructor.newInstance(dockerImageName);
if (this.containerSetup != null) {
((Consumer) this.containerSetup).accept(container);
}
return container;
}
catch (Exception ex) {
throw new IllegalStateException("Unable to create container " + containerClass, ex);
}
}
@Override
public String toString() {
return (this.tag != null) ? this.name + ":" + this.tag : this.name;
}
/**
* Factory method to create and configure a {@link Container} using a deduced
* {@link TestImage}.
* @param <C> the container type
* @param containerClass the container type
* @return a container instance
*/
public static <C extends Container<?>> C container(Class<C> containerClass) {
return forContainerClass(containerClass).createContainer(containerClass);
}
private static TestImage forContainerClass(Class<?> containerClass) {
List<TestImage> images = Arrays.stream(values())
.filter((image) -> image.matchesContainerClass(containerClass))
.toList();
Assert.state(!images.isEmpty(), () -> "Unknown container class " + containerClass);
Assert.state(images.size() == 1, () -> "Multiple test images match container class " + containerClass);
return images.get(0);
}
}

View File

@@ -0,0 +1,34 @@
/*
* Copyright 2012-2024 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
*
* https://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.springframework.boot.testsupport.container;
import org.testcontainers.containers.GenericContainer;
import org.testcontainers.utility.DockerImageName;
/**
* A {@link GenericContainer} for Zipkin.
*
* @author Phillip Webb
*/
public class ZipkinContainer extends GenericContainer<ZipkinContainer> {
public ZipkinContainer(DockerImageName dockerImageName) {
super(dockerImageName);
withExposedPorts(9411);
}
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2012-2019 the original author or authors.
* Copyright 2012-2024 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.
@@ -17,4 +17,4 @@
/**
* Support for testing with Testcontainers.
*/
package org.springframework.boot.testsupport.testcontainers;
package org.springframework.boot.testsupport.container;

View File

@@ -1,242 +0,0 @@
/*
* Copyright 2012-2024 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
*
* https://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.springframework.boot.testsupport.testcontainers;
import org.testcontainers.utility.DockerImageName;
/**
* Create {@link DockerImageName} instances for services used in integration tests.
*
* @author Stephane Nicoll
* @author Eddú Meléndez
* @author Moritz Halbritter
* @author Chris Bono
*/
public final class DockerImageNames {
private static final String ACTIVE_MQ_VERSION = "5.18.0";
private static final String CASSANDRA_VERSION = "3.11.10";
private static final String COUCHBASE_VERSION = "7.1.4";
private static final String ELASTICSEARCH_VERSION = "7.17.5";
private static final String ELASTICSEARCH_8_VERSION = "8.6.1";
private static final String KAFKA_VERSION = "7.4.0";
private static final String MARIADB_VERSION = "10.10";
private static final String MONGO_VERSION = "5.0.17";
private static final String MYSQL_VERSION = "8.0";
private static final String NEO4J_VERSION = "4.4.11";
private static final String ORACLE_FREE_VERSION = "23.3-slim";
private static final String ORACLE_XE_VERSION = "18.4.0-slim";
private static final String OPENTELEMETRY_VERSION = "0.75.0";
private static final String PULSAR_VERSION = "3.1.0";
private static final String POSTGRESQL_VERSION = "14.0";
private static final String RABBIT_VERSION = "3.11-alpine";
private static final String REDIS_VERSION = "7.0.11";
private static final String REDPANDA_VERSION = "v23.1.2";
private static final String REGISTRY_VERSION = "2.7.1";
private static final String ZIPKIN_VERSION = "2.24.1";
private DockerImageNames() {
}
/**
* Return a {@link DockerImageName} suitable for running ActiveMQ.
* @return a docker image name for running activeMq
*/
public static DockerImageName activeMq() {
return DockerImageName.parse("symptoma/activemq").withTag(ACTIVE_MQ_VERSION);
}
/**
* Return a {@link DockerImageName} suitable for running Cassandra.
* @return a docker image name for running cassandra
*/
public static DockerImageName cassandra() {
return DockerImageName.parse("cassandra").withTag(CASSANDRA_VERSION);
}
/**
* Return a {@link DockerImageName} suitable for running Couchbase.
* @return a docker image name for running couchbase
*/
public static DockerImageName couchbase() {
return DockerImageName.parse("couchbase/server").withTag(COUCHBASE_VERSION);
}
/**
* Return a {@link DockerImageName} suitable for running Elasticsearch 7.
* @return a docker image name for running elasticsearch
*/
public static DockerImageName elasticsearch() {
return DockerImageName.parse("docker.elastic.co/elasticsearch/elasticsearch").withTag(ELASTICSEARCH_VERSION);
}
/**
* Return a {@link DockerImageName} suitable for running Elasticsearch 8.
* @return a docker image name for running elasticsearch
*/
public static DockerImageName elasticsearch8() {
return DockerImageName.parse("elasticsearch").withTag(ELASTICSEARCH_8_VERSION);
}
/**
* Return a {@link DockerImageName} suitable for running Kafka.
* @return a docker image name for running Kafka
*/
public static DockerImageName kafka() {
return DockerImageName.parse("confluentinc/cp-kafka").withTag(KAFKA_VERSION);
}
/**
* Return a {@link DockerImageName} suitable for running MariaDB.
* @return a docker image name for running MariaDB
*/
public static DockerImageName mariadb() {
return DockerImageName.parse("mariadb").withTag(MARIADB_VERSION);
}
/**
* Return a {@link DockerImageName} suitable for running Mongo.
* @return a docker image name for running mongo
*/
public static DockerImageName mongo() {
return DockerImageName.parse("mongo").withTag(MONGO_VERSION);
}
/**
* Return a {@link DockerImageName} suitable for running MySQL.
* @return a docker image name for running MySQL
*/
public static DockerImageName mysql() {
return DockerImageName.parse("mysql").withTag(MYSQL_VERSION);
}
/**
* Return a {@link DockerImageName} suitable for running Neo4j.
* @return a docker image name for running neo4j
*/
public static DockerImageName neo4j() {
return DockerImageName.parse("neo4j").withTag(NEO4J_VERSION);
}
/**
* Return a {@link DockerImageName} suitable for running the Oracle database.
* @return a docker image name for running the Oracle database
*/
public static DockerImageName oracleFree() {
return DockerImageName.parse("gvenzl/oracle-free").withTag(ORACLE_FREE_VERSION);
}
/**
* Return a {@link DockerImageName} suitable for running the Oracle database.
* @return a docker image name for running the Oracle database
*/
public static DockerImageName oracleXe() {
return DockerImageName.parse("gvenzl/oracle-xe").withTag(ORACLE_XE_VERSION);
}
/**
* Return a {@link DockerImageName} suitable for running OpenTelemetry.
* @return a docker image name for running OpenTelemetry
*/
public static DockerImageName opentelemetry() {
return DockerImageName.parse("otel/opentelemetry-collector-contrib").withTag(OPENTELEMETRY_VERSION);
}
/**
* Return a {@link DockerImageName} suitable for running Apache Pulsar.
* @return a docker image name for running pulsar
*/
public static DockerImageName pulsar() {
return DockerImageName.parse("apachepulsar/pulsar").withTag(PULSAR_VERSION);
}
/**
* Return a {@link DockerImageName} suitable for running PostgreSQL.
* @return a docker image name for running postgresql
*/
public static DockerImageName postgresql() {
return DockerImageName.parse("postgres").withTag(POSTGRESQL_VERSION);
}
/**
* Return a {@link DockerImageName} suitable for running RabbitMQ.
* @return a docker image name for running RabbitMQ
*/
public static DockerImageName rabbit() {
return DockerImageName.parse("rabbitmq").withTag(RABBIT_VERSION);
}
/**
* Return a {@link DockerImageName} suitable for running Redis.
* @return a docker image name for running redis
*/
public static DockerImageName redis() {
return DockerImageName.parse("redis").withTag(REDIS_VERSION);
}
/**
* Return a {@link DockerImageName} suitable for running Redpanda.
* @return a docker image name for running redpanda
*/
public static DockerImageName redpanda() {
return DockerImageName.parse("redpandadata/redpanda").withTag(REDPANDA_VERSION);
}
/**
* Return a {@link DockerImageName} suitable for running Microsoft SQLServer.
* @return a docker image name for running SQLServer
*/
public static DockerImageName sqlserver() {
return DockerImageName.parse("mcr.microsoft.com/mssql/server");
}
/**
* Return a {@link DockerImageName} suitable for running a Docker registry.
* @return a docker image name for running a registry
*/
public static DockerImageName registry() {
return DockerImageName.parse("registry").withTag(REGISTRY_VERSION);
}
/**
* Return a {@link DockerImageName} suitable for running Zipkin.
* @return a docker image name for running Zipkin
*/
public static DockerImageName zipkin() {
return DockerImageName.parse("openzipkin/zipkin").withTag(ZIPKIN_VERSION);
}
}