diff --git a/README.md b/README.md index 108485b..3b42502 100644 --- a/README.md +++ b/README.md @@ -21,3 +21,48 @@ Makes testing & asserts with Testcontainers even easier. - [Cassandra](cassandra) - [Redis](redis) - [MockServer](mockserver) + +## Usage + +Here is an example of [Kafka Extension](kafka) where KafkaContainer is started in `PER_RUN` mode with topic reset per method: + +```java + +@TestcontainersKafka(mode = ContainerMode.PER_RUN, + topics = @Topics(value = "my-topic-name", reset = Topics.Mode.PER_METHOD)) +class ExampleTests { + + @ContainerKafkaConnection + private KafkaConnection kafkaConnection; + + @Test + void test() { + var consumer = kafkaConnection.subscribe("my-topic-name"); + kafkaConnection.send("my-topic-name", Event.ofValue("event1"), Event.ofValue("event2")); + consumer.assertReceivedAtLeast(2, Duration.ofSeconds(5)); + } +} +``` + +Here is an example of [Postgres Extension](postgres) where PostgresContainer is started `PER_RUN` mode and migrations are applied per method: + +```java + +@TestcontainersPostgreSQL(mode = ContainerMode.PER_RUN, + migration = @Migration( + engine = Migration.Engines.FLYWAY, + apply = Migration.Mode.PER_METHOD, + drop = Migration.Mode.PER_METHOD)) +class ExampleTests { + + @ConnectionPostgreSQL + private JdbcConnection postgresConnection; + + @Test + void test() { + postgresConnection.execute("INSERT INTO users VALUES(1);"); + var usersFound = postgresConnection.queryMany("SELECT * FROM users;", r -> r.getInt(1)); + assertEquals(1, usersFound.size()); + } +} +``` \ No newline at end of file diff --git a/cassandra/README.md b/cassandra/README.md index 5547f12..8068968 100644 --- a/cassandra/README.md +++ b/cassandra/README.md @@ -18,7 +18,7 @@ Features: **Gradle** ```groovy -testImplementation "io.goodforgod:testcontainers-extensions-cassandra:0.9.6" +testImplementation "io.goodforgod:testcontainers-extensions-cassandra:0.10.0" ``` **Maven** @@ -26,7 +26,7 @@ testImplementation "io.goodforgod:testcontainers-extensions-cassandra:0.9.6" io.goodforgod testcontainers-extensions-cassandra - 0.9.6 + 0.10.0 test ``` @@ -53,10 +53,9 @@ testImplementation "com.datastax.oss:java-driver-core:4.17.0" ## Content - [Usage](#usage) - [Old Driver](#container-old-driver) -- [Container](#container) - - [Connection](#container-connection) - - [Migration](#container-migration) -- [Annotation](#container) +- [Connection](#connection) + - [Migration](#connection-migration) +- [Annotation](#annotation) - [Manual](#manual-container) - [Network](#network) - [Connection](#annotation-connection) @@ -78,7 +77,7 @@ Test with container start in `PER_RUN` mode and migration per method will look l class ExampleTests { @Test - void test(@ContainerCassandraConnection CassandraConnection connection) { + void test(@ConnectionCassandra CassandraConnection connection) { connection.execute("INSERT INTO cassandra.users(id) VALUES(1);"); var usersFound = connection.queryMany("SELECT * FROM cassandra.users;", r -> r.getInt(0)); assertEquals(1, usersFound.size()); @@ -93,63 +92,39 @@ class ExampleTests { Library excludes [com.datastax.cassandra:cassandra-driver-core](https://mvnrepository.com/artifact/com.datastax.cassandra/cassandra-driver-core/3.10.0) old driver from dependency leaking due to lots of vulnerabilities, if you require it add such dependency manually yourself. -## Container +## Connection -Library provides special `CassandraContainerExtra` with ability for migration and connection. -It can be used with [Testcontainers JUnit Extension](https://java.testcontainers.org/test_framework_integration/junit_5/). +`CassandraConnection` is an abstraction with asserting data in database container and easily manipulate container connection settings. +You can inject connection via `@ConnectionCassandra` as field or method argument or manually create it from container or manual settings. ```java class ExampleTests { + private static final CassandraContainer container = new CassandraContainer<>(); + @Test void test() { - try (var container = new CassandraContainerExtra<>(DockerImageName.parse("cassandra:4.1"))) { - container.start(); - } - } -} -``` - -### Container Connection - -`CassandraConnection` provides connection parameters, useful asserts, checks, etc. for easier testing. - -```java -class ExampleTests { - - @Test - void test() { - try (var container = new CassandraContainerExtra<>(DockerImageName.parse("cassandra:4.1"))) { container.start(); - container.connection().assertQueriesNone("SELECT * FROM cassandra.users;"); + CassandraConnection connection = CassandraConnection.forContainer(container); + connection.execute("INSERT INTO users VALUES(1);"); } - } } ``` -### Container Migration +### Connection Migration `Migrations` allow easily migrate database between test executions and drop after tests. - -Annotation parameters: -- `engine` - to use for migration. -- `apply` - parameter configures migration mode. -- `drop` - configures when to reset/drop/clear database. - -Available migration engines: -- Scripts - For `apply` load scripts from specified paths or directories and execute in ASC order, for `drop` clean all Non System tables in all cassandra +You can migrate container via `@TestcontainersCassandra#migration` annotation parameter or manually using `CassandraConnection`. ```java +@TestcontainersMariaDB class ExampleTests { @Test - void test() { - try (var container = new CassandraContainerExtra<>(DockerImageName.parse("cassandra:4.1"))) { - container.start(); - container.migrate(Migration.Engines.SCRIPTS, List.of("migration")); - container.connection().assertQueriesNone("SELECT * FROM cassandra.users;"); - container.drop(Migration.Engines.SCRIPTS, List.of("migration")); - } + void test(@ConnectionCassandra CassandraConnection connection) { + connection.migrationEngine(Migration.Engines.SCRIPTS).apply("migration/setup.cql"); + connection.execute("INSERT INTO users VALUES(1);"); + connection.migrationEngine(Migration.Engines.SCRIPTS).drop("migration/setup.cql"); } } ``` @@ -172,7 +147,7 @@ Simple example on how to start container per class, **no need to configure** con class ExampleTests { @Test - void test(@ContainerCassandraConnection CassandraConnection connection) { + void test(@ConnectionCassandra CassandraConnection connection) { assertNotNull(connection); } } @@ -212,12 +187,10 @@ class ExampleTests { @ContainerCassandra private static final CassandraContainer container = new CassandraContainer<>() - .withEnv("CASSANDRA_DC", "mydc") - .withLogConsumer(new Slf4jLogConsumer(LoggerFactory.getLogger(CassandraContainer.class))) - .withNetwork(Network.SHARED); + .withEnv("CASSANDRA_DC", "mydc"); @Test - void test(@ContainerCassandraConnection CassandraConnection connection) { + void test(@ConnectionCassandra CassandraConnection connection) { assertEquals("mydc", connection.params().datacenter()); } } @@ -262,7 +235,7 @@ Image syntax: ### Annotation Connection -`CassandraConnection` - can be injected to field or method parameter and used to communicate with running container via `@ContainerCassandraConnection` annotation. +`CassandraConnection` - can be injected to field or method parameter and used to communicate with running container via `@ConnectionCassandra` annotation. `CassandraConnection` provides connection parameters, useful asserts, checks, etc. for easier testing. Example: @@ -270,11 +243,11 @@ Example: @TestcontainersCassandra(mode = ContainerMode.PER_CLASS, image = "cassandra:4.1") class ExampleTests { - @ContainerCassandraConnection - private CassandraConnection connectionInField; + @ConnectionCassandra + private CassandraConnection connection; @Test - void test(@ContainerCassandraConnection CassandraConnection connection) { + void test() { connection.execute("INSERT INTO cassandra.users(id) VALUES(1);"); connection.execute("INSERT INTO cassandra.users(id) VALUES(2);"); var usersFound = connection.queryMany("SELECT * FROM cassandra.users;", r -> r.getInt(0)); @@ -291,6 +264,7 @@ Annotation parameters: - `engine` - to use for migration. - `apply` - parameter configures migration mode. - `drop` - configures when to reset/drop/clear database. +- `locations` - configures locations where migrations are placed. Available migration engines: - Scripts - For `apply` load scripts from specified paths or directories and execute in ASC order, for `drop` clean all Non System tables in all cassandra @@ -318,7 +292,7 @@ Test with container and migration per method will look like: class ExampleTests { @Test - void test(@ContainerCassandraConnection CassandraConnection connection) { + void test(@ConnectionCassandra CassandraConnection connection) { connection.execute("INSERT INTO cassandra.users(id) VALUES(1);"); connection.execute("INSERT INTO cassandra.users(id) VALUES(2);"); var usersFound = connection.queryMany("SELECT * FROM cassandra.users;", r -> r.getInt(0)); diff --git a/cassandra/src/main/java/io/goodforgod/testcontainers/extensions/cassandra/CassandraConnection.java b/cassandra/src/main/java/io/goodforgod/testcontainers/extensions/cassandra/CassandraConnection.java index 5f6bb6d..cf3678f 100644 --- a/cassandra/src/main/java/io/goodforgod/testcontainers/extensions/cassandra/CassandraConnection.java +++ b/cassandra/src/main/java/io/goodforgod/testcontainers/extensions/cassandra/CassandraConnection.java @@ -7,11 +7,12 @@ import java.util.Optional; import org.intellij.lang.annotations.Language; import org.jetbrains.annotations.NotNull; +import org.testcontainers.containers.CassandraContainer; /** - * Describes active Cassandra connection of currently running {@link CassandraContainerExtra} + * Describes active Cassandra connection of currently running {@link CassandraContainer} */ -public interface CassandraConnection { +public interface CassandraConnection extends AutoCloseable { @FunctionalInterface interface RowMapper { @@ -64,7 +65,10 @@ interface Params { * @return new Cassandra connection */ @NotNull - CqlSession get(); + CqlSession getConnection(); + + @NotNull + CassandraMigrationEngine migrationEngine(@NotNull Migration.Engines engine); /** * @param keyspaceName to create @@ -180,4 +184,30 @@ List queryMany(@NotNull @Language("CQL") String cql, * @return true if executed CQL results in exact number of expected rows */ boolean checkQueriesEquals(int expected, @NotNull @Language("CQL") String cql); + + @Override + void close(); + + static CassandraConnection forContainer(CassandraContainer container) { + if (!container.isRunning()) { + throw new IllegalStateException(container.getClass().getSimpleName() + " container is not running"); + } + + var params = new CassandraConnectionImpl.ParamsImpl(container.getHost(), + container.getMappedPort(CassandraContainer.CQL_PORT), + container.getLocalDatacenter(), container.getUsername(), container.getPassword()); + final Params network = new CassandraConnectionImpl.ParamsImpl(container.getNetworkAliases().get(0), + CassandraContainer.CQL_PORT, + container.getLocalDatacenter(), container.getUsername(), container.getPassword()); + return new CassandraConnectionClosableImpl(params, network); + } + + static CassandraConnection forParams(String host, + int port, + String datacenter, + String username, + String password) { + var params = new CassandraConnectionImpl.ParamsImpl(host, port, datacenter, username, password); + return new CassandraConnectionClosableImpl(params, null); + } } diff --git a/cassandra/src/main/java/io/goodforgod/testcontainers/extensions/cassandra/CassandraConnectionClosableImpl.java b/cassandra/src/main/java/io/goodforgod/testcontainers/extensions/cassandra/CassandraConnectionClosableImpl.java new file mode 100644 index 0000000..bc5f98b --- /dev/null +++ b/cassandra/src/main/java/io/goodforgod/testcontainers/extensions/cassandra/CassandraConnectionClosableImpl.java @@ -0,0 +1,16 @@ +package io.goodforgod.testcontainers.extensions.cassandra; + +import org.jetbrains.annotations.ApiStatus.Internal; + +@Internal +final class CassandraConnectionClosableImpl extends CassandraConnectionImpl { + + CassandraConnectionClosableImpl(Params params, Params network) { + super(params, network); + } + + @Override + public void close() { + super.stop(); + } +} diff --git a/cassandra/src/main/java/io/goodforgod/testcontainers/extensions/cassandra/CassandraConnectionImpl.java b/cassandra/src/main/java/io/goodforgod/testcontainers/extensions/cassandra/CassandraConnectionImpl.java index 3c22eab..55cde5a 100644 --- a/cassandra/src/main/java/io/goodforgod/testcontainers/extensions/cassandra/CassandraConnectionImpl.java +++ b/cassandra/src/main/java/io/goodforgod/testcontainers/extensions/cassandra/CassandraConnectionImpl.java @@ -21,9 +21,9 @@ import org.slf4j.LoggerFactory; @Internal -final class CassandraConnectionImpl implements CassandraConnection { +class CassandraConnectionImpl implements CassandraConnection { - private static final class ParamsImpl implements Params { + static final class ParamsImpl implements Params { private final String host; private final int port; @@ -84,6 +84,7 @@ public String toString() { private final Params params; private final Params network; + private volatile boolean isClosed = false; private volatile CqlSession connection; CassandraConnectionImpl(Params params, Params network) { @@ -114,7 +115,7 @@ static CassandraConnection forExternal(String host, String datacenter, String username, String password) { - var params = new ParamsImpl(host, port, datacenter, username, password); + var params = new CassandraConnectionImpl.ParamsImpl(host, port, datacenter, username, password); return new CassandraConnectionImpl(params, null); } @@ -129,12 +130,11 @@ static CassandraConnection forExternal(String host, } @NotNull - public CqlSession get() { - return connection(); - } + public CqlSession getConnection() { + if (isClosed) { + throw new IllegalStateException("JdbcConnection was closed"); + } - @NotNull - private CqlSession connection() { if (connection == null) { connection = openConnection(); } else if (connection.isClosed()) { @@ -167,6 +167,15 @@ private CqlSession openConnection() { return sessionBuilder.build(); } + @Override + public @NotNull CassandraMigrationEngine migrationEngine(Migration.@NotNull Engines engine) { + if (engine == Migration.Engines.SCRIPTS) { + return new ScriptCassandraMigrationEngine(this); + } + + throw new UnsupportedOperationException("Unsupported engine: " + engine); + } + @Override public void createKeyspace(@NotNull String keyspaceName) { execute("CREATE KEYSPACE IF NOT EXISTS " + keyspaceName @@ -177,8 +186,8 @@ public void createKeyspace(@NotNull String keyspaceName) { public void execute(@Language("CQL") @NotNull String cql) { logger.debug("Executing CQL:\n{}", cql); try { - var boundStatement = connection().prepare(cql).bind().setTimeout(Duration.ofMinutes(5)); - connection().execute(boundStatement).wasApplied(); + var boundStatement = getConnection().prepare(cql).bind().setTimeout(Duration.ofMinutes(5)); + getConnection().execute(boundStatement).wasApplied(); } catch (Exception e) { throw new CassandraConnectionException(e); } @@ -246,8 +255,8 @@ public Optional queryOne(@Language("CQL") @NotNull S throws E { logger.debug("Executing CQL:\n{}", cql); try { - var boundStatement = connection().prepare(cql).bind().setTimeout(Duration.ofMinutes(5)); - var row = connection().execute(boundStatement).one(); + var boundStatement = getConnection().prepare(cql).bind().setTimeout(Duration.ofMinutes(5)); + var row = getConnection().execute(boundStatement).one(); return (row != null) ? Optional.ofNullable(extractor.apply(row)) : Optional.empty(); @@ -262,8 +271,8 @@ public List queryMany(@Language("CQL") @NotNull Stri throws E { logger.debug("Executing CQL:\n{}", cql); try { - var boundStatement = connection().prepare(cql).bind().setTimeout(Duration.ofMinutes(5)); - var rows = connection().execute(boundStatement).all(); + var boundStatement = getConnection().prepare(cql).bind().setTimeout(Duration.ofMinutes(5)); + var rows = getConnection().execute(boundStatement).all(); final List result = new ArrayList<>(rows.size()); for (Row row : rows) { result.add(extractor.apply(row)); @@ -283,8 +292,8 @@ interface QueryAssert { private void assertQuery(@Language("CQL") String cql, QueryAssert consumer) { logger.debug("Executing CQL:\n{}", cql); try { - var boundStatement = connection().prepare(cql).bind().setTimeout(Duration.ofMinutes(5)); - var rows = connection().execute(boundStatement); + var boundStatement = getConnection().prepare(cql).bind().setTimeout(Duration.ofMinutes(5)); + var rows = getConnection().execute(boundStatement); consumer.accept(rows); } catch (Exception e) { throw new CassandraConnectionException(e); @@ -325,8 +334,8 @@ interface QueryChecker { private boolean checkQuery(@Language("CQL") String cql, QueryChecker checker) { logger.debug("Executing CQL:\n{}", cql); try { - var boundStatement = connection().prepare(cql).bind().setTimeout(Duration.ofMinutes(5)); - var rows = connection().execute(boundStatement); + var boundStatement = getConnection().prepare(cql).bind().setTimeout(Duration.ofMinutes(5)); + var rows = getConnection().execute(boundStatement); return checker.apply(rows); } catch (Exception e) { throw new CassandraConnectionException(e); @@ -354,6 +363,19 @@ public boolean checkQueriesEquals(int expected, @NotNull String cql) { }); } + void stop() { + this.isClosed = true; + if (connection != null) { + connection.close(); + connection = null; + } + } + + @Override + public void close() { + // do nothing + } + @Override public boolean equals(Object o) { if (this == o) @@ -373,11 +395,4 @@ public int hashCode() { public String toString() { return params().toString(); } - - void close() { - if (connection != null) { - connection.close(); - connection = null; - } - } } diff --git a/cassandra/src/main/java/io/goodforgod/testcontainers/extensions/cassandra/CassandraContainerExtra.java b/cassandra/src/main/java/io/goodforgod/testcontainers/extensions/cassandra/CassandraContext.java similarity index 60% rename from cassandra/src/main/java/io/goodforgod/testcontainers/extensions/cassandra/CassandraContainerExtra.java rename to cassandra/src/main/java/io/goodforgod/testcontainers/extensions/cassandra/CassandraContext.java index 16b6682..fbac0da 100644 --- a/cassandra/src/main/java/io/goodforgod/testcontainers/extensions/cassandra/CassandraContainerExtra.java +++ b/cassandra/src/main/java/io/goodforgod/testcontainers/extensions/cassandra/CassandraContext.java @@ -1,17 +1,13 @@ package io.goodforgod.testcontainers.extensions.cassandra; -import java.time.Duration; -import java.util.ArrayList; -import java.util.List; +import io.goodforgod.testcontainers.extensions.ContainerContext; import java.util.Optional; +import org.jetbrains.annotations.ApiStatus.Internal; import org.jetbrains.annotations.NotNull; -import org.slf4j.LoggerFactory; import org.testcontainers.containers.CassandraContainer; -import org.testcontainers.containers.output.Slf4jLogConsumer; -import org.testcontainers.containers.wait.strategy.Wait; -import org.testcontainers.utility.DockerImageName; -public class CassandraContainerExtra> extends CassandraContainer { +@Internal +final class CassandraContext implements ContainerContext { private static final String EXTERNAL_TEST_CASSANDRA_USERNAME = "EXTERNAL_TEST_CASSANDRA_USERNAME"; private static final String EXTERNAL_TEST_CASSANDRA_PASSWORD = "EXTERNAL_TEST_CASSANDRA_PASSWORD"; @@ -21,40 +17,29 @@ public class CassandraContainerExtra> private volatile CassandraConnectionImpl connection; - public CassandraContainerExtra(String dockerImageName) { - this(DockerImageName.parse(dockerImageName)); - } - - public CassandraContainerExtra(DockerImageName dockerImageName) { - super(dockerImageName); - - final String alias = "cassandra-" + System.currentTimeMillis(); - this.withLogConsumer(new Slf4jLogConsumer(LoggerFactory.getLogger(CassandraContainerExtra.class)) - .withMdc("image", dockerImageName.asCanonicalNameString()) - .withMdc("alias", alias)); - this.waitingFor(Wait.forListeningPort()); - this.withStartupTimeout(Duration.ofMinutes(5)); + private final CassandraContainer container; - this.setNetworkAliases(new ArrayList<>(List.of(alias))); + CassandraContext(CassandraContainer container) { + this.container = container; } @NotNull public CassandraConnection connection() { if (connection == null) { final Optional connectionExternal = getConnectionExternal(); - if (connectionExternal.isEmpty() && !isRunning()) { + if (connectionExternal.isEmpty() && !container.isRunning()) { throw new IllegalStateException("CassandraConnection can't be create for container that is not running"); } final CassandraConnection jdbcConnection = connectionExternal.orElseGet(() -> { - final String alias = getNetworkAliases().get(getNetworkAliases().size() - 1); - return CassandraConnectionImpl.forContainer(getHost(), - getMappedPort(CassandraContainer.CQL_PORT), + final String alias = container.getNetworkAliases().get(container.getNetworkAliases().size() - 1); + return CassandraConnectionImpl.forContainer(container.getHost(), + container.getMappedPort(CassandraContainer.CQL_PORT), alias, CassandraContainer.CQL_PORT, - getLocalDatacenter(), - getUsername(), - getPassword()); + container.getLocalDatacenter(), + container.getUsername(), + container.getPassword()); }); this.connection = (CassandraConnectionImpl) jdbcConnection; @@ -67,17 +52,17 @@ public CassandraConnection connection() { public void start() { final Optional connectionExternal = getConnectionExternal(); if (connectionExternal.isEmpty()) { - super.start(); + container.start(); } } @Override public void stop() { if (connection != null) { - connection.close(); + connection.stop(); connection = null; } - super.stop(); + container.stop(); } @NotNull @@ -94,4 +79,9 @@ private static Optional getConnectionExternal() { return Optional.empty(); } } + + @Override + public String toString() { + return container.getDockerImageName(); + } } diff --git a/cassandra/src/main/java/io/goodforgod/testcontainers/extensions/cassandra/CassandraMetadata.java b/cassandra/src/main/java/io/goodforgod/testcontainers/extensions/cassandra/CassandraMetadata.java index e5a8eef..03e2f0e 100644 --- a/cassandra/src/main/java/io/goodforgod/testcontainers/extensions/cassandra/CassandraMetadata.java +++ b/cassandra/src/main/java/io/goodforgod/testcontainers/extensions/cassandra/CassandraMetadata.java @@ -3,7 +3,6 @@ import io.goodforgod.testcontainers.extensions.AbstractContainerMetadata; import io.goodforgod.testcontainers.extensions.ContainerMode; import org.jetbrains.annotations.ApiStatus.Internal; -import org.jetbrains.annotations.NotNull; @Internal final class CassandraMetadata extends AbstractContainerMetadata { @@ -15,11 +14,6 @@ final class CassandraMetadata extends AbstractContainerMetadata { this.migration = migration; } - @Override - public @NotNull String networkAliasDefault() { - return "cassandra-" + System.currentTimeMillis(); - } - Migration migration() { return migration; } diff --git a/cassandra/src/main/java/io/goodforgod/testcontainers/extensions/cassandra/CassandraMigrationEngine.java b/cassandra/src/main/java/io/goodforgod/testcontainers/extensions/cassandra/CassandraMigrationEngine.java index a8f349a..157fa41 100644 --- a/cassandra/src/main/java/io/goodforgod/testcontainers/extensions/cassandra/CassandraMigrationEngine.java +++ b/cassandra/src/main/java/io/goodforgod/testcontainers/extensions/cassandra/CassandraMigrationEngine.java @@ -5,7 +5,15 @@ public interface CassandraMigrationEngine { - void migrate(@NotNull List locations); + default void apply(@NotNull String location) { + apply(List.of(location)); + } + + void apply(@NotNull List locations); + + default void drop(@NotNull String location) { + drop(List.of(location)); + } void drop(@NotNull List locations); } diff --git a/cassandra/src/main/java/io/goodforgod/testcontainers/extensions/cassandra/ContainerCassandraConnection.java b/cassandra/src/main/java/io/goodforgod/testcontainers/extensions/cassandra/ConnectionCassandra.java similarity index 87% rename from cassandra/src/main/java/io/goodforgod/testcontainers/extensions/cassandra/ContainerCassandraConnection.java rename to cassandra/src/main/java/io/goodforgod/testcontainers/extensions/cassandra/ConnectionCassandra.java index 2a7d72a..f61020c 100644 --- a/cassandra/src/main/java/io/goodforgod/testcontainers/extensions/cassandra/ContainerCassandraConnection.java +++ b/cassandra/src/main/java/io/goodforgod/testcontainers/extensions/cassandra/ConnectionCassandra.java @@ -10,4 +10,4 @@ @Documented @Target({ ElementType.FIELD, ElementType.PARAMETER }) @Retention(RetentionPolicy.RUNTIME) -public @interface ContainerCassandraConnection {} +public @interface ConnectionCassandra {} diff --git a/cassandra/src/main/java/io/goodforgod/testcontainers/extensions/cassandra/ContainerCassandra.java b/cassandra/src/main/java/io/goodforgod/testcontainers/extensions/cassandra/ContainerCassandra.java index b81b371..0770c1a 100644 --- a/cassandra/src/main/java/io/goodforgod/testcontainers/extensions/cassandra/ContainerCassandra.java +++ b/cassandra/src/main/java/io/goodforgod/testcontainers/extensions/cassandra/ContainerCassandra.java @@ -1,9 +1,10 @@ package io.goodforgod.testcontainers.extensions.cassandra; import java.lang.annotation.*; +import org.testcontainers.containers.CassandraContainer; /** - * Indicates that annotated field containers {@link CassandraContainerExtra} instance + * Indicates that annotated field containers {@link CassandraContainer} instance * that should be used by {@link TestcontainersCassandra} rather than creating default container */ @Documented diff --git a/cassandra/src/main/java/io/goodforgod/testcontainers/extensions/cassandra/Migration.java b/cassandra/src/main/java/io/goodforgod/testcontainers/extensions/cassandra/Migration.java index ace639c..f9c842e 100644 --- a/cassandra/src/main/java/io/goodforgod/testcontainers/extensions/cassandra/Migration.java +++ b/cassandra/src/main/java/io/goodforgod/testcontainers/extensions/cassandra/Migration.java @@ -31,7 +31,7 @@ /** * @return path for resource directory with scripts or scripts itself */ - String[] migrations(); + String[] locations(); /** * Database migration engine implementation diff --git a/cassandra/src/main/java/io/goodforgod/testcontainers/extensions/cassandra/ScriptCassandraMigrationEngine.java b/cassandra/src/main/java/io/goodforgod/testcontainers/extensions/cassandra/ScriptCassandraMigrationEngine.java index 27b1e69..f8032f4 100644 --- a/cassandra/src/main/java/io/goodforgod/testcontainers/extensions/cassandra/ScriptCassandraMigrationEngine.java +++ b/cassandra/src/main/java/io/goodforgod/testcontainers/extensions/cassandra/ScriptCassandraMigrationEngine.java @@ -55,7 +55,7 @@ private static List getFilesFromLocations(List locations) { } @Override - public void migrate(@NotNull List locations) { + public void apply(@NotNull List locations) { if (locations.isEmpty()) { logger.warn("Empty locations for schema migration for engine '{}' for connection: {}", getClass().getSimpleName(), connection); diff --git a/cassandra/src/main/java/io/goodforgod/testcontainers/extensions/cassandra/TestcontainersCassandra.java b/cassandra/src/main/java/io/goodforgod/testcontainers/extensions/cassandra/TestcontainersCassandra.java index e9b9045..46e9a3f 100644 --- a/cassandra/src/main/java/io/goodforgod/testcontainers/extensions/cassandra/TestcontainersCassandra.java +++ b/cassandra/src/main/java/io/goodforgod/testcontainers/extensions/cassandra/TestcontainersCassandra.java @@ -5,9 +5,10 @@ import java.lang.annotation.*; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.extension.ExtendWith; +import org.testcontainers.containers.CassandraContainer; /** - * Extension that is running {@link CassandraContainerExtra} for tests in different modes with + * Extension that is running {@link CassandraContainer} for tests in different modes with * database * schema migration support between test executions */ @@ -26,7 +27,6 @@ * 3) Image environment variable can have default value if empty using syntax: * "${MY_IMAGE_ENV|cassandra:4.1}" *

- * @see TestcontainersCassandraExtension#getContainerDefault(CassandraMetadata) */ String image() default "cassandra:4.1"; @@ -43,5 +43,5 @@ Migration migration() default @Migration(engine = Migration.Engines.SCRIPTS, apply = Migration.Mode.NONE, drop = Migration.Mode.NONE, - migrations = {}); + locations = {}); } diff --git a/cassandra/src/main/java/io/goodforgod/testcontainers/extensions/cassandra/TestcontainersCassandraExtension.java b/cassandra/src/main/java/io/goodforgod/testcontainers/extensions/cassandra/TestcontainersCassandraExtension.java index de5accc..23ef681 100644 --- a/cassandra/src/main/java/io/goodforgod/testcontainers/extensions/cassandra/TestcontainersCassandraExtension.java +++ b/cassandra/src/main/java/io/goodforgod/testcontainers/extensions/cassandra/TestcontainersCassandraExtension.java @@ -1,35 +1,66 @@ package io.goodforgod.testcontainers.extensions.cassandra; import io.goodforgod.testcontainers.extensions.AbstractTestcontainersExtension; +import io.goodforgod.testcontainers.extensions.ContainerContext; import io.goodforgod.testcontainers.extensions.ContainerMode; import java.lang.annotation.Annotation; +import java.time.Duration; import java.util.*; import org.jetbrains.annotations.ApiStatus.Internal; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.extension.ExtensionConfigurationException; import org.junit.jupiter.api.extension.ExtensionContext; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.CassandraContainer; import org.testcontainers.containers.Network; +import org.testcontainers.containers.output.Slf4jLogConsumer; +import org.testcontainers.containers.wait.strategy.Wait; import org.testcontainers.utility.DockerImageName; @Internal class TestcontainersCassandraExtension extends - AbstractTestcontainersExtension, CassandraMetadata> { + AbstractTestcontainersExtension, CassandraMetadata> { private static final ExtensionContext.Namespace NAMESPACE = ExtensionContext.Namespace .create(TestcontainersCassandraExtension.class); + @SuppressWarnings("unchecked") + protected Class> getContainerType() { + return (Class>) ((Class) CassandraContainer.class); + } + + protected Class getContainerAnnotation() { + return ContainerCassandra.class; + } + + protected Class getConnectionAnnotation() { + return ConnectionCassandra.class; + } + + @Override + protected ExtensionContext.Namespace getNamespace() { + return NAMESPACE; + } + @Override protected Class getConnectionType() { return CassandraConnection.class; } @Override - protected CassandraContainerExtra getContainerDefault(CassandraMetadata metadata) { - var dockerImage = DockerImageName.parse(metadata.image()) + protected CassandraContainer createContainerDefault(CassandraMetadata metadata) { + var image = DockerImageName.parse(metadata.image()) .asCompatibleSubstituteFor(DockerImageName.parse("cassandra")); - var container = new CassandraContainerExtra<>(dockerImage); - container.setNetworkAliases(new ArrayList<>(List.of(metadata.networkAliasOrDefault()))); + final CassandraContainer container = new CassandraContainer<>(image); + final String alias = Optional.ofNullable(metadata.networkAlias()) + .orElseGet(() -> "cassandra-" + System.currentTimeMillis()); + container.withLogConsumer(new Slf4jLogConsumer(LoggerFactory.getLogger(CassandraContainer.class)) + .withMdc("image", image.asCanonicalNameString()) + .withMdc("alias", alias)); + container.waitingFor(Wait.forListeningPort()); + container.withStartupTimeout(Duration.ofMinutes(5)); + container.setNetworkAliases(new ArrayList<>(List.of(alias))); if (metadata.networkShared()) { container.withNetwork(Network.SHARED); } @@ -38,21 +69,8 @@ protected CassandraContainerExtra getContainerDefault(CassandraMetadata metad } @Override - protected ExtensionContext.Namespace getNamespace() { - return NAMESPACE; - } - - @SuppressWarnings("unchecked") - protected Class> getContainerType() { - return (Class>) ((Class) CassandraContainerExtra.class); - } - - protected Class getContainerAnnotation() { - return ContainerCassandra.class; - } - - protected Class getConnectionAnnotation() { - return ContainerCassandraConnection.class; + protected ContainerContext createContainerContext(CassandraContainer container) { + return new CassandraContext(container); } @NotNull @@ -61,20 +79,15 @@ protected Optional findMetadata(@NotNull ExtensionContext con .map(a -> new CassandraMetadata(a.network().shared(), a.network().alias(), a.image(), a.mode(), a.migration())); } - @NotNull - protected CassandraConnection getConnectionForContainer(CassandraMetadata metadata, CassandraContainerExtra container) { - return container.connection(); - } - private void tryMigrateIfRequired(CassandraMetadata annotation, CassandraConnection connection) { if (annotation.migration().engine() == Migration.Engines.SCRIPTS) { - new ScriptCassandraMigrationEngine(connection).migrate(Arrays.asList(annotation.migration().migrations())); + new ScriptCassandraMigrationEngine(connection).apply(Arrays.asList(annotation.migration().locations())); } } private void tryDropIfRequired(CassandraMetadata annotation, CassandraConnection connection) { if (annotation.migration().engine() == Migration.Engines.SCRIPTS) { - new ScriptCassandraMigrationEngine(connection).drop(Arrays.asList(annotation.migration().migrations())); + new ScriptCassandraMigrationEngine(connection).drop(Arrays.asList(annotation.migration().locations())); } } @@ -85,7 +98,7 @@ public void beforeAll(ExtensionContext context) { var metadata = getMetadata(context); if (metadata.migration().apply() == Migration.Mode.PER_CLASS) { var storage = getStorage(context); - var connectionCurrent = getConnectionCurrent(context); + var connectionCurrent = getContainerContext(context).connection(); tryMigrateIfRequired(metadata, connectionCurrent); storage.put(Migration.class, metadata.migration().apply()); } @@ -103,7 +116,7 @@ public void beforeEach(ExtensionContext context) { super.beforeEach(context); if (metadata.migration().apply() == Migration.Mode.PER_METHOD) { - var connectionCurrent = getConnectionCurrent(context); + var connectionCurrent = getContainerContext(context).connection(); tryMigrateIfRequired(metadata, connectionCurrent); } } @@ -115,7 +128,7 @@ public void afterEach(ExtensionContext context) { storage.remove(Migration.class); if (metadata.migration().drop() == Migration.Mode.PER_METHOD) { if (metadata.runMode() != ContainerMode.PER_METHOD) { - var connectionCurrent = getConnectionCurrent(context); + var connectionCurrent = getContainerContext(context).connection(); tryDropIfRequired(metadata, connectionCurrent); } } @@ -126,7 +139,7 @@ public void afterEach(ExtensionContext context) { @Override public void afterAll(ExtensionContext context) { var metadata = getMetadata(context); - var connectionCurrent = getConnectionCurrent(context); + var connectionCurrent = getContainerContext(context).connection(); if (metadata.migration().drop() == Migration.Mode.PER_CLASS) { if (metadata.runMode() == ContainerMode.PER_RUN) { tryDropIfRequired(metadata, connectionCurrent); diff --git a/cassandra/src/test/java/io/goodforgod/testcontainers/extensions/cassandra/CassandraConnectionAssertsTests.java b/cassandra/src/test/java/io/goodforgod/testcontainers/extensions/cassandra/CassandraConnectionAssertsTests.java index bf1f24a..0c21d6a 100644 --- a/cassandra/src/test/java/io/goodforgod/testcontainers/extensions/cassandra/CassandraConnectionAssertsTests.java +++ b/cassandra/src/test/java/io/goodforgod/testcontainers/extensions/cassandra/CassandraConnectionAssertsTests.java @@ -11,29 +11,29 @@ engine = Migration.Engines.SCRIPTS, apply = Migration.Mode.PER_METHOD, drop = Migration.Mode.PER_METHOD, - migrations = { "migration/setup.cql" })) + locations = { "migration/setup.cql" })) class CassandraConnectionAssertsTests { @Test - void execute(@ContainerCassandraConnection CassandraConnection connection) { + void execute(@ConnectionCassandra CassandraConnection connection) { assertThrows(CassandraConnectionException.class, () -> connection.execute("CREATE TABLE cassandra.users(id INT, PRIMARY KEY (id))")); } @Test - void executeFromResources(@ContainerCassandraConnection CassandraConnection connection) { + void executeFromResources(@ConnectionCassandra CassandraConnection connection) { connection.executeFromResources("migration/setup.cql"); } @Test - void queryOne(@ContainerCassandraConnection CassandraConnection connection) { + void queryOne(@ConnectionCassandra CassandraConnection connection) { connection.execute("INSERT INTO cassandra.users(id) VALUES(1);"); var usersFound = connection.queryOne("SELECT * FROM cassandra.users;", r -> r.getInt(0)).orElse(null); assertEquals(1, usersFound); } @Test - void queryMany(@ContainerCassandraConnection CassandraConnection connection) { + void queryMany(@ConnectionCassandra CassandraConnection connection) { connection.execute("INSERT INTO cassandra.users(id) VALUES(1);"); connection.execute("INSERT INTO cassandra.users(id) VALUES(2);"); var usersFound = connection.queryMany("SELECT * FROM cassandra.users;", r -> r.getInt(0)); @@ -41,148 +41,148 @@ void queryMany(@ContainerCassandraConnection CassandraConnection connection) { } @Test - void count(@ContainerCassandraConnection CassandraConnection connection) { + void count(@ConnectionCassandra CassandraConnection connection) { connection.execute("INSERT INTO cassandra.users(id) VALUES(1);"); assertEquals(1, connection.count("cassandra.users")); } @Test - void assertCountsNoneWhenMore(@ContainerCassandraConnection CassandraConnection connection) { + void assertCountsNoneWhenMore(@ConnectionCassandra CassandraConnection connection) { connection.execute("INSERT INTO cassandra.users(id) VALUES(1);"); assertThrows(AssertionFailedError.class, () -> connection.assertCountsNone("cassandra.users")); } @Test - void assertCountsNoneWhenZero(@ContainerCassandraConnection CassandraConnection connection) { + void assertCountsNoneWhenZero(@ConnectionCassandra CassandraConnection connection) { assertDoesNotThrow(() -> connection.assertCountsNone("cassandra.users")); } @Test - void assertCountsAtLeastWhenZero(@ContainerCassandraConnection CassandraConnection connection) { + void assertCountsAtLeastWhenZero(@ConnectionCassandra CassandraConnection connection) { assertThrows(AssertionFailedError.class, () -> connection.assertCountsAtLeast(1, "cassandra.users")); } @Test - void assertCountsAtLeastWhenMore(@ContainerCassandraConnection CassandraConnection connection) { + void assertCountsAtLeastWhenMore(@ConnectionCassandra CassandraConnection connection) { connection.execute("INSERT INTO cassandra.users(id) VALUES(1);"); connection.execute("INSERT INTO cassandra.users(id) VALUES(2);"); assertDoesNotThrow(() -> connection.assertCountsAtLeast(1, "cassandra.users")); } @Test - void assertCountsAtLeastWhenEquals(@ContainerCassandraConnection CassandraConnection connection) { + void assertCountsAtLeastWhenEquals(@ConnectionCassandra CassandraConnection connection) { connection.execute("INSERT INTO cassandra.users(id) VALUES(1);"); assertDoesNotThrow(() -> connection.assertCountsAtLeast(1, "cassandra.users")); } @Test - void assertCountsExactWhenZero(@ContainerCassandraConnection CassandraConnection connection) { + void assertCountsExactWhenZero(@ConnectionCassandra CassandraConnection connection) { assertThrows(AssertionFailedError.class, () -> connection.assertCountsEquals(1, "cassandra.users")); } @Test - void assertCountsExactWhenMore(@ContainerCassandraConnection CassandraConnection connection) { + void assertCountsExactWhenMore(@ConnectionCassandra CassandraConnection connection) { connection.execute("INSERT INTO cassandra.users(id) VALUES(1);"); connection.execute("INSERT INTO cassandra.users(id) VALUES(2);"); assertThrows(AssertionFailedError.class, () -> connection.assertCountsEquals(1, "cassandra.users")); } @Test - void assertCountsExactWhenEquals(@ContainerCassandraConnection CassandraConnection connection) { + void assertCountsExactWhenEquals(@ConnectionCassandra CassandraConnection connection) { connection.execute("INSERT INTO cassandra.users(id) VALUES(1);"); assertDoesNotThrow(() -> connection.assertCountsEquals(1, "cassandra.users")); } @Test - void assertQueriesNoneWhenMore(@ContainerCassandraConnection CassandraConnection connection) { + void assertQueriesNoneWhenMore(@ConnectionCassandra CassandraConnection connection) { connection.execute("INSERT INTO cassandra.users(id) VALUES(1);"); assertThrows(AssertionFailedError.class, () -> connection.assertQueriesNone("SELECT * FROM cassandra.users;")); } @Test - void assertQueriesNoneWhenZero(@ContainerCassandraConnection CassandraConnection connection) { + void assertQueriesNoneWhenZero(@ConnectionCassandra CassandraConnection connection) { assertDoesNotThrow(() -> connection.assertQueriesNone("SELECT * FROM cassandra.users;")); } @Test - void assertQueriesAtLeastWhenZero(@ContainerCassandraConnection CassandraConnection connection) { + void assertQueriesAtLeastWhenZero(@ConnectionCassandra CassandraConnection connection) { assertThrows(AssertionFailedError.class, () -> connection.assertQueriesAtLeast(1, "SELECT * FROM cassandra.users;")); } @Test - void assertQueriesAtLeastWhenMore(@ContainerCassandraConnection CassandraConnection connection) { + void assertQueriesAtLeastWhenMore(@ConnectionCassandra CassandraConnection connection) { connection.execute("INSERT INTO cassandra.users(id) VALUES(1);"); connection.execute("INSERT INTO cassandra.users(id) VALUES(2);"); assertDoesNotThrow(() -> connection.assertQueriesAtLeast(1, "SELECT * FROM cassandra.users;")); } @Test - void assertQueriesAtLeastWhenEquals(@ContainerCassandraConnection CassandraConnection connection) { + void assertQueriesAtLeastWhenEquals(@ConnectionCassandra CassandraConnection connection) { connection.execute("INSERT INTO cassandra.users(id) VALUES(1);"); assertDoesNotThrow(() -> connection.assertQueriesAtLeast(1, "SELECT * FROM cassandra.users;")); } @Test - void assertQueriesExactWhenZero(@ContainerCassandraConnection CassandraConnection connection) { + void assertQueriesExactWhenZero(@ConnectionCassandra CassandraConnection connection) { assertThrows(AssertionFailedError.class, () -> connection.assertQueriesEquals(1, "SELECT * FROM cassandra.users;")); } @Test - void assertQueriesExactWhenMore(@ContainerCassandraConnection CassandraConnection connection) { + void assertQueriesExactWhenMore(@ConnectionCassandra CassandraConnection connection) { connection.execute("INSERT INTO cassandra.users(id) VALUES(1);"); connection.execute("INSERT INTO cassandra.users(id) VALUES(2);"); assertThrows(AssertionFailedError.class, () -> connection.assertQueriesEquals(1, "SELECT * FROM cassandra.users;")); } @Test - void assertQueriesExactWhenEquals(@ContainerCassandraConnection CassandraConnection connection) { + void assertQueriesExactWhenEquals(@ConnectionCassandra CassandraConnection connection) { connection.execute("INSERT INTO cassandra.users(id) VALUES(1);"); assertDoesNotThrow(() -> connection.assertQueriesEquals(1, "SELECT * FROM cassandra.users;")); } @Test - void checkQueriesNoneWhenMore(@ContainerCassandraConnection CassandraConnection connection) { + void checkQueriesNoneWhenMore(@ConnectionCassandra CassandraConnection connection) { connection.execute("INSERT INTO cassandra.users(id) VALUES(1);"); assertFalse(connection.checkQueriesNone("SELECT * FROM cassandra.users;")); } @Test - void checkQueriesNoneWhenZero(@ContainerCassandraConnection CassandraConnection connection) { + void checkQueriesNoneWhenZero(@ConnectionCassandra CassandraConnection connection) { assertTrue(connection.checkQueriesNone("SELECT * FROM cassandra.users;")); } @Test - void checkQueriesAtLeastWhenZero(@ContainerCassandraConnection CassandraConnection connection) { + void checkQueriesAtLeastWhenZero(@ConnectionCassandra CassandraConnection connection) { assertFalse(connection.checkQueriesAtLeast(1, "SELECT * FROM cassandra.users;")); } @Test - void checkQueriesAtLeastWhenMore(@ContainerCassandraConnection CassandraConnection connection) { + void checkQueriesAtLeastWhenMore(@ConnectionCassandra CassandraConnection connection) { connection.execute("INSERT INTO cassandra.users(id) VALUES(1);"); connection.execute("INSERT INTO cassandra.users(id) VALUES(2);"); assertTrue(connection.checkQueriesAtLeast(1, "SELECT * FROM cassandra.users;")); } @Test - void checkQueriesAtLeastWhenEquals(@ContainerCassandraConnection CassandraConnection connection) { + void checkQueriesAtLeastWhenEquals(@ConnectionCassandra CassandraConnection connection) { connection.execute("INSERT INTO cassandra.users(id) VALUES(1);"); assertTrue(connection.checkQueriesAtLeast(1, "SELECT * FROM cassandra.users;")); } @Test - void checkQueriesExactWhenZero(@ContainerCassandraConnection CassandraConnection connection) { + void checkQueriesExactWhenZero(@ConnectionCassandra CassandraConnection connection) { assertFalse(connection.checkQueriesEquals(1, "SELECT * FROM cassandra.users;")); } @Test - void checkQueriesExactWhenMore(@ContainerCassandraConnection CassandraConnection connection) { + void checkQueriesExactWhenMore(@ConnectionCassandra CassandraConnection connection) { connection.execute("INSERT INTO cassandra.users(id) VALUES(1);"); connection.execute("INSERT INTO cassandra.users(id) VALUES(2);"); assertFalse(connection.checkQueriesEquals(1, "SELECT * FROM cassandra.users;")); } @Test - void checkQueriesExactWhenEquals(@ContainerCassandraConnection CassandraConnection connection) { + void checkQueriesExactWhenEquals(@ConnectionCassandra CassandraConnection connection) { connection.execute("INSERT INTO cassandra.users(id) VALUES(1);"); assertTrue(connection.checkQueriesEquals(1, "SELECT * FROM cassandra.users;")); } diff --git a/cassandra/src/test/java/io/goodforgod/testcontainers/extensions/cassandra/CassandraExtraMigrationTests.java b/cassandra/src/test/java/io/goodforgod/testcontainers/extensions/cassandra/CassandraExtraMigrationTests.java deleted file mode 100644 index 292bb41..0000000 --- a/cassandra/src/test/java/io/goodforgod/testcontainers/extensions/cassandra/CassandraExtraMigrationTests.java +++ /dev/null @@ -1,20 +0,0 @@ -package io.goodforgod.testcontainers.extensions.cassandra; - -import java.util.List; -import org.junit.jupiter.api.Test; -import org.testcontainers.utility.DockerImageName; - -class CassandraExtraMigrationTests { - - @Test - void script() { - try (var container = new CassandraContainerExtra<>(DockerImageName.parse("cassandra:4.1"))) { - container.start(); - ScriptCassandraMigrationEngine scriptCassandraMigrationEngine = new ScriptCassandraMigrationEngine( - container.connection()); - scriptCassandraMigrationEngine.migrate(List.of("migration")); - container.connection().assertQueriesNone("SELECT * FROM cassandra.users;"); - scriptCassandraMigrationEngine.drop(List.of("migration")); - } - } -} diff --git a/cassandra/src/test/java/io/goodforgod/testcontainers/extensions/cassandra/CassandraSimplePerClassMigrationTests.java b/cassandra/src/test/java/io/goodforgod/testcontainers/extensions/cassandra/CassandraSimplePerClassMigrationTests.java index a3eeb69..02a677d 100644 --- a/cassandra/src/test/java/io/goodforgod/testcontainers/extensions/cassandra/CassandraSimplePerClassMigrationTests.java +++ b/cassandra/src/test/java/io/goodforgod/testcontainers/extensions/cassandra/CassandraSimplePerClassMigrationTests.java @@ -11,31 +11,31 @@ engine = Migration.Engines.SCRIPTS, apply = Migration.Mode.PER_CLASS, drop = Migration.Mode.PER_CLASS, - migrations = { "migration" })) + locations = { "migration" })) @TestMethodOrder(MethodOrderer.OrderAnnotation.class) class CassandraSimplePerClassMigrationTests { @BeforeAll - public static void setupAll(@ContainerCassandraConnection CassandraConnection paramConnection) { + public static void setupAll(@ConnectionCassandra CassandraConnection paramConnection) { paramConnection.queryOne("SELECT * FROM cassandra.users;", r -> r.getInt(0)); assertNotNull(paramConnection); } @BeforeEach - public void setupEach(@ContainerCassandraConnection CassandraConnection paramConnection) { + public void setupEach(@ConnectionCassandra CassandraConnection paramConnection) { paramConnection.queryOne("SELECT * FROM cassandra.users;", r -> r.getInt(0)); assertNotNull(paramConnection); } @Order(1) @Test - void firstRun(@ContainerCassandraConnection CassandraConnection connection) { + void firstRun(@ConnectionCassandra CassandraConnection connection) { connection.execute("INSERT INTO cassandra.users(id) VALUES(1);"); } @Order(2) @Test - void secondRun(@ContainerCassandraConnection CassandraConnection connection) { + void secondRun(@ConnectionCassandra CassandraConnection connection) { var usersFound = connection.queryOne("SELECT * FROM cassandra.users;", r -> r.getInt(0)).orElse(null); assertEquals(1, usersFound); } diff --git a/cassandra/src/test/java/io/goodforgod/testcontainers/extensions/cassandra/CassandraSimplePerMethodMigrationTests.java b/cassandra/src/test/java/io/goodforgod/testcontainers/extensions/cassandra/CassandraSimplePerMethodMigrationTests.java index c195c2d..30d450c 100644 --- a/cassandra/src/test/java/io/goodforgod/testcontainers/extensions/cassandra/CassandraSimplePerMethodMigrationTests.java +++ b/cassandra/src/test/java/io/goodforgod/testcontainers/extensions/cassandra/CassandraSimplePerMethodMigrationTests.java @@ -11,25 +11,25 @@ engine = Migration.Engines.SCRIPTS, apply = Migration.Mode.PER_METHOD, drop = Migration.Mode.PER_METHOD, - migrations = { "migration" })) + locations = { "migration" })) @TestMethodOrder(MethodOrderer.OrderAnnotation.class) class CassandraSimplePerMethodMigrationTests { @BeforeEach - public void setupEach(@ContainerCassandraConnection CassandraConnection paramConnection) { + public void setupEach(@ConnectionCassandra CassandraConnection paramConnection) { paramConnection.queryOne("SELECT * FROM cassandra.users;", r -> r.getInt(0)); assertNotNull(paramConnection); } @Order(1) @Test - void firstRun(@ContainerCassandraConnection CassandraConnection connection) { + void firstRun(@ConnectionCassandra CassandraConnection connection) { connection.execute("INSERT INTO cassandra.users(id) VALUES(1);"); } @Order(2) @Test - void secondRun(@ContainerCassandraConnection CassandraConnection connection) { + void secondRun(@ConnectionCassandra CassandraConnection connection) { var usersFound = connection.queryOne("SELECT * FROM cassandra.users;", r -> r.getInt(1)); assertTrue(usersFound.isEmpty()); } diff --git a/cockroachdb/README.md b/cockroachdb/README.md index d7b5d92..f1a67ed 100644 --- a/cockroachdb/README.md +++ b/cockroachdb/README.md @@ -18,7 +18,7 @@ Features: **Gradle** ```groovy -testImplementation "io.goodforgod:testcontainers-extensions-cockroachdb:0.9.6" +testImplementation "io.goodforgod:testcontainers-extensions-cockroachdb:0.10.0" ``` **Maven** @@ -26,7 +26,7 @@ testImplementation "io.goodforgod:testcontainers-extensions-cockroachdb:0.9.6" io.goodforgod testcontainers-extensions-cockroachdb - 0.9.6 + 0.10.0 test ``` @@ -52,9 +52,8 @@ testRuntimeOnly "org.postgresql:postgresql:42.6.0" ## Content - [Usage](#usage) -- [Container](#container) - - [Connection](#container-connection) - - [Migration](#container-migration) +- [Connection](#connection) + - [Migration](#connection-migration) - [Annotation](#annotation) - [Manual Container](#manual-container) - [Connection](#annotation-connection) @@ -66,15 +65,18 @@ testRuntimeOnly "org.postgresql:postgresql:42.6.0" Test with container start in `PER_RUN` mode and migration per method will look like: ```java -@TestcontainersCockroachdb(mode = ContainerMode.PER_RUN, +@TestcontainersCockroach(mode = ContainerMode.PER_RUN, migration = @Migration( engine = Migration.Engines.FLYWAY, apply = Migration.Mode.PER_METHOD, drop = Migration.Mode.PER_METHOD)) class ExampleTests { + @ConnectionCockroach + private JdbcConnection connection; + @Test - void test(@ContainerCockroachdbConnection JdbcConnection connection) { + void test() { connection.execute("INSERT INTO users VALUES(1);"); var usersFound = connection.queryMany("SELECT * FROM users;", r -> r.getInt(1)); assertEquals(1, usersFound.size()); @@ -82,56 +84,50 @@ class ExampleTests { } ``` -## Container +## Connection -Library provides special `CockorackContainerExtra` with ability for migration and connection. -It can be used with [Testcontainers JUnit Extension](https://java.testcontainers.org/test_framework_integration/junit_5/). +`JdbcConnection` is an abstraction with asserting data in database container and easily manipulate container connection settings. +You can inject connection via `@ConnectionCockroach` as field or method argument or manually create it from container or manual settings. ```java class ExampleTests { + private static final CockroachContainer container = new CockroachContainer(); + @Test void test() { - try (var container = new CockorackContainerExtra(DockerImageName.parse("cockroachdb/cockroach:latest-v23.1"))) { - container.start(); - } + container.start(); + JdbcConnection connection = JdbcConnection.forContainer(container); + connection.execute("INSERT INTO users VALUES(1);"); } } ``` -### Container Connection +### Connection Migration -`JdbcConnection` provides connection parameters, useful asserts, checks, etc. for easier testing. +`Migrations` allow easily migrate database between test executions and drop after tests. +You can migrate container via `@TestcontainersCockroach#migration` annotation parameter or manually using `JdbcConnection`. ```java +@TestcontainersCockroach class ExampleTests { - @Test - void test() { - try (var container = new CockorackContainerExtra(DockerImageName.parse("cockroachdb/cockroach:latest-v23.1"))) { - container.start(); - container.connection().assertQueriesNone("SELECT * FROM users;"); + @Test + void test(@ConnectionCockroach JdbcConnection connection) { + connection.migrationEngine(Migration.Engines.FLYWAY).apply("db/migration"); + connection.execute("INSERT INTO users VALUES(1);"); + connection.migrationEngine(Migration.Engines.FLYWAY).drop("db/migration"); } - } } ``` -### Container Migration - -`Migrations` allow easily migrate database between test executions and drop after tests. - -Annotation parameters: -- `engine` - to use for migration. -- `apply` - parameter configures migration mode. -- `drop` - configures when to reset/drop/clear database. - Available migration engines: - [Flyway](https://documentation.red-gate.com/fd/cockroachdb-184127591.html) - [Liquibase](https://www.liquibase.com/databases/cockroachdb-2) ## Annotation -`@TestcontainersCockroachdb` - allow **automatically start container** with specified image in different modes without the need to configure it. +`@TestcontainersCockroach` - allow **automatically start container** with specified image in different modes without the need to configure it. Available containers modes: @@ -141,11 +137,11 @@ Available containers modes: Simple example on how to start container per class, **no need to configure** container: ```java -@TestcontainersCockroachdb(mode = ContainerMode.PER_CLASS) +@TestcontainersCockroach(mode = ContainerMode.PER_CLASS) class ExampleTests { @Test - void test(@ContainerCockroachdbConnection JdbcConnection connection) { + void test(@ConnectionCockroach JdbcConnection connection) { assertNotNull(connection); } } @@ -157,7 +153,7 @@ It is possible to customize image with annotation `image` parameter. Image also can be provided from environment variable: ```java -@TestcontainersCockroachdb(image = "${MY_IMAGE_ENV|cockroachdb/cockroach:latest-v23.1}") +@TestcontainersCockroach(image = "${MY_IMAGE_ENV|cockroachdb/cockroach:latest-v23.1}") class ExampleTests { @Test @@ -175,19 +171,19 @@ Image syntax: ### Manual Container -When you need to **manually configure container** with specific options, you can provide such container as instance that will be used by `@TestcontainersCockroachdb`, -this can be done using `@ContainerCockroachdb` annotation for container. +When you need to **manually configure container** with specific options, you can provide such container as instance that will be used by `@TestcontainersCockroach`, +this can be done using `@ContainerCockroach` annotation for container. Example: ```java -@TestcontainersCockroachdb(mode = ContainerMode.PER_CLASS) +@TestcontainersCockroach(mode = ContainerMode.PER_CLASS) class ExampleTests { - @ContainerCockroachdb - private static final CockroachContainer container = new CockroachContainer(dockerImage); + @ContainerCockroach + private static final CockroachContainer container = new CockroachContainer(); @Test - void test(@ContainerCockroachdbConnection JdbcConnection connection) { + void test(@ConnectionCockroach JdbcConnection connection) { // do something } } @@ -197,7 +193,7 @@ class ExampleTests { In case you want to enable [Network.SHARED](https://java.testcontainers.org/features/networking/) for containers you can do this using `network` & `shared` parameter in annotation: ```java -@TestcontainersCockroachdb(network = @Network(shared = true)) +@TestcontainersCockroach(network = @Network(shared = true)) class ExampleTests { @Test @@ -214,7 +210,7 @@ Alias can be extracted from environment variable also or default value can be pr In case specified environment variable is missing `default alias` will be created: ```java -@TestcontainersCockroachdb(network = @Network(alias = "${MY_ALIAS_ENV|my_default_alias}")) +@TestcontainersCockroach(network = @Network(alias = "${MY_ALIAS_ENV|my_default_alias}")) class ExampleTests { @Test @@ -232,19 +228,19 @@ Image syntax: ### Annotation Connection -`JdbcConnection` - can be injected to field or method parameter and used to communicate with running container via `@ContainerCockroachdbConnection` annotation. +`JdbcConnection` - can be injected to field or method parameter and used to communicate with running container via `@ConnectionCockroach` annotation. `JdbcConnection` provides connection parameters, useful asserts, checks, etc. for easier testing. Example: ```java -@TestcontainersCockroachdb(mode = ContainerMode.PER_CLASS, image = "cockroachdb/cockroach:latest-v23.1") +@TestcontainersCockroach(mode = ContainerMode.PER_CLASS, image = "cockroachdb/cockroach:latest-v23.1") class ExampleTests { - @ContainerCockroachdbConnection - private JdbcConnection connectionInField; + @ConnectionCockroach + private JdbcConnection connection; @Test - void test(@ContainerCockroachdbConnection JdbcConnection connection) { + void test() { connection.execute("CREATE TABLE users (id INT NOT NULL PRIMARY KEY);"); connection.execute("INSERT INTO users VALUES(1);"); connection.assertInserted("INSERT INTO users VALUES(2);"); @@ -261,17 +257,17 @@ In case you want to use some external Cockroachdb instance that is running in CI you can use special *environment variables* and extension will use them to propagate connection and no Cockroachdb containers will be running in such case. Special environment variables: -- `EXTERNAL_TEST_COCKROACHDB_JDBC_URL` - Cockroachdb instance JDBC url. -- `EXTERNAL_TEST_COCKROACHDB_USERNAME` - Cockroachdb instance username (optional). -- `EXTERNAL_TEST_COCKROACHDB_PASSWORD` - Cockroachdb instance password (optional). -- `EXTERNAL_TEST_COCKROACHDB_HOST` - Cockroachdb instance host (optional if JDBC url specified). -- `EXTERNAL_TEST_COCKROACHDB_PORT` - Cockroachdb instance port (optional if JDBC url specified). -- `EXTERNAL_TEST_COCKROACHDB_DATABASE` - Cockroachdb instance database (`cockroachdb` by default) (optional if JDBC url specified) +- `EXTERNAL_TEST_COCKROACH_JDBC_URL` - Cockroachdb instance JDBC url. +- `EXTERNAL_TEST_COCKROACH_USERNAME` - Cockroachdb instance username (optional). +- `EXTERNAL_TEST_COCKROACH_PASSWORD` - Cockroachdb instance password (optional). +- `EXTERNAL_TEST_COCKROACH_HOST` - Cockroachdb instance host (optional if JDBC url specified). +- `EXTERNAL_TEST_COCKROACH_PORT` - Cockroachdb instance port (optional if JDBC url specified). +- `EXTERNAL_TEST_COCKROACH_DATABASE` - Cockroachdb instance database (`cockroachdb` by default) (optional if JDBC url specified) -Use can use either `EXTERNAL_TEST_COCKROACHDB_JDBC_URL` to specify connection with username & password combination -or use combination of `EXTERNAL_TEST_COCKROACHDB_HOST` & `EXTERNAL_TEST_COCKROACHDB_PORT` & `EXTERNAL_TEST_COCKROACHDB_DATABASE`. +Use can use either `EXTERNAL_TEST_COCKROACH_JDBC_URL` to specify connection with username & password combination +or use combination of `EXTERNAL_TEST_COCKROACH_HOST` & `EXTERNAL_TEST_COCKROACH_PORT` & `EXTERNAL_TEST_COCKROACH_DATABASE`. -`EXTERNAL_TEST_COCKROACHDB_JDBC_URL` env have higher priority over host & port & database. +`EXTERNAL_TEST_COCKROACH_JDBC_URL` env have higher priority over host & port & database. ### Annotation Migration @@ -281,6 +277,7 @@ Annotation parameters: - `engine` - to use for migration. - `apply` - parameter configures migration mode. - `drop` - configures when to reset/drop/clear database. +- `locations` - configures locations where migrations are placed. Available migration engines: - [Flyway](https://documentation.red-gate.com/fd/cockroachdb-184127591.html) @@ -296,7 +293,7 @@ CREATE TABLE IF NOT EXISTS users Test with container and migration per method will look like: ```java -@TestcontainersCockroachdb(mode = ContainerMode.PER_CLASS, +@TestcontainersCockroach(mode = ContainerMode.PER_CLASS, migration = @Migration( engine = Migration.Engines.FLYWAY, apply = Migration.Mode.PER_METHOD, @@ -304,7 +301,7 @@ Test with container and migration per method will look like: class ExampleTests { @Test - void test(@ContainerCockroachdbConnection JdbcConnection connection) { + void test(@ConnectionCockroach JdbcConnection connection) { connection.execute("INSERT INTO users VALUES(1);"); var usersFound = connection.queryMany("SELECT * FROM users;", r -> r.getInt(1)); assertEquals(1, usersFound.size()); diff --git a/cockroachdb/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/CockroachContainerExtra.java b/cockroachdb/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/CockroachContainerExtra.java deleted file mode 100644 index 19be85e..0000000 --- a/cockroachdb/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/CockroachContainerExtra.java +++ /dev/null @@ -1,135 +0,0 @@ -package io.goodforgod.testcontainers.extensions.jdbc; - -import java.time.Duration; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import org.jetbrains.annotations.ApiStatus.Internal; -import org.jetbrains.annotations.NotNull; -import org.slf4j.LoggerFactory; -import org.testcontainers.containers.CockroachContainer; -import org.testcontainers.containers.output.Slf4jLogConsumer; -import org.testcontainers.utility.DockerImageName; - -public class CockroachContainerExtra extends CockroachContainer { - - private static final String PROTOCOL = "postgresql"; - private static final int PORT = 26257; - - private static final String EXTERNAL_TEST_COCKROACHDB_JDBC_URL = "EXTERNAL_TEST_COCKROACHDB_JDBC_URL"; - private static final String EXTERNAL_TEST_COCKROACHDB_USERNAME = "EXTERNAL_TEST_COCKROACHDB_USERNAME"; - private static final String EXTERNAL_TEST_COCKROACHDB_PASSWORD = "EXTERNAL_TEST_COCKROACHDB_PASSWORD"; - private static final String EXTERNAL_TEST_COCKROACHDB_HOST = "EXTERNAL_TEST_COCKROACHDB_HOST"; - private static final String EXTERNAL_TEST_COCKROACHDB_PORT = "EXTERNAL_TEST_COCKROACHDB_PORT"; - private static final String EXTERNAL_TEST_COCKROACHDB_DATABASE = "EXTERNAL_TEST_COCKROACHDB_DATABASE"; - - private volatile JdbcConnectionImpl connection; - private volatile FlywayJdbcMigrationEngine flywayJdbcMigrationEngine; - private volatile LiquibaseJdbcMigrationEngine liquibaseJdbcMigrationEngine; - - public CockroachContainerExtra(String dockerImageName) { - this(DockerImageName.parse(dockerImageName)); - } - - public CockroachContainerExtra(DockerImageName dockerImageName) { - super(dockerImageName); - - final String alias = "cockroachdb-" + System.currentTimeMillis(); - this.withLogConsumer(new Slf4jLogConsumer(LoggerFactory.getLogger(CockroachContainerExtra.class)) - .withMdc("image", dockerImageName.asCanonicalNameString()) - .withMdc("alias", alias)); - this.withStartupTimeout(Duration.ofMinutes(5)); - - this.setNetworkAliases(new ArrayList<>(List.of(alias))); - } - - @Internal - JdbcMigrationEngine getMigrationEngine(@NotNull Migration.Engines engine) { - if (engine == Migration.Engines.FLYWAY) { - if (flywayJdbcMigrationEngine == null) { - this.flywayJdbcMigrationEngine = new FlywayJdbcMigrationEngine(connection()); - } - return this.flywayJdbcMigrationEngine; - } else if (engine == Migration.Engines.LIQUIBASE) { - if (liquibaseJdbcMigrationEngine == null) { - this.liquibaseJdbcMigrationEngine = new LiquibaseJdbcMigrationEngine(connection()); - } - return this.liquibaseJdbcMigrationEngine; - } else { - throw new UnsupportedOperationException("Unsupported engine: " + engine); - } - } - - @NotNull - public JdbcConnection connection() { - if (connection == null) { - final Optional connectionExternal = getConnectionExternal(); - if (connectionExternal.isEmpty() && !isRunning()) { - throw new IllegalStateException("MariadbConnection can't be create for container that is not running"); - } - - final JdbcConnection jdbcConnection = connectionExternal.orElseGet(() -> { - final String alias = getNetworkAliases().get(getNetworkAliases().size() - 1); - return JdbcConnectionImpl.forJDBC(getJdbcUrl(), - getHost(), - getMappedPort(PORT), - alias, - PORT, - getDatabaseName(), - getUsername(), - getPassword()); - }); - - this.connection = (JdbcConnectionImpl) jdbcConnection; - } - - return connection; - } - - @Override - public void start() { - final Optional connectionExternal = getConnectionExternal(); - if (connectionExternal.isEmpty()) { - super.start(); - } - } - - @Override - public void stop() { - if (flywayJdbcMigrationEngine != null) { - flywayJdbcMigrationEngine.close(); - flywayJdbcMigrationEngine = null; - } - if (liquibaseJdbcMigrationEngine != null) { - liquibaseJdbcMigrationEngine.close(); - liquibaseJdbcMigrationEngine = null; - } - if (connection != null) { - connection.close(); - connection = null; - } - super.stop(); - } - - @NotNull - private static Optional getConnectionExternal() { - var url = System.getenv(EXTERNAL_TEST_COCKROACHDB_JDBC_URL); - var host = System.getenv(EXTERNAL_TEST_COCKROACHDB_HOST); - var port = System.getenv(EXTERNAL_TEST_COCKROACHDB_PORT); - var user = System.getenv(EXTERNAL_TEST_COCKROACHDB_USERNAME); - var password = System.getenv(EXTERNAL_TEST_COCKROACHDB_PASSWORD); - - var db = Optional.ofNullable(System.getenv(EXTERNAL_TEST_COCKROACHDB_DATABASE)).orElse("cockroachdb"); - if (url != null) { - if (host != null && port != null) { - return Optional.of(JdbcConnectionImpl.forJDBC(url, host, Integer.parseInt(port), null, null, db, user, password)); - } else { - return Optional.of(JdbcConnectionImpl.forExternal(url, user, password)); - } - } else if (host != null && port != null) { - return Optional.of(JdbcConnectionImpl.forProtocol(PROTOCOL, host, Integer.parseInt(port), db, user, password)); - } else { - return Optional.empty(); - } - } -} diff --git a/cockroachdb/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/CockroachContext.java b/cockroachdb/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/CockroachContext.java new file mode 100644 index 0000000..8530115 --- /dev/null +++ b/cockroachdb/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/CockroachContext.java @@ -0,0 +1,99 @@ +package io.goodforgod.testcontainers.extensions.jdbc; + +import io.goodforgod.testcontainers.extensions.ContainerContext; +import java.util.Optional; +import org.jetbrains.annotations.ApiStatus.Internal; +import org.jetbrains.annotations.NotNull; +import org.testcontainers.containers.CockroachContainer; + +@Internal +final class CockroachContext implements ContainerContext { + + private static final String PROTOCOL = "postgresql"; + private static final int PORT = 26257; + + private static final String EXTERNAL_TEST_COCKROACH_JDBC_URL = "EXTERNAL_TEST_COCKROACH_JDBC_URL"; + private static final String EXTERNAL_TEST_COCKROACH_USERNAME = "EXTERNAL_TEST_COCKROACH_USERNAME"; + private static final String EXTERNAL_TEST_COCKROACH_PASSWORD = "EXTERNAL_TEST_COCKROACH_PASSWORD"; + private static final String EXTERNAL_TEST_COCKROACH_HOST = "EXTERNAL_TEST_COCKROACH_HOST"; + private static final String EXTERNAL_TEST_COCKROACH_PORT = "EXTERNAL_TEST_COCKROACH_PORT"; + private static final String EXTERNAL_TEST_COCKROACH_DATABASE = "EXTERNAL_TEST_COCKROACH_DATABASE"; + + private volatile JdbcConnectionImpl connection; + + private final CockroachContainer container; + + CockroachContext(CockroachContainer container) { + this.container = container; + } + + @NotNull + public JdbcConnection connection() { + if (connection == null) { + final Optional connectionExternal = getConnectionExternal(); + if (connectionExternal.isEmpty() && !container.isRunning()) { + throw new IllegalStateException("CockroachConnection can't be create for container that is not running"); + } + + final JdbcConnection containerConnection = connectionExternal.orElseGet(() -> { + final String alias = container.getNetworkAliases().get(container.getNetworkAliases().size() - 1); + return JdbcConnectionImpl.forJDBC(container.getJdbcUrl(), + container.getHost(), + container.getMappedPort(PORT), + alias, + PORT, + container.getDatabaseName(), + container.getUsername(), + container.getPassword()); + }); + + this.connection = (JdbcConnectionImpl) containerConnection; + } + + return connection; + } + + @Override + public void start() { + final Optional connectionExternal = getConnectionExternal(); + if (connectionExternal.isEmpty()) { + container.start(); + } + } + + @Override + public void stop() { + if (connection != null) { + connection.stop(); + connection = null; + } + container.stop(); + } + + @NotNull + private static Optional getConnectionExternal() { + var url = System.getenv(EXTERNAL_TEST_COCKROACH_JDBC_URL); + var host = System.getenv(EXTERNAL_TEST_COCKROACH_HOST); + var port = System.getenv(EXTERNAL_TEST_COCKROACH_PORT); + var user = System.getenv(EXTERNAL_TEST_COCKROACH_USERNAME); + var password = System.getenv(EXTERNAL_TEST_COCKROACH_PASSWORD); + + var db = Optional.ofNullable(System.getenv(EXTERNAL_TEST_COCKROACH_DATABASE)).orElse("postgres"); + if (url != null) { + if (host != null && port != null) { + return Optional.of(JdbcConnectionImpl.forJDBC(url, host, Integer.parseInt(port), null, null, db, user, password)); + } else { + return Optional.of(JdbcConnectionImpl.forExternal(url, user, password)); + } + } else if (host != null && port != null) { + return Optional.of(JdbcConnectionImpl.forProtocol(PROTOCOL, host, Integer.parseInt(port), db, user, password)); + } else { + return Optional.empty(); + } + } + + @Override + public String toString() { + return container.getDockerImageName(); + } +} diff --git a/cockroachdb/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/CockroachMetadata.java b/cockroachdb/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/CockroachMetadata.java deleted file mode 100644 index 8eb1dfe..0000000 --- a/cockroachdb/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/CockroachMetadata.java +++ /dev/null @@ -1,18 +0,0 @@ -package io.goodforgod.testcontainers.extensions.jdbc; - -import io.goodforgod.testcontainers.extensions.ContainerMode; -import org.jetbrains.annotations.ApiStatus.Internal; -import org.jetbrains.annotations.NotNull; - -@Internal -final class CockroachMetadata extends JdbcMetadata { - - CockroachMetadata(boolean network, String alias, String image, ContainerMode runMode, Migration migration) { - super(network, alias, image, runMode, migration); - } - - @Override - public @NotNull String networkAliasDefault() { - return "cockroachdb-" + System.currentTimeMillis(); - } -} diff --git a/mysql/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/ContainerMysqlConnection.java b/cockroachdb/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/ConnectionCockroach.java similarity index 87% rename from mysql/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/ContainerMysqlConnection.java rename to cockroachdb/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/ConnectionCockroach.java index 010f8e9..c82e586 100644 --- a/mysql/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/ContainerMysqlConnection.java +++ b/cockroachdb/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/ConnectionCockroach.java @@ -9,4 +9,4 @@ @Documented @Target({ ElementType.FIELD, ElementType.PARAMETER }) @Retention(RetentionPolicy.RUNTIME) -public @interface ContainerMysqlConnection {} +public @interface ConnectionCockroach {} diff --git a/cockroachdb/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/ContainerCockroachdb.java b/cockroachdb/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/ContainerCockroach.java similarity index 52% rename from cockroachdb/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/ContainerCockroachdb.java rename to cockroachdb/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/ContainerCockroach.java index 7a667f6..c9572ce 100644 --- a/cockroachdb/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/ContainerCockroachdb.java +++ b/cockroachdb/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/ContainerCockroach.java @@ -1,12 +1,15 @@ package io.goodforgod.testcontainers.extensions.jdbc; import java.lang.annotation.*; +import org.testcontainers.containers.CockroachContainer; /** - * Indicates that annotated field containers {@link CockroachContainerExtra} instance - * that should be used by {@link TestcontainersCockroachdb} rather than creating default container + * Indicates that annotated field containers {@link CockroachContainer} instance + * that should be used by {@link TestcontainersCockroach} rather than creating default container */ @Documented @Target({ ElementType.FIELD }) @Retention(RetentionPolicy.RUNTIME) -public @interface ContainerCockroachdb {} +public @interface ContainerCockroach { + +} diff --git a/cockroachdb/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/ContainerCockroachdbConnection.java b/cockroachdb/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/ContainerCockroachdbConnection.java deleted file mode 100644 index cd4b618..0000000 --- a/cockroachdb/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/ContainerCockroachdbConnection.java +++ /dev/null @@ -1,12 +0,0 @@ -package io.goodforgod.testcontainers.extensions.jdbc; - -import java.lang.annotation.*; - -/** - * Indicates that annotated field or parameter should be injected with {@link JdbcConnection} value - * of current active container - */ -@Documented -@Target({ ElementType.FIELD, ElementType.PARAMETER }) -@Retention(RetentionPolicy.RUNTIME) -public @interface ContainerCockroachdbConnection {} diff --git a/cockroachdb/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/TestcontainersCockroachdb.java b/cockroachdb/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/TestcontainersCockroach.java similarity index 82% rename from cockroachdb/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/TestcontainersCockroachdb.java rename to cockroachdb/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/TestcontainersCockroach.java index 1383b1f..f444ad0 100644 --- a/cockroachdb/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/TestcontainersCockroachdb.java +++ b/cockroachdb/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/TestcontainersCockroach.java @@ -5,18 +5,19 @@ import java.lang.annotation.*; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.extension.ExtendWith; +import org.testcontainers.containers.CockroachContainer; /** - * Extension that is running {@link CockroachContainerExtra} for tests in different modes with + * Extension that is running {@link CockroachContainer} for tests in different modes with * database * schema migration support between test executions */ @Order(Order.DEFAULT - 100) // Run before other extensions -@ExtendWith(TestcontainersCockroachdbExtension.class) +@ExtendWith(TestcontainersCockroachExtension.class) @Documented @Target({ ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) -public @interface TestcontainersCockroachdb { +@interface TestcontainersCockroach { /** * @return Cockroachdb image @@ -25,8 +26,6 @@ * 2) Image can be provided via environment variable using syntax: "${MY_IMAGE_ENV}" * 3) Image environment variable can have default value if empty using syntax: * "${MY_IMAGE_ENV|cockroachdb/cockroach:latest-v23.1}" - *

- * @see TestcontainersCockroachdbExtension#getContainerDefault(CockroachMetadata) */ String image() default "cockroachdb/cockroach:latest-v23.1"; diff --git a/cockroachdb/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/TestcontainersCockroachExtension.java b/cockroachdb/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/TestcontainersCockroachExtension.java new file mode 100644 index 0000000..db52826 --- /dev/null +++ b/cockroachdb/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/TestcontainersCockroachExtension.java @@ -0,0 +1,73 @@ +package io.goodforgod.testcontainers.extensions.jdbc; + +import io.goodforgod.testcontainers.extensions.ContainerContext; +import java.lang.annotation.Annotation; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.CockroachContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.containers.output.Slf4jLogConsumer; +import org.testcontainers.utility.DockerImageName; + +final class TestcontainersCockroachExtension extends + AbstractTestcontainersJdbcExtension { + + private static final ExtensionContext.Namespace NAMESPACE = ExtensionContext.Namespace + .create(TestcontainersCockroachExtension.class); + + @Override + protected Class getContainerType() { + return CockroachContainer.class; + } + + @Override + protected Class getContainerAnnotation() { + return ContainerCockroach.class; + } + + @Override + protected Class getConnectionAnnotation() { + return ConnectionCockroach.class; + } + + @Override + protected ExtensionContext.Namespace getNamespace() { + return NAMESPACE; + } + + @Override + protected CockroachContainer createContainerDefault(JdbcMetadata metadata) { + var image = DockerImageName.parse(metadata.image()) + .asCompatibleSubstituteFor(DockerImageName.parse("cockroachdb/cockroach")); + + final CockroachContainer container = new CockroachContainer(image); + final String alias = Optional.ofNullable(metadata.networkAlias()) + .orElseGet(() -> "cockroach-" + System.currentTimeMillis()); + container.withLogConsumer(new Slf4jLogConsumer(LoggerFactory.getLogger(CockroachContainer.class)) + .withMdc("image", image.asCanonicalNameString()) + .withMdc("alias", alias)); + container.withStartupTimeout(Duration.ofMinutes(5)); + container.setNetworkAliases(new ArrayList<>(List.of(alias))); + if (metadata.networkShared()) { + container.withNetwork(Network.SHARED); + } + + return container; + } + + @Override + protected ContainerContext createContainerContext(CockroachContainer container) { + return new CockroachContext(container); + } + + @NotNull + protected Optional findMetadata(@NotNull ExtensionContext context) { + return findAnnotation(TestcontainersCockroach.class, context) + .map(a -> new JdbcMetadata(a.network().shared(), a.network().alias(), a.image(), a.mode(), a.migration())); + } +} diff --git a/cockroachdb/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/TestcontainersCockroachdbExtension.java b/cockroachdb/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/TestcontainersCockroachdbExtension.java deleted file mode 100644 index 7a231a2..0000000 --- a/cockroachdb/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/TestcontainersCockroachdbExtension.java +++ /dev/null @@ -1,68 +0,0 @@ -package io.goodforgod.testcontainers.extensions.jdbc; - -import java.lang.annotation.Annotation; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import org.jetbrains.annotations.NotNull; -import org.junit.jupiter.api.extension.ExtensionContext; -import org.testcontainers.containers.Network; -import org.testcontainers.utility.DockerImageName; - -final class TestcontainersCockroachdbExtension extends - AbstractTestcontainersJdbcExtension { - - private static final ExtensionContext.Namespace NAMESPACE = ExtensionContext.Namespace - .create(TestcontainersCockroachdbExtension.class); - - @Override - protected Class getContainerType() { - return CockroachContainerExtra.class; - } - - @Override - protected Class getContainerAnnotation() { - return ContainerCockroachdb.class; - } - - @Override - protected Class getConnectionAnnotation() { - return ContainerCockroachdbConnection.class; - } - - @Override - protected CockroachContainerExtra getContainerDefault(CockroachMetadata metadata) { - var dockerImage = DockerImageName.parse(metadata.image()) - .asCompatibleSubstituteFor(DockerImageName.parse("cockroachdb/cockroach")); - - var container = new CockroachContainerExtra(dockerImage); - container.setNetworkAliases(new ArrayList<>(List.of(metadata.networkAliasOrDefault()))); - if (metadata.networkShared()) { - container.withNetwork(Network.SHARED); - } - - return container; - } - - @Override - protected JdbcMigrationEngine getMigrationEngine(Migration.Engines engine, ExtensionContext context) { - var containerCurrent = getContainerCurrent(context); - return containerCurrent.getMigrationEngine(engine); - } - - @Override - protected ExtensionContext.Namespace getNamespace() { - return NAMESPACE; - } - - @NotNull - protected Optional findMetadata(@NotNull ExtensionContext context) { - return findAnnotation(TestcontainersCockroachdb.class, context) - .map(a -> new CockroachMetadata(a.network().shared(), a.network().alias(), a.image(), a.mode(), a.migration())); - } - - @NotNull - protected JdbcConnection getConnectionForContainer(CockroachMetadata metadata, @NotNull CockroachContainerExtra container) { - return container.connection(); - } -} diff --git a/cockroachdb/src/test/java/io/goodforgod/testcontainers/extensions/jdbc/CockroachdbFlywayPerMethodMigrationTests.java b/cockroachdb/src/test/java/io/goodforgod/testcontainers/extensions/jdbc/CockroachdbFlywayPerMethodMigrationTests.java index 1d5d069..bb11ba3 100644 --- a/cockroachdb/src/test/java/io/goodforgod/testcontainers/extensions/jdbc/CockroachdbFlywayPerMethodMigrationTests.java +++ b/cockroachdb/src/test/java/io/goodforgod/testcontainers/extensions/jdbc/CockroachdbFlywayPerMethodMigrationTests.java @@ -8,7 +8,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestMethodOrder; -@TestcontainersCockroachdb(mode = ContainerMode.PER_CLASS, +@TestcontainersCockroach(mode = ContainerMode.PER_CLASS, image = "cockroachdb/cockroach:latest-v23.1", migration = @Migration( engine = Migration.Engines.FLYWAY, @@ -19,13 +19,13 @@ class CockroachdbFlywayPerMethodMigrationTests { @Order(1) @Test - void firstRun(@ContainerCockroachdbConnection JdbcConnection connection) { + void firstRun(@ConnectionCockroach JdbcConnection connection) { connection.execute("INSERT INTO users VALUES(1);"); } @Order(2) @Test - void secondRun(@ContainerCockroachdbConnection JdbcConnection connection) { + void secondRun(@ConnectionCockroach JdbcConnection connection) { var usersFound = connection.queryOne("SELECT * FROM users;", r -> r.getInt(1)); assertTrue(usersFound.isEmpty()); } diff --git a/cockroachdb/src/test/java/io/goodforgod/testcontainers/extensions/jdbc/CockroachdbLiquibaseMigrationPerMethodTests.java b/cockroachdb/src/test/java/io/goodforgod/testcontainers/extensions/jdbc/CockroachdbLiquibaseMigrationPerMethodTests.java index 9d5d788..1a4234d 100644 --- a/cockroachdb/src/test/java/io/goodforgod/testcontainers/extensions/jdbc/CockroachdbLiquibaseMigrationPerMethodTests.java +++ b/cockroachdb/src/test/java/io/goodforgod/testcontainers/extensions/jdbc/CockroachdbLiquibaseMigrationPerMethodTests.java @@ -8,7 +8,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestMethodOrder; -@TestcontainersCockroachdb(mode = ContainerMode.PER_CLASS, +@TestcontainersCockroach(mode = ContainerMode.PER_CLASS, image = "cockroachdb/cockroach:latest-v23.1", migration = @Migration( engine = Migration.Engines.LIQUIBASE, @@ -19,13 +19,13 @@ class CockroachdbLiquibaseMigrationPerMethodTests { @Order(1) @Test - void firstRun(@ContainerCockroachdbConnection JdbcConnection connection) { + void firstRun(@ConnectionCockroach JdbcConnection connection) { connection.execute("INSERT INTO users VALUES(1);"); } @Order(2) @Test - void secondRun(@ContainerCockroachdbConnection JdbcConnection connection) { + void secondRun(@ConnectionCockroach JdbcConnection connection) { var usersFound = connection.queryOne("SELECT * FROM users;", r -> r.getInt(1)); assertTrue(usersFound.isEmpty()); } diff --git a/common/src/main/java/io/goodforgod/testcontainers/extensions/AbstractContainerMetadata.java b/common/src/main/java/io/goodforgod/testcontainers/extensions/AbstractContainerMetadata.java index 9eeb61c..eb57f18 100644 --- a/common/src/main/java/io/goodforgod/testcontainers/extensions/AbstractContainerMetadata.java +++ b/common/src/main/java/io/goodforgod/testcontainers/extensions/AbstractContainerMetadata.java @@ -10,7 +10,6 @@ public abstract class AbstractContainerMetadata implements ContainerMetadata { private final boolean network; private final String alias; - private final String aliasOrDefault; private final String image; private final ContainerMode runMode; @@ -20,7 +19,6 @@ protected AbstractContainerMetadata(boolean network, String alias, String image, this.alias = Optional.ofNullable(getEnvValue("Alias", alias)) .filter(a -> !a.isBlank()) .orElse(null); - this.aliasOrDefault = Optional.ofNullable(this.alias).orElse(networkAliasDefault()); this.image = Optional.ofNullable(getEnvValue("Image", image)) .filter(a -> !a.isBlank()) .orElseThrow(() -> new IllegalArgumentException( @@ -66,11 +64,6 @@ public boolean networkShared() { return alias; } - @Override - public @NotNull String networkAliasOrDefault() { - return aliasOrDefault; - } - @Override public @NotNull String image() { return image; diff --git a/common/src/main/java/io/goodforgod/testcontainers/extensions/AbstractTestcontainersExtension.java b/common/src/main/java/io/goodforgod/testcontainers/extensions/AbstractTestcontainersExtension.java index 2dab74a..78017ed 100644 --- a/common/src/main/java/io/goodforgod/testcontainers/extensions/AbstractTestcontainersExtension.java +++ b/common/src/main/java/io/goodforgod/testcontainers/extensions/AbstractTestcontainersExtension.java @@ -3,14 +3,12 @@ import java.lang.annotation.Annotation; import java.lang.reflect.Field; import java.lang.reflect.Modifier; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; +import java.util.*; import java.util.concurrent.ConcurrentHashMap; import org.jetbrains.annotations.ApiStatus.Internal; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.extension.*; import org.junit.platform.commons.support.AnnotationSupport; @@ -73,10 +71,6 @@ static final class SharedContainerKey implements SharedKey { this.alias = alias; } - public String image() { - return image; - } - @Override public boolean equals(Object o) { if (this == o) @@ -101,7 +95,7 @@ public String toString() { } } - static final Map>> CLASS_TO_SHARED_CONTAINERS = new ConcurrentHashMap<>(); + static final Map>> CLASS_TO_SHARED_CONTAINERS = new ConcurrentHashMap<>(); protected final Logger logger = LoggerFactory.getLogger(getClass()); @@ -115,15 +109,15 @@ public String toString() { protected abstract Optional findMetadata(ExtensionContext context); - protected abstract Container getContainerDefault(Metadata metadata); - - protected abstract Connection getConnectionForContainer(Metadata metadata, Container container); + protected final Metadata getMetadata(ExtensionContext context) { + return findMetadata(context).orElseThrow(() -> new ExtensionConfigurationException("Extension annotation not found")); + } protected abstract ExtensionContext.Namespace getNamespace(); - protected ExtensionContainer getExtensionContainer(Container container, Connection connection) { - return new ExtensionContainerImpl<>(container, connection); - } + protected abstract Container createContainerDefault(Metadata metadata); + + protected abstract ContainerContext createContainerContext(Container container); protected final ExtensionContext.Store getStorage(ExtensionContext context) { if (context.getParent().isPresent() && context.getParent().get().getParent().isPresent()) { @@ -135,20 +129,9 @@ protected final ExtensionContext.Store getStorage(ExtensionContext context) { } } - protected final Metadata getMetadata(ExtensionContext context) { - return findMetadata(context) - .orElseThrow(() -> new ExtensionConfigurationException("Extension annotation not found")); - } - - protected final Connection getConnectionCurrent(ExtensionContext context) { - return getStorage(context).get(getConnectionType(), getConnectionType()); - } - - protected final Container getContainerCurrent(ExtensionContext context) { + protected ContainerContext getContainerContext(ExtensionContext context) { Metadata metadata = getMetadata(context); - ExtensionContainer extensionContainer = getStorage(context).get(metadata.runMode(), - ExtensionContainer.class); - return extensionContainer.container(); + return getStorage(context).get(metadata.runMode(), ContainerContext.class); } protected Optional findAnnotation(Class annotationType, ExtensionContext context) { @@ -170,63 +153,109 @@ protected Optional findAnnotation(Class annotationT return Optional.empty(); } - @SuppressWarnings("unchecked") - protected Optional getContainerFromField(ExtensionContext context) { + protected Optional findContainerFromField(ExtensionContext context) { logger.debug("Looking for {} Container...", getContainerType().getSimpleName()); - final Optional> testClass = context.getTestClass(); - if (testClass.isEmpty()) { + if (context.getTestClass().isEmpty() || context.getTestInstance().isEmpty()) { + return Optional.empty(); + } + + final Optional container = findContainerInClassField(context.getTestInstance().get()); + if (container.isPresent()) { + return container; + } else if (context.getTestClass().filter(c -> c.isAnnotationPresent(Nested.class)).isPresent()) { + return findParentTestClassIfNested(context).flatMap(this::findContainerInClassField); + } else { return Optional.empty(); } + } + + private static Optional findParentTestClassIfNested(ExtensionContext context) { + if (context.getTestClass().filter(c -> c.isAnnotationPresent(Nested.class)).isPresent()) { + return context.getTestInstance() + .flatMap(instance -> findParentTestClass(instance.getClass(), context) + .flatMap(aClass -> Arrays.stream(instance.getClass().getDeclaredFields()) + .filter(f -> f.getType().equals(aClass)) + .findFirst() + .map(f -> { + try { + f.setAccessible(true); + return f.get(instance); + } catch (IllegalAccessException e) { + throw new IllegalStateException(e); + } + }))); + } - return ReflectionUtils.findFields(testClass.get(), + return Optional.empty(); + } + + @SuppressWarnings("unchecked") + private Optional findContainerInClassField(Object testClassInstance) { + return ReflectionUtils.findFields(testClassInstance.getClass(), f -> !f.isSynthetic() && f.getAnnotation(getContainerAnnotation()) != null, ReflectionUtils.HierarchyTraversalMode.TOP_DOWN) .stream() .findFirst() - .flatMap(field -> context.getTestInstance() - .map(instance -> { - try { - field.setAccessible(true); - Object possibleContainer = field.get(instance); - if (getContainerType().isAssignableFrom(possibleContainer.getClass())) { - logger.debug("Found {} Container in field: {}", getContainerType().getSimpleName(), - field.getName()); - return ((Container) possibleContainer); - } else { - throw new IllegalArgumentException(String.format( - "Field '%s' annotated with @%s value must be instance of %s", - field.getName(), getContainerAnnotation().getSimpleName(), getContainerType())); - } - } catch (IllegalAccessException e) { - throw new IllegalStateException( - String.format("Failed retrieving value from field '%s' annotated with @%s", - field.getName(), getContainerAnnotation().getSimpleName()), - e); - } - })); + .map(field -> { + try { + field.setAccessible(true); + Object possibleContainer = field.get(testClassInstance); + if (getContainerType().isAssignableFrom(possibleContainer.getClass())) { + logger.debug("Found {} Container in field: {}", getContainerType().getSimpleName(), + field.getName()); + return ((Container) possibleContainer); + } else { + throw new IllegalArgumentException(String.format( + "Field '%s' annotated with @%s value must be instance of %s", + field.getName(), getContainerAnnotation().getSimpleName(), getContainerType())); + } + } catch (IllegalAccessException e) { + throw new IllegalStateException( + String.format("Failed retrieving value from field '%s' annotated with @%s", + field.getName(), getContainerAnnotation().getSimpleName()), + e); + } + }); } - protected void injectConnection(Connection connection, ExtensionContext context) { + private static Optional> findParentTestClass(Class childTestClass, ExtensionContext context) { + return context.getTestClass() + .filter(c -> !c.equals(childTestClass)) + .or(() -> context.getParent() + .flatMap(parentContext -> findParentTestClass(childTestClass, parentContext))); + } + + protected void injectContext(ContainerContext containerContext, ExtensionContext context) { + context.getTestInstance().ifPresent(instance -> injectContextIntoInstance(containerContext, instance)); + + if (context.getTestClass().filter(c -> c.isAnnotationPresent(Nested.class)).isPresent()) { + findParentTestClassIfNested(context).ifPresent(instance -> injectContextIntoInstance(containerContext, instance)); + } + } + + protected void injectContextIntoInstance(ContainerContext containerContext, Object testClassInstance) { Class connectionAnnotation = getConnectionAnnotation(); - List connectionFields = ReflectionUtils.findFields(context.getRequiredTestClass(), + List connectionFields = ReflectionUtils.findFields(testClassInstance.getClass(), f -> !f.isSynthetic() && !Modifier.isFinal(f.getModifiers()) && !Modifier.isStatic(f.getModifiers()) && f.getAnnotation(connectionAnnotation) != null, ReflectionUtils.HierarchyTraversalMode.TOP_DOWN); - logger.debug("Starting field injection for connection: {}", connection); - context.getTestInstance().ifPresent(instance -> { - for (Field field : connectionFields) { - try { - field.setAccessible(true); - field.set(instance, connection); - } catch (IllegalAccessException e) { - throw new IllegalStateException(String.format("Field '%s' annotated with @%s can't set connection", - field.getName(), connectionAnnotation.getSimpleName()), e); - } - } - }); + logger.debug("Starting field injection for connection: {}", containerContext.connection()); + for (Field field : connectionFields) { + injectContextIntoField(containerContext, field, testClassInstance); + } + } + + protected void injectContextIntoField(ContainerContext containerContext, Field field, Object testClassInstance) { + try { + field.setAccessible(true); + field.set(testClassInstance, containerContext.connection()); + } catch (IllegalAccessException e) { + throw new IllegalStateException(String.format("Field '%s' annotated with @%s can't set connection", + field.getName(), getConnectionAnnotation().getSimpleName()), e); + } } @Override @@ -251,9 +280,9 @@ public boolean supportsParameter(ParameterContext parameterContext, ExtensionCon public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) throws ParameterResolutionException { CallMode callMode = getCallMode(parameterContext); - Connection connection = getConnectionCurrent(extensionContext); - if (connection != null) { - return connection; + ContainerContext containerContext = getContainerContext(extensionContext); + if (containerContext != null) { + return containerContext.connection(); } else { Metadata metadata = getMetadata(extensionContext); if (metadata.runMode() == ContainerMode.PER_RUN || metadata.runMode() == ContainerMode.PER_CLASS) { @@ -276,7 +305,7 @@ public Object resolveParameter(ParameterContext parameterContext, ExtensionConte beforeEach(extensionContext); } - return Optional.ofNullable(getConnectionCurrent(extensionContext)) + return Optional.ofNullable(getContainerContext(extensionContext).connection()) .orElseThrow(() -> new ParameterResolutionException(String.format( "Parameter named '%s' with type '%s' can't be resolved cause it probably isn't initialized yet, please check extension annotation execution order", parameterContext.getParameter().getName(), getConnectionType()))); @@ -300,10 +329,9 @@ private CallMode getCallMode(ParameterContext parameterContext) { public void beforeAll(ExtensionContext context) { final Metadata metadata = getMetadata(context); final ExtensionContext.Store storage = getStorage(context); - final Connection storageConnection = getConnectionCurrent(context); - if (storageConnection == null) { + if (getContainerContext(context) == null) { if (metadata.runMode() == ContainerMode.PER_RUN) { - final Optional containerFromField = getContainerFromField(context); + final Optional containerFromField = findContainerFromField(context); final SharedKey sharedKey = containerFromField .map(c -> ((SharedKey) new SharedContainerInstance(c))) .orElseGet(() -> { @@ -327,37 +355,34 @@ public void beforeAll(ExtensionContext context) { getClass().getCanonicalName(), k -> new ConcurrentHashMap<>()); - var extensionContainer = sharedContainerMap.computeIfAbsent(sharedKey, k -> { + var containerContext = sharedContainerMap.computeIfAbsent(sharedKey, k -> { Container container = containerFromField.orElseGet(() -> { logger.debug("Getting default container for image: {}", metadata.image()); - return getContainerDefault(metadata); + return createContainerDefault(metadata); }); - logger.debug("Starting in mode '{}' container: {}", metadata.runMode(), container.getDockerImageName()); - container.withReuse(true).start(); - logger.info("Started in mode '{}' container: {}", metadata.runMode(), - container.getDockerImageName()); - Connection connection = getConnectionForContainer(metadata, container); - return getExtensionContainer(container, connection); + container.withReuse(true); + ContainerContext conContext = createContainerContext(container); + logger.debug("Starting in mode '{}' container: {}", metadata.runMode(), conContext); + conContext.start(); + logger.info("Started in mode '{}' container: {}", metadata.runMode(), conContext); + return conContext; }); - storage.put(metadata.runMode(), extensionContainer); - storage.put(getConnectionType(), extensionContainer.connection()); - injectConnection((Connection) extensionContainer.connection(), context); + storage.put(metadata.runMode(), containerContext); + injectContext((ContainerContext) containerContext, context); } else if (metadata.runMode() == ContainerMode.PER_CLASS) { - Container container = getContainerFromField(context).orElseGet(() -> { + Container container = findContainerFromField(context).orElseGet(() -> { logger.debug("Getting default container for image: {}", metadata.image()); - return getContainerDefault(metadata); + return createContainerDefault(metadata); }); - logger.debug("Starting in mode '{}' container: {}", metadata.runMode(), container.getDockerImageName()); - container.start(); - logger.info("Started in mode '{}' container: {}", metadata.runMode(), container.getDockerImageName()); - Connection connection = getConnectionForContainer(metadata, container); - ExtensionContainer extensionContainer = getExtensionContainer(container, connection); - storage.put(metadata.runMode(), extensionContainer); - storage.put(getConnectionType(), connection); - injectConnection(connection, context); + ContainerContext containerContext = createContainerContext(container); + logger.debug("Starting in mode '{}' container: {}", metadata.runMode(), containerContext); + containerContext.start(); + logger.info("Started in mode '{}' container: {}", metadata.runMode(), containerContext); + storage.put(metadata.runMode(), containerContext); + injectContext(containerContext, context); } } } @@ -366,34 +391,31 @@ public void beforeAll(ExtensionContext context) { public void beforeEach(ExtensionContext context) { Metadata metadata = getMetadata(context); ExtensionContext.Store storage = getStorage(context); - Connection storageConnection = getConnectionCurrent(context); - if (storageConnection == null) { + if (getContainerContext(context) == null) { if (metadata.runMode() == ContainerMode.PER_METHOD) { - Container container = getContainerFromField(context).orElseGet(() -> { + Container container = findContainerFromField(context).orElseGet(() -> { logger.debug("Getting default container for image: {}", metadata.image()); - return getContainerDefault(metadata); + return createContainerDefault(metadata); }); - logger.debug("Starting in mode '{}' container: {}", metadata.runMode(), container.getDockerImageName()); + ContainerContext containerContext = createContainerContext(container); + logger.debug("Starting in mode '{}' container: {}", metadata.runMode(), containerContext); container.start(); - logger.info("Started in mode '{}' container: {}", metadata.runMode(), container.getDockerImageName()); - Connection connection = getConnectionForContainer(metadata, container); - ExtensionContainer extensionContainer = getExtensionContainer(container, connection); - storage.put(metadata.runMode(), extensionContainer); - storage.put(getConnectionType(), connection); + logger.info("Started in mode '{}' container: {}", metadata.runMode(), containerContext); + storage.put(metadata.runMode(), containerContext); } } TestInstance.Lifecycle lifecycle = context.getTestInstanceLifecycle().orElse(TestInstance.Lifecycle.PER_METHOD); if (lifecycle == TestInstance.Lifecycle.PER_METHOD) { - Connection connection = getConnectionCurrent(context); - if (connection != null) { - injectConnection(connection, context); + var containerContext = getContainerContext(context); + if (containerContext != null) { + injectContext(containerContext, context); } } else if (metadata.runMode() == ContainerMode.PER_METHOD) { - Connection connection = getConnectionCurrent(context); - if (connection != null) { - injectConnection(connection, context); + var containerContext = getContainerContext(context); + if (containerContext != null) { + injectContext(containerContext, context); } } } @@ -403,14 +425,11 @@ public void afterEach(ExtensionContext context) { Metadata metadata = getMetadata(context); if (metadata.runMode() == ContainerMode.PER_METHOD) { ExtensionContext.Store storage = getStorage(context); - final ExtensionContainer extensionContainer = storage.get(metadata.runMode(), - ExtensionContainer.class); - if (extensionContainer != null) { - logger.debug("Stopping in mode '{}' container: {}", - metadata.runMode(), extensionContainer.container().getDockerImageName()); - extensionContainer.stop(); - logger.info("Stopped in mode '{}' container: {}", - metadata.runMode(), extensionContainer.container().getDockerImageName()); + final ContainerContext containerContext = getContainerContext(context); + if (containerContext != null) { + logger.debug("Stopping in mode '{}' container: {}", metadata.runMode(), containerContext); + containerContext.stop(); + logger.info("Stopped in mode '{}' container: {}", metadata.runMode(), containerContext); storage.remove(getConnectionType()); storage.remove(metadata.runMode()); @@ -420,17 +439,13 @@ public void afterEach(ExtensionContext context) { @Override public void afterAll(ExtensionContext context) { - ExtensionContext.Store storage = getStorage(context); Metadata metadata = getMetadata(context); if (metadata.runMode() == ContainerMode.PER_CLASS) { - final ExtensionContainer extensionContainer = storage.get(metadata.runMode(), - ExtensionContainer.class); - if (extensionContainer != null) { - logger.debug("Stopping in mode '{}' container: {}", - metadata.runMode(), extensionContainer.container().getDockerImageName()); - extensionContainer.stop(); - logger.info("Stopped in mode '{}' container: {}", - metadata.runMode(), extensionContainer.container().getDockerImageName()); + final ContainerContext containerContext = getContainerContext(context); + if (containerContext != null) { + logger.debug("Stopping in mode '{}' container: {}", metadata.runMode(), containerContext); + containerContext.stop(); + logger.info("Stopped in mode '{}' container: {}", metadata.runMode(), containerContext); } } } diff --git a/common/src/main/java/io/goodforgod/testcontainers/extensions/ContainerContext.java b/common/src/main/java/io/goodforgod/testcontainers/extensions/ContainerContext.java new file mode 100644 index 0000000..cca5247 --- /dev/null +++ b/common/src/main/java/io/goodforgod/testcontainers/extensions/ContainerContext.java @@ -0,0 +1,13 @@ +package io.goodforgod.testcontainers.extensions; + +import org.jetbrains.annotations.ApiStatus.Internal; + +@Internal +public interface ContainerContext { + + Connection connection(); + + void start(); + + void stop(); +} diff --git a/common/src/main/java/io/goodforgod/testcontainers/extensions/ContainerMetadata.java b/common/src/main/java/io/goodforgod/testcontainers/extensions/ContainerMetadata.java index 4b1200d..76cb112 100644 --- a/common/src/main/java/io/goodforgod/testcontainers/extensions/ContainerMetadata.java +++ b/common/src/main/java/io/goodforgod/testcontainers/extensions/ContainerMetadata.java @@ -18,15 +18,6 @@ public interface ContainerMetadata { @Nullable String networkAlias(); - @NotNull - String networkAliasDefault(); - - /** - * @see Network - */ - @NotNull - String networkAliasOrDefault(); - @NotNull String image(); diff --git a/common/src/main/java/io/goodforgod/testcontainers/extensions/ContainerMode.java b/common/src/main/java/io/goodforgod/testcontainers/extensions/ContainerMode.java index 7020e20..0e6850e 100644 --- a/common/src/main/java/io/goodforgod/testcontainers/extensions/ContainerMode.java +++ b/common/src/main/java/io/goodforgod/testcontainers/extensions/ContainerMode.java @@ -10,8 +10,7 @@ public enum ContainerMode { * classes *

* Container image and Container network and Container network alias must be same across annotations - * or - * will create container per image + * or container will be created such pair */ PER_RUN, /** diff --git a/common/src/main/java/io/goodforgod/testcontainers/extensions/ExtensionContainer.java b/common/src/main/java/io/goodforgod/testcontainers/extensions/ExtensionContainer.java deleted file mode 100644 index 08d0ed4..0000000 --- a/common/src/main/java/io/goodforgod/testcontainers/extensions/ExtensionContainer.java +++ /dev/null @@ -1,14 +0,0 @@ -package io.goodforgod.testcontainers.extensions; - -import org.jetbrains.annotations.ApiStatus.Internal; -import org.testcontainers.containers.GenericContainer; - -@Internal -public interface ExtensionContainer, Connection> { - - Container container(); - - Connection connection(); - - void stop(); -} diff --git a/common/src/main/java/io/goodforgod/testcontainers/extensions/ExtensionContainerImpl.java b/common/src/main/java/io/goodforgod/testcontainers/extensions/ExtensionContainerImpl.java deleted file mode 100644 index 4d474ca..0000000 --- a/common/src/main/java/io/goodforgod/testcontainers/extensions/ExtensionContainerImpl.java +++ /dev/null @@ -1,32 +0,0 @@ -package io.goodforgod.testcontainers.extensions; - -import org.jetbrains.annotations.ApiStatus.Internal; -import org.testcontainers.containers.GenericContainer; - -@Internal -final class ExtensionContainerImpl, Connection> implements - ExtensionContainer { - - private final Container container; - private final Connection connection; - - ExtensionContainerImpl(Container container, Connection connection) { - this.container = container; - this.connection = connection; - } - - @Override - public Container container() { - return container; - } - - @Override - public Connection connection() { - return connection; - } - - @Override - public void stop() { - container.stop(); - } -} diff --git a/common/src/main/java/io/goodforgod/testcontainers/extensions/TestcontainersExtensionListener.java b/common/src/main/java/io/goodforgod/testcontainers/extensions/TestcontainersExtensionListener.java index 03f4fdf..1e8a237 100644 --- a/common/src/main/java/io/goodforgod/testcontainers/extensions/TestcontainersExtensionListener.java +++ b/common/src/main/java/io/goodforgod/testcontainers/extensions/TestcontainersExtensionListener.java @@ -15,9 +15,9 @@ public final class TestcontainersExtensionListener implements TestExecutionListe public void testPlanExecutionFinished(TestPlan testPlan) { for (var imageToContainers : AbstractTestcontainersExtension.CLASS_TO_SHARED_CONTAINERS.entrySet()) { for (var imageToContainer : imageToContainers.getValue().entrySet()) { - logger.debug("Stopping in mode '{}' container: {}", ContainerMode.PER_RUN, imageToContainer.getKey()); + logger.debug("Stopping in mode '{}' container: {}", ContainerMode.PER_RUN, imageToContainer.getValue()); imageToContainer.getValue().stop(); - logger.info("Stopped in mode '{}' container: {}", ContainerMode.PER_RUN, imageToContainer.getKey()); + logger.info("Stopped in mode '{}' container: {}", ContainerMode.PER_RUN, imageToContainer.getValue()); } } } diff --git a/common/src/test/java/io/goodforgod/testcontainers/extensions/ContainerFromAnnotationTests.java b/common/src/test/java/io/goodforgod/testcontainers/extensions/ContainerFromAnnotationTests.java index fed260d..84aed56 100644 --- a/common/src/test/java/io/goodforgod/testcontainers/extensions/ContainerFromAnnotationTests.java +++ b/common/src/test/java/io/goodforgod/testcontainers/extensions/ContainerFromAnnotationTests.java @@ -18,13 +18,13 @@ class ContainerFromAnnotationTests { .withLogConsumer(new Slf4jLogConsumer(LoggerFactory.getLogger(RedisContainer.class))); @Test - void checkParams(@ContainerRedisConnection RedisConnection connection) { + void checkParams(@ConnectionRedis RedisConnection connection) { assertTrue(container.isRunning()); connection.deleteAll(); } @Test - void checkParamsAgain(@ContainerRedisConnection RedisConnection connection) { + void checkParamsAgain(@ConnectionRedis RedisConnection connection) { assertTrue(container.isRunning()); connection.deleteAll(); } diff --git a/common/src/test/java/io/goodforgod/testcontainers/extensions/ContainerNestedTests.java b/common/src/test/java/io/goodforgod/testcontainers/extensions/ContainerNestedTests.java new file mode 100644 index 0000000..6be4760 --- /dev/null +++ b/common/src/test/java/io/goodforgod/testcontainers/extensions/ContainerNestedTests.java @@ -0,0 +1,36 @@ +package io.goodforgod.testcontainers.extensions; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import io.goodforgod.testcontainers.extensions.example.*; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.output.Slf4jLogConsumer; + +@TestcontainersRedis(mode = ContainerMode.PER_METHOD) +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class ContainerNestedTests { + + @ContainerRedis + private static final RedisContainer container = new RedisContainer("redis:7.2-alpine") + .withLogConsumer(new Slf4jLogConsumer(LoggerFactory.getLogger(RedisContainer.class))); + + @Nested + public class NestedTests { + + @Test + void checkParams(@ConnectionRedis RedisConnection connection) { + assertTrue(container.isRunning()); + connection.deleteAll(); + } + + @Test + void checkParamsAgain(@ConnectionRedis RedisConnection connection) { + assertTrue(container.isRunning()); + connection.deleteAll(); + } + } +} diff --git a/common/src/test/java/io/goodforgod/testcontainers/extensions/ContainerPerClassAbstractTests.java b/common/src/test/java/io/goodforgod/testcontainers/extensions/ContainerPerClassAbstractTests.java index ea5113a..8e15466 100644 --- a/common/src/test/java/io/goodforgod/testcontainers/extensions/ContainerPerClassAbstractTests.java +++ b/common/src/test/java/io/goodforgod/testcontainers/extensions/ContainerPerClassAbstractTests.java @@ -2,7 +2,7 @@ import static org.junit.jupiter.api.Assertions.*; -import io.goodforgod.testcontainers.extensions.example.ContainerRedisConnection; +import io.goodforgod.testcontainers.extensions.example.ConnectionRedis; import io.goodforgod.testcontainers.extensions.example.RedisConnection; import io.goodforgod.testcontainers.extensions.example.TestcontainersRedis; import org.junit.jupiter.api.MethodOrderer; @@ -13,7 +13,7 @@ @TestcontainersRedis(mode = ContainerMode.PER_CLASS) abstract class ContainerPerClassAbstractTests { - @ContainerRedisConnection + @ConnectionRedis protected RedisConnection sameConnectionParent; @TestMethodOrder(MethodOrderer.OrderAnnotation.class) @@ -23,14 +23,14 @@ public static class TestClassChild extends ContainerPerClassAbstractTests { private static RedisConnection firstConnection; - TestClassChild(@ContainerRedisConnection RedisConnection sameConnectionChild) { + TestClassChild(@ConnectionRedis RedisConnection sameConnectionChild) { this.sameConnectionChild = sameConnectionChild; assertNotNull(sameConnectionChild); } @Order(1) @Test - void firstConnection(@ContainerRedisConnection RedisConnection connection) { + void firstConnection(@ConnectionRedis RedisConnection connection) { assertNull(firstConnection); assertNotNull(connection); assertNotNull(connection.params().uri()); @@ -46,7 +46,7 @@ void firstConnection(@ContainerRedisConnection RedisConnection connection) { @Order(2) @Test - void secondConnection(@ContainerRedisConnection RedisConnection connection) { + void secondConnection(@ConnectionRedis RedisConnection connection) { assertNotNull(connection); assertNotNull(connection.params().uri()); assertTrue(connection.paramsInNetwork().isPresent()); diff --git a/common/src/test/java/io/goodforgod/testcontainers/extensions/ContainerPerClassBeforeTests.java b/common/src/test/java/io/goodforgod/testcontainers/extensions/ContainerPerClassBeforeTests.java index 1c8f097..5f51106 100644 --- a/common/src/test/java/io/goodforgod/testcontainers/extensions/ContainerPerClassBeforeTests.java +++ b/common/src/test/java/io/goodforgod/testcontainers/extensions/ContainerPerClassBeforeTests.java @@ -3,7 +3,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; -import io.goodforgod.testcontainers.extensions.example.ContainerRedisConnection; +import io.goodforgod.testcontainers.extensions.example.ConnectionRedis; import io.goodforgod.testcontainers.extensions.example.RedisConnection; import io.goodforgod.testcontainers.extensions.example.TestcontainersRedis; import org.junit.jupiter.api.*; @@ -16,25 +16,25 @@ class ContainerPerClassBeforeTests { private static RedisConnection setupEach; private static RedisConnection setupFirst; - @ContainerRedisConnection + @ConnectionRedis private RedisConnection fieldConnection; @BeforeAll - public static void setupAll(@ContainerRedisConnection RedisConnection paramConnection) { + public static void setupAll(@ConnectionRedis RedisConnection paramConnection) { assertNotNull(paramConnection); assertNotNull(paramConnection.params().uri()); setupAll = paramConnection; } @BeforeEach - public void setupEach(@ContainerRedisConnection RedisConnection paramConnection) { + public void setupEach(@ConnectionRedis RedisConnection paramConnection) { assertEquals(fieldConnection, paramConnection); setupEach = paramConnection; } @Order(1) @Test - void firstConnection(@ContainerRedisConnection RedisConnection paramConnection) { + void firstConnection(@ConnectionRedis RedisConnection paramConnection) { assertNotNull(paramConnection); assertNotNull(paramConnection.params().uri()); assertNotNull(fieldConnection); @@ -46,7 +46,7 @@ void firstConnection(@ContainerRedisConnection RedisConnection paramConnection) @Order(2) @Test - void secondConnection(@ContainerRedisConnection RedisConnection paramConnection) { + void secondConnection(@ConnectionRedis RedisConnection paramConnection) { assertNotNull(paramConnection); assertNotNull(paramConnection.params().uri()); assertNotNull(fieldConnection); diff --git a/common/src/test/java/io/goodforgod/testcontainers/extensions/ContainerPerClassConstructorTests.java b/common/src/test/java/io/goodforgod/testcontainers/extensions/ContainerPerClassConstructorTests.java index 38ce461..6d715ba 100644 --- a/common/src/test/java/io/goodforgod/testcontainers/extensions/ContainerPerClassConstructorTests.java +++ b/common/src/test/java/io/goodforgod/testcontainers/extensions/ContainerPerClassConstructorTests.java @@ -2,7 +2,7 @@ import static org.junit.jupiter.api.Assertions.*; -import io.goodforgod.testcontainers.extensions.example.ContainerRedisConnection; +import io.goodforgod.testcontainers.extensions.example.ConnectionRedis; import io.goodforgod.testcontainers.extensions.example.RedisConnection; import io.goodforgod.testcontainers.extensions.example.TestcontainersRedis; import org.junit.jupiter.api.*; @@ -17,14 +17,14 @@ class ContainerPerClassConstructorTests { private static RedisConnection firstConnection; - ContainerPerClassConstructorTests(@ContainerRedisConnection RedisConnection sameConnection) { + ContainerPerClassConstructorTests(@ConnectionRedis RedisConnection sameConnection) { this.sameConnection = sameConnection; assertNotNull(sameConnection); } @Order(1) @Test - void firstConnection(@ContainerRedisConnection RedisConnection connection) { + void firstConnection(@ConnectionRedis RedisConnection connection) { assertEquals("my_alias", connection.paramsInNetwork().orElseThrow().host()); assertNull(firstConnection); assertNotNull(connection); @@ -36,7 +36,7 @@ void firstConnection(@ContainerRedisConnection RedisConnection connection) { @Order(2) @Test - void secondConnection(@ContainerRedisConnection RedisConnection connection) { + void secondConnection(@ConnectionRedis RedisConnection connection) { assertEquals("my_alias", connection.paramsInNetwork().orElseThrow().host()); assertNotNull(connection); assertNotNull(connection.params().uri()); diff --git a/common/src/test/java/io/goodforgod/testcontainers/extensions/ContainerPerClassInstanceClassTests.java b/common/src/test/java/io/goodforgod/testcontainers/extensions/ContainerPerClassInstanceClassTests.java index 0bb9858..3957c81 100644 --- a/common/src/test/java/io/goodforgod/testcontainers/extensions/ContainerPerClassInstanceClassTests.java +++ b/common/src/test/java/io/goodforgod/testcontainers/extensions/ContainerPerClassInstanceClassTests.java @@ -2,7 +2,7 @@ import static org.junit.jupiter.api.Assertions.*; -import io.goodforgod.testcontainers.extensions.example.ContainerRedisConnection; +import io.goodforgod.testcontainers.extensions.example.ConnectionRedis; import io.goodforgod.testcontainers.extensions.example.RedisConnection; import io.goodforgod.testcontainers.extensions.example.TestcontainersRedis; import org.junit.jupiter.api.*; @@ -14,14 +14,14 @@ @TestInstance(TestInstance.Lifecycle.PER_CLASS) class ContainerPerClassInstanceClassTests { - @ContainerRedisConnection + @ConnectionRedis private RedisConnection sameConnection; private static RedisConnection firstConnection; @Order(1) @Test - void firstConnection(@ContainerRedisConnection RedisConnection connection) { + void firstConnection(@ConnectionRedis RedisConnection connection) { assertEquals("my_alias_env", connection.paramsInNetwork().orElseThrow().host()); assertNull(firstConnection); assertNotNull(connection); @@ -33,7 +33,7 @@ void firstConnection(@ContainerRedisConnection RedisConnection connection) { @Order(2) @Test - void secondConnection(@ContainerRedisConnection RedisConnection connection) { + void secondConnection(@ConnectionRedis RedisConnection connection) { assertEquals("my_alias_env", connection.paramsInNetwork().orElseThrow().host()); assertNotNull(connection); assertNotNull(connection.params().uri()); diff --git a/common/src/test/java/io/goodforgod/testcontainers/extensions/ContainerPerClassInstanceMethodTests.java b/common/src/test/java/io/goodforgod/testcontainers/extensions/ContainerPerClassInstanceMethodTests.java index f8c34be..508e7db 100644 --- a/common/src/test/java/io/goodforgod/testcontainers/extensions/ContainerPerClassInstanceMethodTests.java +++ b/common/src/test/java/io/goodforgod/testcontainers/extensions/ContainerPerClassInstanceMethodTests.java @@ -2,7 +2,7 @@ import static org.junit.jupiter.api.Assertions.*; -import io.goodforgod.testcontainers.extensions.example.ContainerRedisConnection; +import io.goodforgod.testcontainers.extensions.example.ConnectionRedis; import io.goodforgod.testcontainers.extensions.example.RedisConnection; import io.goodforgod.testcontainers.extensions.example.TestcontainersRedis; import org.junit.jupiter.api.*; @@ -14,14 +14,14 @@ @TestInstance(TestInstance.Lifecycle.PER_METHOD) class ContainerPerClassInstanceMethodTests { - @ContainerRedisConnection + @ConnectionRedis private RedisConnection sameConnection; private static RedisConnection firstConnection; @Order(1) @Test - void firstConnection(@ContainerRedisConnection RedisConnection connection) { + void firstConnection(@ConnectionRedis RedisConnection connection) { assertEquals("my_default_alias", connection.paramsInNetwork().orElseThrow().host()); assertNull(firstConnection); assertNotNull(connection); @@ -33,7 +33,7 @@ void firstConnection(@ContainerRedisConnection RedisConnection connection) { @Order(2) @Test - void secondConnection(@ContainerRedisConnection RedisConnection connection) { + void secondConnection(@ConnectionRedis RedisConnection connection) { assertEquals("my_default_alias", connection.paramsInNetwork().orElseThrow().host()); assertNotNull(connection); assertNotNull(connection.params().uri()); diff --git a/common/src/test/java/io/goodforgod/testcontainers/extensions/ContainerPerClassNestedTests.java b/common/src/test/java/io/goodforgod/testcontainers/extensions/ContainerPerClassNestedTests.java new file mode 100644 index 0000000..da5260f --- /dev/null +++ b/common/src/test/java/io/goodforgod/testcontainers/extensions/ContainerPerClassNestedTests.java @@ -0,0 +1,92 @@ +package io.goodforgod.testcontainers.extensions; + +import static org.junit.jupiter.api.Assertions.*; + +import io.goodforgod.testcontainers.extensions.example.ConnectionRedis; +import io.goodforgod.testcontainers.extensions.example.RedisConnection; +import io.goodforgod.testcontainers.extensions.example.TestcontainersRedis; +import org.junit.jupiter.api.*; + +@TestcontainersRedis(mode = ContainerMode.PER_CLASS) +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class ContainerPerClassNestedTests { + + @ConnectionRedis + private RedisConnection samePerMethodConnection; + + private static RedisConnection firstConnection; + + private static boolean nextNested = false; + + @BeforeEach + void setupNested() { + if (nextNested) { + firstConnection = null; + nextNested = false; + } + } + + @TestMethodOrder(MethodOrderer.OrderAnnotation.class) + @Nested + public class NestedFirstTests { + + @Order(1) + @Test + void firstConnection(@ConnectionRedis RedisConnection connection) { + assertNull(firstConnection); + assertNotNull(connection); + assertNotNull(connection.params().uri()); + firstConnection = connection; + assertNotNull(samePerMethodConnection); + assertEquals(samePerMethodConnection, connection); + assertSame(samePerMethodConnection, connection); + } + + @Order(2) + @Test + void secondConnection(@ConnectionRedis RedisConnection connection) { + assertNotNull(connection); + assertNotNull(connection.params().uri()); + assertNotNull(samePerMethodConnection); + assertEquals(samePerMethodConnection, connection); + assertSame(samePerMethodConnection, connection); + assertNotNull(firstConnection); + assertEquals(firstConnection, connection); + assertSame(firstConnection, connection); + + nextNested = true; + } + } + + @TestMethodOrder(MethodOrderer.OrderAnnotation.class) + @Nested + public class NestedSecondTests { + + @Order(1) + @Test + void firstConnection(@ConnectionRedis RedisConnection connection) { + assertNull(firstConnection); + assertNotNull(connection); + assertNotNull(connection.params().uri()); + firstConnection = connection; + assertNotNull(samePerMethodConnection); + assertEquals(samePerMethodConnection, connection); + assertSame(samePerMethodConnection, connection); + } + + @Order(2) + @Test + void secondConnection(@ConnectionRedis RedisConnection connection) { + assertNotNull(connection); + assertNotNull(connection.params().uri()); + assertNotNull(samePerMethodConnection); + assertEquals(samePerMethodConnection, connection); + assertSame(samePerMethodConnection, connection); + assertNotNull(firstConnection); + assertEquals(firstConnection, connection); + assertSame(firstConnection, connection); + + nextNested = true; + } + } +} diff --git a/common/src/test/java/io/goodforgod/testcontainers/extensions/ContainerPerMethodBeforeTests.java b/common/src/test/java/io/goodforgod/testcontainers/extensions/ContainerPerMethodBeforeTests.java index 2db972e..ae29405 100644 --- a/common/src/test/java/io/goodforgod/testcontainers/extensions/ContainerPerMethodBeforeTests.java +++ b/common/src/test/java/io/goodforgod/testcontainers/extensions/ContainerPerMethodBeforeTests.java @@ -2,7 +2,7 @@ import static org.junit.jupiter.api.Assertions.*; -import io.goodforgod.testcontainers.extensions.example.ContainerRedisConnection; +import io.goodforgod.testcontainers.extensions.example.ConnectionRedis; import io.goodforgod.testcontainers.extensions.example.RedisConnection; import io.goodforgod.testcontainers.extensions.example.TestcontainersRedis; import org.junit.jupiter.api.*; @@ -14,18 +14,18 @@ class ContainerPerMethodBeforeTests { private static RedisConnection setupEach; private static RedisConnection setupFirst; - @ContainerRedisConnection + @ConnectionRedis private RedisConnection fieldConnection; @BeforeEach - public void setupEach(@ContainerRedisConnection RedisConnection paramConnection) { + public void setupEach(@ConnectionRedis RedisConnection paramConnection) { assertEquals(fieldConnection, paramConnection); setupEach = paramConnection; } @Order(1) @Test - void firstConnection(@ContainerRedisConnection RedisConnection paramConnection) { + void firstConnection(@ConnectionRedis RedisConnection paramConnection) { assertNotNull(paramConnection); assertNotNull(paramConnection.params().uri()); assertNotNull(fieldConnection); @@ -36,7 +36,7 @@ void firstConnection(@ContainerRedisConnection RedisConnection paramConnection) @Order(2) @Test - void secondConnection(@ContainerRedisConnection RedisConnection paramConnection) { + void secondConnection(@ConnectionRedis RedisConnection paramConnection) { assertNotNull(paramConnection); assertNotNull(paramConnection.params().uri()); assertNotNull(fieldConnection); diff --git a/common/src/test/java/io/goodforgod/testcontainers/extensions/ContainerPerMethodConstructorTests.java b/common/src/test/java/io/goodforgod/testcontainers/extensions/ContainerPerMethodConstructorTests.java index f86a878..8058d40 100644 --- a/common/src/test/java/io/goodforgod/testcontainers/extensions/ContainerPerMethodConstructorTests.java +++ b/common/src/test/java/io/goodforgod/testcontainers/extensions/ContainerPerMethodConstructorTests.java @@ -2,7 +2,7 @@ import static org.junit.jupiter.api.Assertions.*; -import io.goodforgod.testcontainers.extensions.example.ContainerRedisConnection; +import io.goodforgod.testcontainers.extensions.example.ConnectionRedis; import io.goodforgod.testcontainers.extensions.example.RedisConnection; import io.goodforgod.testcontainers.extensions.example.TestcontainersRedis; import org.junit.jupiter.api.*; @@ -16,14 +16,14 @@ class ContainerPerMethodConstructorTests { private static RedisConnection firstConnection; - ContainerPerMethodConstructorTests(@ContainerRedisConnection RedisConnection diffPerMethod) { + ContainerPerMethodConstructorTests(@ConnectionRedis RedisConnection diffPerMethod) { this.diffPerMethod = diffPerMethod; assertNotNull(diffPerMethod); } @Order(1) @Test - void firstConnection(@ContainerRedisConnection RedisConnection connection) { + void firstConnection(@ConnectionRedis RedisConnection connection) { assertNull(firstConnection); assertNotNull(connection); assertNotNull(connection.params().uri()); @@ -34,7 +34,7 @@ void firstConnection(@ContainerRedisConnection RedisConnection connection) { @Order(2) @Test - void secondConnection(@ContainerRedisConnection RedisConnection connection) { + void secondConnection(@ConnectionRedis RedisConnection connection) { assertNotNull(connection); assertNotNull(connection.params().uri()); assertNotNull(diffPerMethod); diff --git a/common/src/test/java/io/goodforgod/testcontainers/extensions/ContainerPerMethodInstanceClassTests.java b/common/src/test/java/io/goodforgod/testcontainers/extensions/ContainerPerMethodInstanceClassTests.java index 18f319c..d86d48e 100644 --- a/common/src/test/java/io/goodforgod/testcontainers/extensions/ContainerPerMethodInstanceClassTests.java +++ b/common/src/test/java/io/goodforgod/testcontainers/extensions/ContainerPerMethodInstanceClassTests.java @@ -2,7 +2,7 @@ import static org.junit.jupiter.api.Assertions.*; -import io.goodforgod.testcontainers.extensions.example.ContainerRedisConnection; +import io.goodforgod.testcontainers.extensions.example.ConnectionRedis; import io.goodforgod.testcontainers.extensions.example.RedisConnection; import io.goodforgod.testcontainers.extensions.example.TestcontainersRedis; import org.junit.jupiter.api.*; @@ -12,14 +12,14 @@ @TestInstance(TestInstance.Lifecycle.PER_CLASS) class ContainerPerMethodInstanceClassTests { - @ContainerRedisConnection + @ConnectionRedis private RedisConnection samePerMethodConnection; private static RedisConnection firstConnection; @Order(1) @Test - void firstConnection(@ContainerRedisConnection RedisConnection connection) { + void firstConnection(@ConnectionRedis RedisConnection connection) { assertNull(firstConnection); assertNotNull(connection); assertNotNull(connection.params().uri()); @@ -30,7 +30,7 @@ void firstConnection(@ContainerRedisConnection RedisConnection connection) { @Order(2) @Test - void secondConnection(@ContainerRedisConnection RedisConnection connection) { + void secondConnection(@ConnectionRedis RedisConnection connection) { assertNotNull(connection); assertNotNull(connection.params().uri()); assertNotNull(samePerMethodConnection); diff --git a/common/src/test/java/io/goodforgod/testcontainers/extensions/ContainerPerMethodInstanceMethodTests.java b/common/src/test/java/io/goodforgod/testcontainers/extensions/ContainerPerMethodInstanceMethodTests.java index a97db56..7080aa9 100644 --- a/common/src/test/java/io/goodforgod/testcontainers/extensions/ContainerPerMethodInstanceMethodTests.java +++ b/common/src/test/java/io/goodforgod/testcontainers/extensions/ContainerPerMethodInstanceMethodTests.java @@ -2,7 +2,7 @@ import static org.junit.jupiter.api.Assertions.*; -import io.goodforgod.testcontainers.extensions.example.ContainerRedisConnection; +import io.goodforgod.testcontainers.extensions.example.ConnectionRedis; import io.goodforgod.testcontainers.extensions.example.RedisConnection; import io.goodforgod.testcontainers.extensions.example.TestcontainersRedis; import org.junit.jupiter.api.*; @@ -12,14 +12,14 @@ @TestInstance(TestInstance.Lifecycle.PER_METHOD) class ContainerPerMethodInstanceMethodTests { - @ContainerRedisConnection + @ConnectionRedis private RedisConnection samePerMethodConnection; private static RedisConnection firstConnection; @Order(1) @Test - void firstConnection(@ContainerRedisConnection RedisConnection connection) { + void firstConnection(@ConnectionRedis RedisConnection connection) { assertNull(firstConnection); assertNotNull(connection); assertNotNull(connection.params().uri()); @@ -30,7 +30,7 @@ void firstConnection(@ContainerRedisConnection RedisConnection connection) { @Order(2) @Test - void secondConnection(@ContainerRedisConnection RedisConnection connection) { + void secondConnection(@ConnectionRedis RedisConnection connection) { assertNotNull(connection); assertNotNull(connection.params().uri()); assertNotNull(samePerMethodConnection); diff --git a/common/src/test/java/io/goodforgod/testcontainers/extensions/ContainerPerMethodNestedTests.java b/common/src/test/java/io/goodforgod/testcontainers/extensions/ContainerPerMethodNestedTests.java new file mode 100644 index 0000000..ec49a18 --- /dev/null +++ b/common/src/test/java/io/goodforgod/testcontainers/extensions/ContainerPerMethodNestedTests.java @@ -0,0 +1,86 @@ +package io.goodforgod.testcontainers.extensions; + +import static org.junit.jupiter.api.Assertions.*; + +import io.goodforgod.testcontainers.extensions.example.ConnectionRedis; +import io.goodforgod.testcontainers.extensions.example.RedisConnection; +import io.goodforgod.testcontainers.extensions.example.TestcontainersRedis; +import org.junit.jupiter.api.*; + +@TestcontainersRedis(mode = ContainerMode.PER_METHOD) +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class ContainerPerMethodNestedTests { + + @ConnectionRedis + private RedisConnection samePerMethodConnection; + + private static RedisConnection firstConnection; + + private static boolean nextNested = false; + + @BeforeEach + void setupNested() { + if (nextNested) { + firstConnection = null; + nextNested = false; + } + } + + @TestMethodOrder(MethodOrderer.OrderAnnotation.class) + @Nested + public class NestedFirstTests { + + @Order(1) + @Test + void firstConnection(@ConnectionRedis RedisConnection connection) { + assertNull(firstConnection); + assertNotNull(connection); + assertNotNull(connection.params().uri()); + firstConnection = connection; + assertNotNull(samePerMethodConnection); + assertEquals(samePerMethodConnection, connection); + } + + @Order(2) + @Test + void secondConnection(@ConnectionRedis RedisConnection connection) { + assertNotNull(connection); + assertNotNull(connection.params().uri()); + assertNotNull(samePerMethodConnection); + assertEquals(samePerMethodConnection, connection); + assertNotNull(firstConnection); + assertNotEquals(firstConnection, connection); + + nextNested = true; + } + } + + @TestMethodOrder(MethodOrderer.OrderAnnotation.class) + @Nested + public class NestedSecondTests { + + @Order(1) + @Test + void firstConnection(@ConnectionRedis RedisConnection connection) { + assertNull(firstConnection); + assertNotNull(connection); + assertNotNull(connection.params().uri()); + firstConnection = connection; + assertNotNull(samePerMethodConnection); + assertEquals(samePerMethodConnection, connection); + } + + @Order(2) + @Test + void secondConnection(@ConnectionRedis RedisConnection connection) { + assertNotNull(connection); + assertNotNull(connection.params().uri()); + assertNotNull(samePerMethodConnection); + assertEquals(samePerMethodConnection, connection); + assertNotNull(firstConnection); + assertNotEquals(firstConnection, connection); + + nextNested = true; + } + } +} diff --git a/common/src/test/java/io/goodforgod/testcontainers/extensions/ContainerPerRunBeforeTests.java b/common/src/test/java/io/goodforgod/testcontainers/extensions/ContainerPerRunBeforeTests.java index 86d3c0a..495ad4c 100644 --- a/common/src/test/java/io/goodforgod/testcontainers/extensions/ContainerPerRunBeforeTests.java +++ b/common/src/test/java/io/goodforgod/testcontainers/extensions/ContainerPerRunBeforeTests.java @@ -2,7 +2,7 @@ import static org.junit.jupiter.api.Assertions.*; -import io.goodforgod.testcontainers.extensions.example.ContainerRedisConnection; +import io.goodforgod.testcontainers.extensions.example.ConnectionRedis; import io.goodforgod.testcontainers.extensions.example.RedisConnection; import io.goodforgod.testcontainers.extensions.example.TestcontainersRedis; import org.junit.jupiter.api.*; @@ -15,25 +15,25 @@ class ContainerPerRunBeforeTests { private static RedisConnection setupEach; private static RedisConnection setupFirst; - @ContainerRedisConnection + @ConnectionRedis private RedisConnection fieldConnection; @BeforeAll - public static void setupAll(@ContainerRedisConnection RedisConnection paramConnection) { + public static void setupAll(@ConnectionRedis RedisConnection paramConnection) { assertNotNull(paramConnection); assertNotNull(paramConnection.params().uri()); setupAll = paramConnection; } @BeforeEach - public void setupEach(@ContainerRedisConnection RedisConnection paramConnection) { + public void setupEach(@ConnectionRedis RedisConnection paramConnection) { assertEquals(fieldConnection, paramConnection); setupEach = paramConnection; } @Order(1) @Test - void firstConnection(@ContainerRedisConnection RedisConnection paramConnection) { + void firstConnection(@ConnectionRedis RedisConnection paramConnection) { assertNotNull(paramConnection); assertNotNull(paramConnection.params().uri()); assertNotNull(fieldConnection); @@ -45,7 +45,7 @@ void firstConnection(@ContainerRedisConnection RedisConnection paramConnection) @Order(2) @Test - void secondConnection(@ContainerRedisConnection RedisConnection paramConnection) { + void secondConnection(@ConnectionRedis RedisConnection paramConnection) { assertNotNull(paramConnection); assertNotNull(paramConnection.params().uri()); assertNotNull(fieldConnection); diff --git a/common/src/test/java/io/goodforgod/testcontainers/extensions/ContainerPerRunFirstTests.java b/common/src/test/java/io/goodforgod/testcontainers/extensions/ContainerPerRunFirstTests.java index 924d055..7b52f10 100644 --- a/common/src/test/java/io/goodforgod/testcontainers/extensions/ContainerPerRunFirstTests.java +++ b/common/src/test/java/io/goodforgod/testcontainers/extensions/ContainerPerRunFirstTests.java @@ -3,7 +3,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; -import io.goodforgod.testcontainers.extensions.example.ContainerRedisConnection; +import io.goodforgod.testcontainers.extensions.example.ConnectionRedis; import io.goodforgod.testcontainers.extensions.example.RedisConnection; import io.goodforgod.testcontainers.extensions.example.TestcontainersRedis; import org.junit.jupiter.api.MethodOrderer; @@ -17,12 +17,12 @@ class ContainerPerRunFirstTests { static volatile RedisConnection perRunConnection; - @ContainerRedisConnection + @ConnectionRedis private RedisConnection sameConnection; @Order(1) @Test - void firstConnection(@ContainerRedisConnection RedisConnection connection) { + void firstConnection(@ConnectionRedis RedisConnection connection) { assertNotNull(connection); assertNotNull(connection.params().uri()); assertNotNull(sameConnection); @@ -39,7 +39,7 @@ void firstConnection(@ContainerRedisConnection RedisConnection connection) { @Order(2) @Test - void secondConnection(@ContainerRedisConnection RedisConnection connection) { + void secondConnection(@ConnectionRedis RedisConnection connection) { assertNotNull(connection); assertNotNull(connection.params().uri()); assertNotNull(sameConnection); diff --git a/common/src/test/java/io/goodforgod/testcontainers/extensions/ContainerPerRunNestedFirstTests.java b/common/src/test/java/io/goodforgod/testcontainers/extensions/ContainerPerRunNestedFirstTests.java new file mode 100644 index 0000000..7214606 --- /dev/null +++ b/common/src/test/java/io/goodforgod/testcontainers/extensions/ContainerPerRunNestedFirstTests.java @@ -0,0 +1,88 @@ +package io.goodforgod.testcontainers.extensions; + +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertSame; + +import io.goodforgod.testcontainers.extensions.example.ConnectionRedis; +import io.goodforgod.testcontainers.extensions.example.RedisConnection; +import io.goodforgod.testcontainers.extensions.example.TestcontainersRedis; +import org.junit.jupiter.api.*; + +@TestcontainersRedis(mode = ContainerMode.PER_RUN) +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class ContainerPerRunNestedFirstTests { + + static volatile RedisConnection perRunConnection; + + @ConnectionRedis + private RedisConnection sameConnection; + + @TestMethodOrder(MethodOrderer.OrderAnnotation.class) + @Nested + public class NestedFirstTests { + + @Order(1) + @Test + void firstConnection(@ConnectionRedis RedisConnection connection) { + assertNotNull(connection); + assertNotNull(connection.params().uri()); + assertNotNull(sameConnection); + assertEquals(sameConnection, connection); + + if (perRunConnection == null) { + perRunConnection = connection; + } + + if (ContainerPerRunSecondTests.perRunConnection != null) { + assertEquals(perRunConnection, ContainerPerRunSecondTests.perRunConnection); + } + } + + @Order(2) + @Test + void secondConnection(@ConnectionRedis RedisConnection connection) { + assertNotNull(connection); + assertNotNull(connection.params().uri()); + assertNotNull(sameConnection); + assertEquals(sameConnection, connection); + assertEquals(perRunConnection, connection); + assertSame(sameConnection, connection); + assertSame(perRunConnection, connection); + } + } + + @TestMethodOrder(MethodOrderer.OrderAnnotation.class) + @Nested + public class NestedSecondTests { + + @Order(1) + @Test + void firstConnection(@ConnectionRedis RedisConnection connection) { + assertNotNull(connection); + assertNotNull(connection.params().uri()); + assertNotNull(sameConnection); + assertEquals(sameConnection, connection); + assertSame(sameConnection, connection); + + if (perRunConnection == null) { + perRunConnection = connection; + } + + if (ContainerPerRunSecondTests.perRunConnection != null) { + assertEquals(perRunConnection, ContainerPerRunSecondTests.perRunConnection); + } + } + + @Order(2) + @Test + void secondConnection(@ConnectionRedis RedisConnection connection) { + assertNotNull(connection); + assertNotNull(connection.params().uri()); + assertNotNull(sameConnection); + assertEquals(sameConnection, connection); + assertEquals(perRunConnection, connection); + assertSame(sameConnection, connection); + assertSame(perRunConnection, connection); + } + } +} diff --git a/common/src/test/java/io/goodforgod/testcontainers/extensions/ContainerPerRunNestedSecondTests.java b/common/src/test/java/io/goodforgod/testcontainers/extensions/ContainerPerRunNestedSecondTests.java new file mode 100644 index 0000000..a14a9d8 --- /dev/null +++ b/common/src/test/java/io/goodforgod/testcontainers/extensions/ContainerPerRunNestedSecondTests.java @@ -0,0 +1,88 @@ +package io.goodforgod.testcontainers.extensions; + +import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertSame; + +import io.goodforgod.testcontainers.extensions.example.ConnectionRedis; +import io.goodforgod.testcontainers.extensions.example.RedisConnection; +import io.goodforgod.testcontainers.extensions.example.TestcontainersRedis; +import org.junit.jupiter.api.*; + +@TestcontainersRedis(mode = ContainerMode.PER_RUN) +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class ContainerPerRunNestedSecondTests { + + static volatile RedisConnection perRunConnection; + + @ConnectionRedis + private RedisConnection sameConnection; + + @TestMethodOrder(MethodOrderer.OrderAnnotation.class) + @Nested + public class NestedFirstTests { + + @Order(1) + @Test + void firstConnection(@ConnectionRedis RedisConnection connection) { + assertNotNull(connection); + assertNotNull(connection.params().uri()); + assertNotNull(sameConnection); + assertEquals(sameConnection, connection); + + if (perRunConnection == null) { + perRunConnection = connection; + } + + if (ContainerPerRunSecondTests.perRunConnection != null) { + assertEquals(perRunConnection, ContainerPerRunSecondTests.perRunConnection); + } + } + + @Order(2) + @Test + void secondConnection(@ConnectionRedis RedisConnection connection) { + assertNotNull(connection); + assertNotNull(connection.params().uri()); + assertNotNull(sameConnection); + assertEquals(sameConnection, connection); + assertEquals(perRunConnection, connection); + assertSame(sameConnection, connection); + assertSame(perRunConnection, connection); + } + } + + @TestMethodOrder(MethodOrderer.OrderAnnotation.class) + @Nested + public class NestedSecondTests { + + @Order(1) + @Test + void firstConnection(@ConnectionRedis RedisConnection connection) { + assertNotNull(connection); + assertNotNull(connection.params().uri()); + assertNotNull(sameConnection); + assertEquals(sameConnection, connection); + assertSame(sameConnection, connection); + + if (perRunConnection == null) { + perRunConnection = connection; + } + + if (ContainerPerRunSecondTests.perRunConnection != null) { + assertEquals(perRunConnection, ContainerPerRunSecondTests.perRunConnection); + } + } + + @Order(2) + @Test + void secondConnection(@ConnectionRedis RedisConnection connection) { + assertNotNull(connection); + assertNotNull(connection.params().uri()); + assertNotNull(sameConnection); + assertEquals(sameConnection, connection); + assertEquals(perRunConnection, connection); + assertSame(sameConnection, connection); + assertSame(perRunConnection, connection); + } + } +} diff --git a/common/src/test/java/io/goodforgod/testcontainers/extensions/ContainerPerRunSecondTests.java b/common/src/test/java/io/goodforgod/testcontainers/extensions/ContainerPerRunSecondTests.java index 5ebcac5..1ed7e37 100644 --- a/common/src/test/java/io/goodforgod/testcontainers/extensions/ContainerPerRunSecondTests.java +++ b/common/src/test/java/io/goodforgod/testcontainers/extensions/ContainerPerRunSecondTests.java @@ -3,7 +3,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; -import io.goodforgod.testcontainers.extensions.example.ContainerRedisConnection; +import io.goodforgod.testcontainers.extensions.example.ConnectionRedis; import io.goodforgod.testcontainers.extensions.example.RedisConnection; import io.goodforgod.testcontainers.extensions.example.TestcontainersRedis; import org.junit.jupiter.api.MethodOrderer; @@ -17,12 +17,12 @@ class ContainerPerRunSecondTests { static volatile RedisConnection perRunConnection; - @ContainerRedisConnection + @ConnectionRedis private RedisConnection sameConnection; @Order(1) @Test - void firstConnection(@ContainerRedisConnection RedisConnection connection) { + void firstConnection(@ConnectionRedis RedisConnection connection) { assertNotNull(connection); assertNotNull(connection.params().uri()); assertNotNull(sameConnection); @@ -39,7 +39,7 @@ void firstConnection(@ContainerRedisConnection RedisConnection connection) { @Order(2) @Test - void secondConnection(@ContainerRedisConnection RedisConnection connection) { + void secondConnection(@ConnectionRedis RedisConnection connection) { assertNotNull(connection); assertNotNull(connection.params().uri()); assertNotNull(sameConnection); diff --git a/common/src/test/java/io/goodforgod/testcontainers/extensions/example/ContainerRedisConnection.java b/common/src/test/java/io/goodforgod/testcontainers/extensions/example/ConnectionRedis.java similarity index 87% rename from common/src/test/java/io/goodforgod/testcontainers/extensions/example/ContainerRedisConnection.java rename to common/src/test/java/io/goodforgod/testcontainers/extensions/example/ConnectionRedis.java index 8061d8a..4685008 100644 --- a/common/src/test/java/io/goodforgod/testcontainers/extensions/example/ContainerRedisConnection.java +++ b/common/src/test/java/io/goodforgod/testcontainers/extensions/example/ConnectionRedis.java @@ -10,4 +10,4 @@ @Documented @Target({ ElementType.FIELD, ElementType.PARAMETER }) @Retention(RetentionPolicy.RUNTIME) -public @interface ContainerRedisConnection {} +public @interface ConnectionRedis {} diff --git a/common/src/test/java/io/goodforgod/testcontainers/extensions/example/RedisContext.java b/common/src/test/java/io/goodforgod/testcontainers/extensions/example/RedisContext.java new file mode 100644 index 0000000..a47a31a --- /dev/null +++ b/common/src/test/java/io/goodforgod/testcontainers/extensions/example/RedisContext.java @@ -0,0 +1,64 @@ +package io.goodforgod.testcontainers.extensions.example; + +import io.goodforgod.testcontainers.extensions.ContainerContext; +import java.util.Optional; +import org.jetbrains.annotations.NotNull; + +final class RedisContext implements ContainerContext { + + private volatile RedisConnectionImpl connection; + + private final RedisContainer container; + + RedisContext(RedisContainer container) { + this.container = container; + } + + @NotNull + public RedisConnection connection() { + if (connection == null) { + final Optional connectionExternal = getConnectionExternal(); + if (connectionExternal.isEmpty() && !container.isRunning()) { + throw new IllegalStateException("RedisConnection can't be create for container that is not running"); + } + + final RedisConnection containerConnection = connectionExternal.orElseGet(() -> { + final String alias = container.getNetworkAliases().get(container.getNetworkAliases().size() - 1); + return RedisConnectionImpl.forContainer(container.getHost(), + container.getMappedPort(RedisContainer.PORT), + alias, + RedisContainer.PORT, + container.getDatabase(), + container.getUser(), + container.getPassword()); + }); + + this.connection = (RedisConnectionImpl) containerConnection; + } + + return connection; + } + + @Override + public void start() { + final Optional connectionExternal = getConnectionExternal(); + if (connectionExternal.isEmpty()) { + container.start(); + } + } + + @Override + public void stop() { + container.stop(); + } + + @NotNull + private static Optional getConnectionExternal() { + return Optional.empty(); + } + + @Override + public String toString() { + return container.getDockerImageName(); + } +} diff --git a/common/src/test/java/io/goodforgod/testcontainers/extensions/example/RedisMetadata.java b/common/src/test/java/io/goodforgod/testcontainers/extensions/example/RedisMetadata.java index b95c99d..b5f5904 100644 --- a/common/src/test/java/io/goodforgod/testcontainers/extensions/example/RedisMetadata.java +++ b/common/src/test/java/io/goodforgod/testcontainers/extensions/example/RedisMetadata.java @@ -2,16 +2,10 @@ import io.goodforgod.testcontainers.extensions.AbstractContainerMetadata; import io.goodforgod.testcontainers.extensions.ContainerMode; -import org.jetbrains.annotations.NotNull; final class RedisMetadata extends AbstractContainerMetadata { RedisMetadata(boolean network, String alias, String image, ContainerMode runMode) { super(network, alias, image, runMode); } - - @Override - public @NotNull String networkAliasDefault() { - return "redis-" + System.currentTimeMillis(); - } } diff --git a/common/src/test/java/io/goodforgod/testcontainers/extensions/example/TestcontainersRedisExtension.java b/common/src/test/java/io/goodforgod/testcontainers/extensions/example/TestcontainersRedisExtension.java index a30cbfa..66fd0a9 100644 --- a/common/src/test/java/io/goodforgod/testcontainers/extensions/example/TestcontainersRedisExtension.java +++ b/common/src/test/java/io/goodforgod/testcontainers/extensions/example/TestcontainersRedisExtension.java @@ -1,6 +1,7 @@ package io.goodforgod.testcontainers.extensions.example; import io.goodforgod.testcontainers.extensions.AbstractTestcontainersExtension; +import io.goodforgod.testcontainers.extensions.ContainerContext; import java.lang.annotation.Annotation; import java.time.Duration; import java.util.Optional; @@ -26,7 +27,7 @@ protected Class getContainerAnnotation() { } protected Class getConnectionAnnotation() { - return ContainerRedisConnection.class; + return ConnectionRedis.class; } @Override @@ -35,15 +36,20 @@ protected Class getConnectionType() { } @Override - protected RedisContainer getContainerDefault(RedisMetadata metadata) { + protected ExtensionContext.Namespace getNamespace() { + return NAMESPACE; + } + + @Override + protected RedisContainer createContainerDefault(RedisMetadata metadata) { var dockerImage = DockerImageName.parse(metadata.image()) .asCompatibleSubstituteFor(DockerImageName.parse("redis")); + String alias = Optional.ofNullable(metadata.networkAlias()).orElseGet(() -> "redis-" + System.currentTimeMillis()); var container = new RedisContainer(dockerImage) .withLogConsumer(new Slf4jLogConsumer(LoggerFactory.getLogger(RedisContainer.class)) - .withMdc("image", metadata.image()) - .withMdc("alias", metadata.networkAliasOrDefault())) - .withNetworkAliases(metadata.networkAliasOrDefault()) + .withMdc("image", metadata.image())) + .withNetworkAliases(alias) .waitingFor(Wait.forListeningPort()) .withStartupTimeout(Duration.ofMinutes(5)); @@ -55,8 +61,8 @@ protected RedisContainer getContainerDefault(RedisMetadata metadata) { } @Override - protected ExtensionContext.Namespace getNamespace() { - return NAMESPACE; + protected ContainerContext createContainerContext(RedisContainer container) { + return new RedisContext(container); } @NotNull @@ -65,27 +71,6 @@ protected Optional findMetadata(@NotNull ExtensionContext context .map(a -> new RedisMetadata(a.network().shared(), a.network().alias(), a.image(), a.mode())); } - @NotNull - protected RedisConnection getConnectionForContainer(@NotNull RedisMetadata metadata, @NotNull RedisContainer container) { - final String alias = Optional.ofNullable(metadata.networkAliasOrDefault()) - .filter(a -> !a.isBlank()) - .or(() -> container.getNetworkAliases().stream() - .filter(a -> a.startsWith("redis")) - .findFirst() - .or(() -> (container.getNetworkAliases().isEmpty()) - ? Optional.empty() - : Optional.of(container.getNetworkAliases().get(container.getNetworkAliases().size() - 1)))) - .orElse(null); - - return RedisConnectionImpl.forContainer(container.getHost(), - container.getMappedPort(RedisContainer.PORT), - alias, - RedisContainer.PORT, - container.getDatabase(), - container.getUser(), - container.getPassword()); - } - @NotNull protected Optional getConnectionExternal() { return Optional.empty(); diff --git a/common/src/test/resources/logback.xml b/common/src/test/resources/logback.xml new file mode 100644 index 0000000..572962d --- /dev/null +++ b/common/src/test/resources/logback.xml @@ -0,0 +1,20 @@ + + + + UTF-8 + %cyan(%d{HH:mm:ss.SSS}) [%cn] %highlight(%-5level) %logger{36} - %msg%n + + + + + + + + + + + + + + + diff --git a/dependencies.gradle b/dependencies.gradle index 30573d7..e66df4c 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -4,11 +4,11 @@ dependencyResolutionManagement { DependencyResolutionManagement it -> it.versionCatalogs { libs { version("slf4j", "2.0.12") - version("junit-api", "5.9.3") - version("junit-platform", "1.9.3") + version("junit-api", "5.10.2") + version("junit-platform", "1.10.2") version("testcontainers", "1.17.6") version("flyway", "9.22.3") - version("liquibase", "4.26.0") + version("liquibase", "4.27.0") library("slf4j-api", "org.slf4j", "slf4j-api").versionRef("slf4j") library("slf4j-jul", "org.slf4j", "jul-to-slf4j").versionRef("slf4j") @@ -18,6 +18,9 @@ dependencyResolutionManagement { DependencyResolutionManagement it -> library("junit-engine", "org.junit.jupiter", "junit-jupiter-engine").versionRef("junit-api") library("junit-launcher", "org.junit.platform", "junit-platform-launcher").versionRef("junit-platform") + library("json", "org.json", "json").version("20240303") + library("hikari", "com.zaxxer", "HikariCP").version("5.1.0") + library("testcontainers-core", "org.testcontainers", "testcontainers").versionRef("testcontainers") library("testcontainers-junit", "org.testcontainers", "junit-jupiter").versionRef("testcontainers") library("testcontainers-jdbc", "org.testcontainers", "jdbc").versionRef("testcontainers") @@ -44,8 +47,6 @@ dependencyResolutionManagement { DependencyResolutionManagement it -> library("driver-kafka", "org.apache.kafka", "kafka-clients").version("3.5.1") library("driver-redis", "redis.clients", "jedis").version("4.4.3") library("driver-mockserver", "org.mock-server", "mockserver-client-java").version("5.15.0") - - library("json", "org.json", "json").version("20230618") } } } \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 6e49aeb..fb11ba7 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,6 @@ groupId=io.goodforgod artifactRootId=testcontainers-extensions -artifactVersion=0.9.6-SNAPSHOT +artifactVersion=0.10.0-SNAPSHOT ##### GRADLE ##### diff --git a/jdbc/build.gradle b/jdbc/build.gradle index 1853211..e59dce2 100644 --- a/jdbc/build.gradle +++ b/jdbc/build.gradle @@ -9,7 +9,7 @@ dependencies { implementation libs.junit.launcher implementation libs.junit.api - implementation "com.zaxxer:HikariCP:5.1.0" + implementation libs.hikari testImplementation libs.driver.postgres testImplementation libs.testcontainers.postgres diff --git a/jdbc/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/AbstractTestcontainersJdbcExtension.java b/jdbc/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/AbstractTestcontainersJdbcExtension.java index 081d16b..73cba95 100644 --- a/jdbc/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/AbstractTestcontainersJdbcExtension.java +++ b/jdbc/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/AbstractTestcontainersJdbcExtension.java @@ -1,6 +1,7 @@ package io.goodforgod.testcontainers.extensions.jdbc; import io.goodforgod.testcontainers.extensions.AbstractTestcontainersExtension; +import io.goodforgod.testcontainers.extensions.ContainerContext; import io.goodforgod.testcontainers.extensions.ContainerMode; import java.util.Arrays; import org.jetbrains.annotations.ApiStatus.Internal; @@ -19,16 +20,24 @@ protected Class getConnectionType() { } private void tryMigrateIfRequired(JdbcMetadata metadata, ExtensionContext context) { - JdbcMigrationEngine migrationEngine = getMigrationEngine(metadata.migration().engine(), context); - migrationEngine.migrate(Arrays.asList(metadata.migration().migrations())); + JdbcMigrationEngine migrationEngine = getContainerContext(context).connection() + .migrationEngine(metadata.migration().engine()); + migrationEngine.apply(Arrays.asList(metadata.migration().locations())); } private void tryDropIfRequired(JdbcMetadata metadata, ExtensionContext context) { - JdbcMigrationEngine migrationEngine = getMigrationEngine(metadata.migration().engine(), context); - migrationEngine.drop(Arrays.asList(metadata.migration().migrations())); + JdbcMigrationEngine migrationEngine = getContainerContext(context).connection() + .migrationEngine(metadata.migration().engine()); + migrationEngine.drop(Arrays.asList(metadata.migration().locations())); } - protected abstract JdbcMigrationEngine getMigrationEngine(Migration.Engines engine, ExtensionContext context); + protected abstract ContainerContext createContainerContext(Container container); + + @Override + protected ContainerContext getContainerContext(ExtensionContext context) { + Metadata metadata = getMetadata(context); + return getStorage(context).get(metadata.runMode(), ContainerContext.class); + } @Override public void beforeAll(ExtensionContext context) { diff --git a/jdbc/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/FlywayJdbcMigrationEngine.java b/jdbc/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/FlywayJdbcMigrationEngine.java index f0a297e..a04e976 100644 --- a/jdbc/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/FlywayJdbcMigrationEngine.java +++ b/jdbc/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/FlywayJdbcMigrationEngine.java @@ -1,7 +1,5 @@ package io.goodforgod.testcontainers.extensions.jdbc; -import com.zaxxer.hikari.HikariConfig; -import com.zaxxer.hikari.HikariDataSource; import java.nio.charset.StandardCharsets; import java.util.List; import javax.sql.DataSource; @@ -14,11 +12,9 @@ public final class FlywayJdbcMigrationEngine implements JdbcMigrationEngine, Aut private static final Logger logger = LoggerFactory.getLogger(FlywayJdbcMigrationEngine.class); - private final JdbcConnection jdbcConnection; + private final JdbcConnectionImpl jdbcConnection; - private volatile HikariDataSource dataSource; - - public FlywayJdbcMigrationEngine(JdbcConnection jdbcConnection) { + public FlywayJdbcMigrationEngine(JdbcConnectionImpl jdbcConnection) { this.jdbcConnection = jdbcConnection; } @@ -39,8 +35,8 @@ private static Flyway getFlyway(DataSource dataSource, List locations) { } @Override - public void migrate(@NotNull List locations) { - logger.debug("Starting schema migration for engine '{}' for connection: {}", + public void apply(@NotNull List locations) { + logger.debug("Starting migration migration for engine '{}' for connection: {}", getClass().getSimpleName(), jdbcConnection); try { @@ -50,48 +46,34 @@ public void migrate(@NotNull List locations) { Thread.sleep(250); getFlyway(getDataSource(), locations).migrate(); } catch (InterruptedException ex) { - logger.error("Failed schema migration for engine '{}' for connection: {}", + logger.error("Failed migration migration for engine '{}' for connection: {}", getClass().getSimpleName(), jdbcConnection); throw new IllegalStateException(ex); } } - logger.info("Finished schema migration for engine '{}' for connection: {}", + logger.info("Finished migration migration for engine '{}' for connection: {}", getClass().getSimpleName(), jdbcConnection); } @Override public void drop(@NotNull List locations) { - logger.debug("Starting schema dropping for engine '{}' for connection: {}", + logger.debug("Starting migration dropping for engine '{}' for connection: {}", getClass().getSimpleName(), jdbcConnection); getFlyway(getDataSource(), locations).clean(); - logger.info("Finished schema dropping for engine '{}' for connection: {}", + logger.info("Finished migration dropping for engine '{}' for connection: {}", getClass().getSimpleName(), jdbcConnection); } - private HikariDataSource getDataSource() { - if (this.dataSource == null) { - HikariConfig hikariConfig = new HikariConfig(); - hikariConfig.setJdbcUrl(jdbcConnection.params().jdbcUrl()); - hikariConfig.setUsername(jdbcConnection.params().username()); - hikariConfig.setPassword(jdbcConnection.params().password()); - hikariConfig.setAutoCommit(true); - hikariConfig.setMinimumIdle(2); - hikariConfig.setMaximumPoolSize(10); - hikariConfig.setPoolName("flyway"); - this.dataSource = new HikariDataSource(hikariConfig); - } - return this.dataSource; + private DataSource getDataSource() { + return this.jdbcConnection.dataSource(); } @Override public void close() { - if (dataSource != null) { - dataSource.close(); - dataSource = null; - } + // do nothing } } diff --git a/jdbc/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/JdbcConnection.java b/jdbc/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/JdbcConnection.java index a247bc4..8bc2b74 100644 --- a/jdbc/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/JdbcConnection.java +++ b/jdbc/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/JdbcConnection.java @@ -12,7 +12,7 @@ /** * Describes active JDBC connection of currently running {@link JdbcDatabaseContainer} */ -public interface JdbcConnection { +public interface JdbcConnection extends AutoCloseable { @FunctionalInterface interface ResultSetMapper { @@ -64,7 +64,10 @@ interface Params { * @return new JDBC connection */ @NotNull - Connection open(); + Connection openConnection(); + + @NotNull + JdbcMigrationEngine migrationEngine(@NotNull Migration.Engines engine); /** * @param sql to execute @@ -111,7 +114,7 @@ List queryMany(@NotNull @Language("SQL") String sql, /** * Asserts that SELECT COUNT(*) in specified table counts 0 rows - * + * * @param tableName to SELECT COUNT(*) in */ void assertCountsNone(@NotNull String tableName); @@ -119,7 +122,7 @@ List queryMany(@NotNull @Language("SQL") String sql, /** * Asserts that SELECT COUNT(*) in specified table counts at least minimal number expectedAtLeast * rows - * + * * @param tableName to SELECT COUNT(*) in * @param expectedAtLeast at least minimal number of rows expected */ @@ -127,7 +130,7 @@ List queryMany(@NotNull @Language("SQL") String sql, /** * Asserts that SELECT COUNT(*) in specified table counts exact number expected rows - * + * * @param tableName to SELECT COUNT(*) in * @param expected exact number of rows expected */ @@ -135,14 +138,14 @@ List queryMany(@NotNull @Language("SQL") String sql, /** * Asserts that executed SQL results in 0 rows - * + * * @param sql to execute */ void assertQueriesNone(@NotNull @Language("SQL") String sql); /** * Asserts that executed SQL results in at least minimal number of expectedAtLeast rows - * + * * @param sql to execute * @param expectedAtLeast at least minimal number of rows expected */ @@ -150,7 +153,7 @@ List queryMany(@NotNull @Language("SQL") String sql, /** * Asserts that executed SQL results in exact number of expected rows - * + * * @param sql to execute * @param expected exact number of rows expected */ @@ -158,21 +161,21 @@ List queryMany(@NotNull @Language("SQL") String sql, /** * Asserts that executed SQL results in any inserted entities - * + * * @param sql to execute */ void assertInserted(@NotNull @Language("SQL") String sql); /** * Asserts that executed SQL results in any updated entities - * + * * @param sql to execute */ void assertUpdated(@NotNull @Language("SQL") String sql); /** * Asserts that executed SQL results in any deleted entities - * + * * @param sql to execute */ void assertDeleted(@NotNull @Language("SQL") String sql); @@ -214,4 +217,40 @@ List queryMany(@NotNull @Language("SQL") String sql, * @return true if executed SQL results in any deleted entities */ boolean checkDeleted(@Language("SQL") String sql); + + @Override + void close(); + + static JdbcConnection forParams(String driverProtocol, + String host, + int port, + String database, + String username, + String password) { + var jdbcUrl = String.format("jdbc:%s://%s:%d/%s", driverProtocol, host, port, database); + var params = new JdbcConnectionImpl.ParamsImpl(jdbcUrl, host, port, database, username, password); + return new JdbcConnectionClosableImpl(params, null); + } + + static JdbcConnection forContainer(JdbcDatabaseContainer container) { + if (!container.isRunning()) { + throw new IllegalStateException(container.getClass().getSimpleName() + " container is not running"); + } + + String jdbcUrl = container.getJdbcUrl(); + int from = jdbcUrl.indexOf("//"); + int to = jdbcUrl.indexOf("/", from + 2); + int port = Integer.parseInt(jdbcUrl.substring(from, to).split(":")[1]); + var params = new JdbcConnectionImpl.ParamsImpl(jdbcUrl, container.getHost(), port, container.getDatabaseName(), + container.getUsername(), container.getPassword()); + + String networkHost = container.getNetworkAliases().get(0); + Integer networkPort = container.getFirstMappedPort(); + var networkJdbcUrl = String.format("%s//%s:%d/%s", jdbcUrl.substring(0, from), networkHost, networkPort, + container.getDatabaseName()); + var network = new JdbcConnectionImpl.ParamsImpl(networkJdbcUrl, container.getHost(), port, container.getDatabaseName(), + container.getUsername(), container.getPassword()); + + return new JdbcConnectionClosableImpl(params, network); + } } diff --git a/jdbc/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/JdbcConnectionClosableImpl.java b/jdbc/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/JdbcConnectionClosableImpl.java new file mode 100644 index 0000000..768a47d --- /dev/null +++ b/jdbc/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/JdbcConnectionClosableImpl.java @@ -0,0 +1,16 @@ +package io.goodforgod.testcontainers.extensions.jdbc; + +import org.jetbrains.annotations.ApiStatus.Internal; + +@Internal +final class JdbcConnectionClosableImpl extends JdbcConnectionImpl { + + JdbcConnectionClosableImpl(Params params, Params network) { + super(params, network); + } + + @Override + public void close() { + super.stop(); + } +} diff --git a/jdbc/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/JdbcConnectionImpl.java b/jdbc/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/JdbcConnectionImpl.java index f6f70d1..8e65f93 100644 --- a/jdbc/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/JdbcConnectionImpl.java +++ b/jdbc/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/JdbcConnectionImpl.java @@ -1,15 +1,17 @@ package io.goodforgod.testcontainers.extensions.jdbc; +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; import java.net.URI; import java.nio.charset.StandardCharsets; import java.sql.Connection; -import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; import java.util.List; import java.util.Objects; import java.util.Optional; +import javax.sql.DataSource; import org.intellij.lang.annotations.Language; import org.jetbrains.annotations.ApiStatus.Internal; import org.jetbrains.annotations.NotNull; @@ -18,9 +20,9 @@ import org.slf4j.LoggerFactory; @Internal -final class JdbcConnectionImpl implements JdbcConnection { +class JdbcConnectionImpl implements JdbcConnection { - private static final class ParamsImpl implements JdbcConnection.Params { + static final class ParamsImpl implements JdbcConnection.Params { private final String jdbcUrl; private final String host; @@ -76,10 +78,15 @@ public String toString() { private static final Logger logger = LoggerFactory.getLogger(JdbcConnection.class); - private volatile boolean isClosed = false; private final Params params; private final Params network; + private volatile FlywayJdbcMigrationEngine flywayJdbcMigrationEngine; + private volatile LiquibaseJdbcMigrationEngine liquibaseJdbcMigrationEngine; + + private volatile boolean isClosed = false; + private volatile HikariDataSource dataSource; + JdbcConnectionImpl(Params params, Params network) { this.params = params; this.network = network; @@ -116,9 +123,7 @@ static JdbcConnection forJDBC(String jdbcUrl, return new JdbcConnectionImpl(params, network); } - static JdbcConnection forExternal(String jdbcUrl, - String username, - String password) { + static JdbcConnection forExternal(String jdbcUrl, String username, String password) { final URI uri = URI.create(jdbcUrl); var host = uri.getHost(); var port = uri.getPort(); @@ -132,6 +137,23 @@ static JdbcConnection forExternal(String jdbcUrl, return new JdbcConnectionImpl(params, null); } + @Override + public @NotNull JdbcMigrationEngine migrationEngine(Migration.@NotNull Engines engine) { + if (engine == Migration.Engines.FLYWAY) { + if (flywayJdbcMigrationEngine == null) { + this.flywayJdbcMigrationEngine = new FlywayJdbcMigrationEngine(this); + } + return this.flywayJdbcMigrationEngine; + } else if (engine == Migration.Engines.LIQUIBASE) { + if (liquibaseJdbcMigrationEngine == null) { + this.liquibaseJdbcMigrationEngine = new LiquibaseJdbcMigrationEngine(this); + } + return this.liquibaseJdbcMigrationEngine; + } else { + throw new UnsupportedOperationException("Unsupported engine: " + engine); + } + } + @Override public @NotNull Params params() { return params; @@ -144,24 +166,22 @@ static JdbcConnection forExternal(String jdbcUrl, @NotNull @Override - public Connection open() { + public Connection openConnection() { if (isClosed) { throw new IllegalStateException("JdbcConnection was closed"); } try { - logger.debug("Opening SQL connection..."); - return DriverManager.getConnection(params.jdbcUrl(), params.username(), params.username()); + return dataSource().getConnection(); } catch (SQLException e) { - throw new JdbcConnectionException(e); + throw new IllegalStateException(e); } } @Override public void execute(@Language("SQL") @NotNull String sql) { logger.debug("Executing SQL:\n{}", sql); - try (var connection = open(); - var stmt = connection.createStatement()) { + try (var openedConnection = openConnection(); var stmt = openedConnection.createStatement()) { stmt.execute(sql); } catch (SQLException e) { throw new JdbcConnectionException(e); @@ -223,8 +243,8 @@ public Optional queryOne(@Language("SQL") @NotNull S @NotNull ResultSetMapper extractor) throws E { logger.debug("Executing SQL:\n{}", sql); - try (var connection = open(); - var stmt = connection.prepareStatement(sql); + try (var openedConnection = openConnection(); + var stmt = openedConnection.prepareStatement(sql); var rs = stmt.executeQuery()) { return (rs.next()) ? Optional.ofNullable(extractor.apply(rs)) @@ -239,8 +259,8 @@ public List queryMany(@Language("SQL") @NotNull Stri @NotNull ResultSetMapper extractor) throws E { logger.debug("Executing SQL:\n{}", sql); - try (var connection = open(); - var stmt = connection.prepareStatement(sql); + try (var openedConnection = openConnection(); + var stmt = openedConnection.prepareStatement(sql); var rs = stmt.executeQuery()) { final List result = new ArrayList<>(); while (rs.next()) { @@ -260,8 +280,8 @@ interface QueryAssert { private void assertQuery(@Language("SQL") String sql, QueryAssert consumer) { logger.debug("Executing SQL:\n{}", sql); - try (var connection = open(); - var stmt = connection.prepareStatement(sql); + try (var openedConnection = openConnection(); + var stmt = openedConnection.prepareStatement(sql); var rs = stmt.executeQuery()) { consumer.accept(rs); } catch (SQLException e) { @@ -305,8 +325,7 @@ public void assertQueriesEquals(int expected, @NotNull String sql) { @Override public void assertInserted(@NotNull String sql) { logger.debug("Executing SQL:\n{}", sql); - try (var connection = open(); - var stmt = connection.prepareStatement(sql)) { + try (var openedConnection = openConnection(); var stmt = openedConnection.prepareStatement(sql)) { var rs = stmt.executeUpdate(); if (rs == 0) { Assertions.fail(String.format("Expected query to update but it didn't for SQL: %s", sql.replace("\n", " "))); @@ -334,8 +353,8 @@ interface QueryChecker { private boolean checkQuery(@Language("SQL") String sql, QueryChecker checker) { logger.debug("Executing SQL:\n{}", sql); - try (var connection = open(); - var stmt = connection.prepareStatement(sql); + try (var openedConnection = openConnection(); + var stmt = openedConnection.prepareStatement(sql); var rs = stmt.executeQuery()) { return checker.apply(rs); } catch (Exception e) { @@ -376,8 +395,7 @@ public boolean checkQueriesEquals(int expected, @NotNull String sql) { @Override public boolean checkInserted(@NotNull String sql) { logger.debug("Executing SQL: {}", sql); - try (var connection = open(); - var stmt = connection.prepareStatement(sql)) { + try (var openedConnection = openConnection(); var stmt = openedConnection.prepareStatement(sql)) { var rs = stmt.executeUpdate(); return rs != 0; } catch (SQLException e) { @@ -395,8 +413,49 @@ public boolean checkDeleted(String sql) { return checkInserted(sql); } - void close() { + DataSource dataSource() { + if (dataSource == null) { + HikariConfig hikariConfig = new HikariConfig(); + hikariConfig.setJdbcUrl(params().jdbcUrl()); + hikariConfig.setUsername(params().username()); + hikariConfig.setPassword(params().password()); + hikariConfig.setAutoCommit(true); + hikariConfig.setMinimumIdle(1); + hikariConfig.setMaximumPoolSize(25); + hikariConfig.setPoolName("jdbc-connection"); + hikariConfig.setLeakDetectionThreshold(10000); + this.dataSource = new HikariDataSource(hikariConfig); + } + + return this.dataSource; + } + + void stop() { this.isClosed = true; + + if (dataSource != null) { + try { + dataSource.close(); + } catch (Exception e) { + // do nothing + } finally { + dataSource = null; + } + } + + if (flywayJdbcMigrationEngine != null) { + flywayJdbcMigrationEngine.close(); + flywayJdbcMigrationEngine = null; + } + if (liquibaseJdbcMigrationEngine != null) { + liquibaseJdbcMigrationEngine.close(); + liquibaseJdbcMigrationEngine = null; + } + } + + @Override + public void close() { + // do nothing } @Override diff --git a/jdbc/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/JdbcMetadata.java b/jdbc/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/JdbcMetadata.java index c892ea6..5e62f23 100644 --- a/jdbc/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/JdbcMetadata.java +++ b/jdbc/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/JdbcMetadata.java @@ -5,7 +5,7 @@ import org.jetbrains.annotations.ApiStatus.Internal; @Internal -abstract class JdbcMetadata extends AbstractContainerMetadata { +class JdbcMetadata extends AbstractContainerMetadata { private final Migration migration; diff --git a/jdbc/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/JdbcMigrationEngine.java b/jdbc/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/JdbcMigrationEngine.java index c6ebcda..277f125 100644 --- a/jdbc/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/JdbcMigrationEngine.java +++ b/jdbc/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/JdbcMigrationEngine.java @@ -5,7 +5,15 @@ public interface JdbcMigrationEngine { - void migrate(@NotNull List locations); + default void apply(@NotNull String location) { + apply(List.of(location)); + } + + void apply(@NotNull List locations); + + default void drop(@NotNull String location) { + drop(List.of(location)); + } void drop(@NotNull List locations); } diff --git a/jdbc/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/LiquibaseJdbcMigrationEngine.java b/jdbc/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/LiquibaseJdbcMigrationEngine.java index 26f86e2..9890bef 100644 --- a/jdbc/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/LiquibaseJdbcMigrationEngine.java +++ b/jdbc/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/LiquibaseJdbcMigrationEngine.java @@ -33,8 +33,6 @@ private interface LiquibaseRunner { private final JdbcConnection jdbcConnection; - private volatile Database database; - public LiquibaseJdbcMigrationEngine(JdbcConnection jdbcConnection) { this.jdbcConnection = jdbcConnection; } @@ -91,47 +89,67 @@ private static void dropLiquibase(Database database, List locations) { } @Override - public void migrate(@NotNull List locations) { - logger.debug("Starting schema migration for engine '{}' for connection: {}", + public void apply(@NotNull List locations) { + logger.debug("Starting migration apply for engine '{}' for connection: {}", getClass().getSimpleName(), jdbcConnection); try { - migrateLiquibase(getDatabase(), locations); + Connection connection = jdbcConnection.openConnection(); + try (var database = getLiquiDatabase(connection)) { + migrateLiquibase(database, locations); + } } catch (Exception e) { try { Thread.sleep(250); - migrateLiquibase(getDatabase(), locations); - } catch (InterruptedException ex) { - logger.error("Failed schema migration for engine '{}' for connection: {}", + Connection connection = jdbcConnection.openConnection(); + try (var database = getLiquiDatabase(connection)) { + migrateLiquibase(database, locations); + } + } catch (Exception ex) { + logger.error("Failed migration apply for engine '{}' for connection: {}", getClass().getSimpleName(), jdbcConnection); throw new IllegalStateException(ex); } } - logger.info("Finished schema migration for engine '{}' for connection: {}", + logger.info("Finished migration apply for engine '{}' for connection: {}", getClass().getSimpleName(), jdbcConnection); } @Override public void drop(@NotNull List locations) { - logger.debug("Starting schema dropping for engine '{}' for connection: {}", + logger.debug("Starting migration dropping for engine '{}' for connection: {}", getClass().getSimpleName(), jdbcConnection); - dropLiquibase(getDatabase(), locations); + try { + Connection connection = jdbcConnection.openConnection(); + try (var database = getLiquiDatabase(connection)) { + dropLiquibase(database, locations); + } + } catch (Exception e) { + try { + Thread.sleep(250); + Connection connection = jdbcConnection.openConnection(); + try (var database = getLiquiDatabase(connection)) { + dropLiquibase(database, locations); + } + } catch (Exception ex) { + logger.error("Failed migration drop for engine '{}' for connection: {}", + getClass().getSimpleName(), jdbcConnection); + + throw new IllegalStateException(ex); + } + } - logger.info("Finished schema dropping for engine '{}' for connection: {}", + logger.info("Finished migration dropping for engine '{}' for connection: {}", getClass().getSimpleName(), jdbcConnection); } - private Database getDatabase() { + private Database getLiquiDatabase(Connection connection) { try { - if (this.database == null) { - Connection connection = jdbcConnection.open(); - var liquibaseConnection = new liquibase.database.jvm.JdbcConnection(connection); - this.database = DatabaseFactory.getInstance().findCorrectDatabaseImplementation(liquibaseConnection); - } - return this.database; + var liquiConnection = new liquibase.database.jvm.JdbcConnection(connection); + return DatabaseFactory.getInstance().findCorrectDatabaseImplementation(liquiConnection); } catch (DatabaseException e) { throw new IllegalStateException(e); } @@ -139,13 +157,6 @@ private Database getDatabase() { @Override public void close() { - if (database != null) { - try { - database.close(); - database = null; - } catch (Exception e) { - throw new IllegalStateException(e); - } - } + // do nothing } } diff --git a/jdbc/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/Migration.java b/jdbc/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/Migration.java index defb21c..72e38b6 100644 --- a/jdbc/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/Migration.java +++ b/jdbc/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/Migration.java @@ -32,7 +32,7 @@ * @return will be by default "classpath:db/migration" for Flyway and "db/changelog.sql" * for Liquibase */ - String[] migrations() default {}; + String[] locations() default {}; /** * Database migration engine implementation diff --git a/jdbc/src/test/java/io/goodforgod/testcontainers/extensions/jdbc/ExampleTestcontainersJdbcExtension.java b/jdbc/src/test/java/io/goodforgod/testcontainers/extensions/jdbc/ExampleTestcontainersJdbcExtension.java deleted file mode 100644 index 8f7a9b6..0000000 --- a/jdbc/src/test/java/io/goodforgod/testcontainers/extensions/jdbc/ExampleTestcontainersJdbcExtension.java +++ /dev/null @@ -1,109 +0,0 @@ -package io.goodforgod.testcontainers.extensions.jdbc; - -import io.goodforgod.testcontainers.extensions.jdbc.example.ContainerJdbc; -import io.goodforgod.testcontainers.extensions.jdbc.example.ContainerJdbcConnection; -import io.goodforgod.testcontainers.extensions.jdbc.example.TestcontainersJdbc; -import java.lang.annotation.Annotation; -import java.util.Optional; -import org.jetbrains.annotations.NotNull; -import org.junit.jupiter.api.extension.ExtensionContext; -import org.slf4j.LoggerFactory; -import org.testcontainers.containers.PostgreSQLContainer; -import org.testcontainers.containers.output.Slf4jLogConsumer; -import org.testcontainers.utility.DockerImageName; - -public final class ExampleTestcontainersJdbcExtension extends - AbstractTestcontainersJdbcExtension, PostgresJdbcMetadata> { - - private static final ExtensionContext.Namespace NAMESPACE = ExtensionContext.Namespace - .create(ExampleTestcontainersJdbcExtension.class); - - private volatile FlywayJdbcMigrationEngine flywayJdbcMigrationEngine; - private volatile LiquibaseJdbcMigrationEngine liquibaseJdbcMigrationEngine; - - @Override - protected ExtensionContext.Namespace getNamespace() { - return NAMESPACE; - } - - @SuppressWarnings("unchecked") - @Override - protected Class> getContainerType() { - return (Class>) ((Class) PostgreSQLContainer.class); - } - - @Override - protected Class getContainerAnnotation() { - return ContainerJdbc.class; - } - - @Override - protected Class getConnectionAnnotation() { - return ContainerJdbcConnection.class; - } - - @Override - protected PostgreSQLContainer getContainerDefault(PostgresJdbcMetadata metadata) { - var dockerImage = DockerImageName.parse(metadata.image()) - .asCompatibleSubstituteFor(DockerImageName.parse(PostgreSQLContainer.IMAGE)); - - return new PostgreSQLContainer<>(dockerImage) - .withDatabaseName("postgres") - .withUsername("postgres") - .withPassword("postgres") - .withLogConsumer(new Slf4jLogConsumer(LoggerFactory.getLogger(PostgreSQLContainer.class)) - .withMdc("image", metadata.image()) - .withMdc("alias", metadata.networkAliasOrDefault())) - .withNetworkAliases(metadata.networkAliasOrDefault()); - } - - @Override - protected JdbcMigrationEngine getMigrationEngine(Migration.Engines engine, ExtensionContext context) { - if (engine == Migration.Engines.FLYWAY) { - if (this.flywayJdbcMigrationEngine == null) { - var connection = getConnectionCurrent(context); - this.flywayJdbcMigrationEngine = new FlywayJdbcMigrationEngine(connection); - } - return this.flywayJdbcMigrationEngine; - } else if (engine == Migration.Engines.LIQUIBASE) { - if (this.liquibaseJdbcMigrationEngine == null) { - var connection = getConnectionCurrent(context); - this.liquibaseJdbcMigrationEngine = new LiquibaseJdbcMigrationEngine(connection); - } - return this.liquibaseJdbcMigrationEngine; - } else { - throw new UnsupportedOperationException(); - } - } - - @Override - public void afterAll(ExtensionContext context) { - super.afterAll(context); - if (this.flywayJdbcMigrationEngine != null) { - this.flywayJdbcMigrationEngine.close(); - this.flywayJdbcMigrationEngine = null; - } - if (this.liquibaseJdbcMigrationEngine != null) { - this.liquibaseJdbcMigrationEngine.close(); - this.liquibaseJdbcMigrationEngine = null; - } - } - - @NotNull - protected Optional findMetadata(@NotNull ExtensionContext context) { - return findAnnotation(TestcontainersJdbc.class, context) - .map(a -> new PostgresJdbcMetadata(false, null, a.image(), a.mode(), a.migration())); - } - - @NotNull - protected JdbcConnection getConnectionForContainer(PostgresJdbcMetadata metadata, @NotNull PostgreSQLContainer container) { - return JdbcConnectionImpl.forJDBC(container.getJdbcUrl(), - container.getHost(), - container.getMappedPort(PostgreSQLContainer.POSTGRESQL_PORT), - container.getNetworkAliases().get(container.getNetworkAliases().size() - 1), - PostgreSQLContainer.POSTGRESQL_PORT, - container.getDatabaseName(), - container.getUsername(), - container.getPassword()); - } -} diff --git a/jdbc/src/test/java/io/goodforgod/testcontainers/extensions/jdbc/PostgresJdbcContext.java b/jdbc/src/test/java/io/goodforgod/testcontainers/extensions/jdbc/PostgresJdbcContext.java new file mode 100644 index 0000000..f1c784a --- /dev/null +++ b/jdbc/src/test/java/io/goodforgod/testcontainers/extensions/jdbc/PostgresJdbcContext.java @@ -0,0 +1,70 @@ +package io.goodforgod.testcontainers.extensions.jdbc; + +import io.goodforgod.testcontainers.extensions.ContainerContext; +import java.util.Optional; +import org.jetbrains.annotations.NotNull; +import org.testcontainers.containers.PostgreSQLContainer; + +final class PostgresJdbcContext implements ContainerContext { + + private volatile JdbcConnectionImpl connection; + + private final PostgreSQLContainer container; + + PostgresJdbcContext(PostgreSQLContainer container) { + this.container = container; + } + + @NotNull + public JdbcConnection connection() { + if (connection == null) { + final Optional connectionExternal = getConnectionExternal(); + if (connectionExternal.isEmpty() && !container.isRunning()) { + throw new IllegalStateException("MysqlConnection can't be create for container that is not running"); + } + + final JdbcConnection containerConnection = connectionExternal.orElseGet(() -> { + final String alias = container.getNetworkAliases().get(container.getNetworkAliases().size() - 1); + return JdbcConnectionImpl.forJDBC(container.getJdbcUrl(), + container.getHost(), + container.getMappedPort(PostgreSQLContainer.POSTGRESQL_PORT), + alias, + PostgreSQLContainer.POSTGRESQL_PORT, + container.getDatabaseName(), + container.getUsername(), + container.getPassword()); + }); + + this.connection = (JdbcConnectionImpl) containerConnection; + } + + return connection; + } + + @Override + public void start() { + final Optional connectionExternal = getConnectionExternal(); + if (connectionExternal.isEmpty()) { + container.start(); + } + } + + @Override + public void stop() { + if (connection != null) { + connection.stop(); + connection = null; + } + container.stop(); + } + + @NotNull + private static Optional getConnectionExternal() { + return Optional.empty(); + } + + @Override + public String toString() { + return container.getDockerImageName(); + } +} diff --git a/jdbc/src/test/java/io/goodforgod/testcontainers/extensions/jdbc/PostgresJdbcMetadata.java b/jdbc/src/test/java/io/goodforgod/testcontainers/extensions/jdbc/PostgresJdbcMetadata.java index 07597c7..2e532c3 100644 --- a/jdbc/src/test/java/io/goodforgod/testcontainers/extensions/jdbc/PostgresJdbcMetadata.java +++ b/jdbc/src/test/java/io/goodforgod/testcontainers/extensions/jdbc/PostgresJdbcMetadata.java @@ -1,16 +1,10 @@ package io.goodforgod.testcontainers.extensions.jdbc; import io.goodforgod.testcontainers.extensions.ContainerMode; -import org.jetbrains.annotations.NotNull; -final class PostgresJdbcMetadata extends JdbcMetadata { +public final class PostgresJdbcMetadata extends JdbcMetadata { PostgresJdbcMetadata(boolean network, String alias, String image, ContainerMode runMode, Migration migration) { super(network, alias, image, runMode, migration); } - - @Override - public @NotNull String networkAliasDefault() { - return "postgres-" + System.currentTimeMillis(); - } } diff --git a/jdbc/src/test/java/io/goodforgod/testcontainers/extensions/jdbc/PostgresJdbcTestcontainersJdbcExtension.java b/jdbc/src/test/java/io/goodforgod/testcontainers/extensions/jdbc/PostgresJdbcTestcontainersJdbcExtension.java new file mode 100644 index 0000000..da75e44 --- /dev/null +++ b/jdbc/src/test/java/io/goodforgod/testcontainers/extensions/jdbc/PostgresJdbcTestcontainersJdbcExtension.java @@ -0,0 +1,68 @@ +package io.goodforgod.testcontainers.extensions.jdbc; + +import io.goodforgod.testcontainers.extensions.ContainerContext; +import io.goodforgod.testcontainers.extensions.jdbc.example.ContainerJdbc; +import io.goodforgod.testcontainers.extensions.jdbc.example.ContainerJdbcConnection; +import io.goodforgod.testcontainers.extensions.jdbc.example.TestcontainersJdbc; +import java.lang.annotation.Annotation; +import java.util.Optional; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.PostgreSQLContainer; +import org.testcontainers.containers.output.Slf4jLogConsumer; +import org.testcontainers.utility.DockerImageName; + +public final class PostgresJdbcTestcontainersJdbcExtension extends + AbstractTestcontainersJdbcExtension, PostgresJdbcMetadata> { + + private static final ExtensionContext.Namespace NAMESPACE = ExtensionContext.Namespace + .create(PostgresJdbcTestcontainersJdbcExtension.class); + + @Override + protected ExtensionContext.Namespace getNamespace() { + return NAMESPACE; + } + + @SuppressWarnings("unchecked") + @Override + protected Class> getContainerType() { + return (Class>) ((Class) PostgreSQLContainer.class); + } + + @Override + protected Class getContainerAnnotation() { + return ContainerJdbc.class; + } + + @Override + protected Class getConnectionAnnotation() { + return ContainerJdbcConnection.class; + } + + @Override + protected PostgreSQLContainer createContainerDefault(PostgresJdbcMetadata metadata) { + var dockerImage = DockerImageName.parse(metadata.image()) + .asCompatibleSubstituteFor(DockerImageName.parse(PostgreSQLContainer.IMAGE)); + + String alias = "postgres-" + System.currentTimeMillis(); + return new PostgreSQLContainer<>(dockerImage) + .withDatabaseName("postgres") + .withUsername("postgres") + .withPassword("postgres") + .withLogConsumer(new Slf4jLogConsumer(LoggerFactory.getLogger(PostgreSQLContainer.class)) + .withMdc("image", metadata.image())) + .withNetworkAliases(alias); + } + + @Override + protected ContainerContext createContainerContext(PostgreSQLContainer container) { + return new PostgresJdbcContext(container); + } + + @NotNull + protected Optional findMetadata(@NotNull ExtensionContext context) { + return findAnnotation(TestcontainersJdbc.class, context) + .map(a -> new PostgresJdbcMetadata(false, null, a.image(), a.mode(), a.migration())); + } +} diff --git a/jdbc/src/test/java/io/goodforgod/testcontainers/extensions/jdbc/example/TestcontainersJdbc.java b/jdbc/src/test/java/io/goodforgod/testcontainers/extensions/jdbc/example/TestcontainersJdbc.java index 9fd3e5d..b55909a 100644 --- a/jdbc/src/test/java/io/goodforgod/testcontainers/extensions/jdbc/example/TestcontainersJdbc.java +++ b/jdbc/src/test/java/io/goodforgod/testcontainers/extensions/jdbc/example/TestcontainersJdbc.java @@ -1,14 +1,14 @@ package io.goodforgod.testcontainers.extensions.jdbc.example; import io.goodforgod.testcontainers.extensions.ContainerMode; -import io.goodforgod.testcontainers.extensions.jdbc.ExampleTestcontainersJdbcExtension; import io.goodforgod.testcontainers.extensions.jdbc.Migration; +import io.goodforgod.testcontainers.extensions.jdbc.PostgresJdbcTestcontainersJdbcExtension; import java.lang.annotation.*; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.extension.ExtendWith; @Order(Order.DEFAULT - 100) // Run before other extensions -@ExtendWith(ExampleTestcontainersJdbcExtension.class) +@ExtendWith(PostgresJdbcTestcontainersJdbcExtension.class) @Documented @Target({ ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) diff --git a/kafka/README.md b/kafka/README.md index e2b1e76..0e07db7 100644 --- a/kafka/README.md +++ b/kafka/README.md @@ -18,7 +18,7 @@ Features: **Gradle** ```groovy -testImplementation "io.goodforgod:testcontainers-extensions-kafka:0.9.6" +testImplementation "io.goodforgod:testcontainers-extensions-kafka:0.10.0" ``` **Maven** @@ -26,7 +26,7 @@ testImplementation "io.goodforgod:testcontainers-extensions-kafka:0.9.6" io.goodforgod testcontainers-extensions-kafka - 0.9.6 + 0.10.0 test ``` @@ -52,8 +52,7 @@ testRuntimeOnly "org.apache.kafka:kafka-clients:3.5.1" ## Content - [Usage](#usage) -- [Container](#container) - - [Connection](#container-connection) +- [Connection](#connection) - [Annotation](#annotation) - [Manual Container](#manual-container) - [Setup topics](#annotation-topics) @@ -69,50 +68,41 @@ Test with container start in `PER_RUN` mode and topic reset per method will look ```java @TestcontainersKafka(mode = ContainerMode.PER_RUN, - topics = @Topics(value = "my-topic", reset = Topics.Mode.PER_METHOD)) + topics = @Topics(value = "my-topic-name", reset = Topics.Mode.PER_METHOD)) class ExampleTests { + @ContainerKafkaConnection + private KafkaConnection connection; + @Test - void test(@ContainerKafkaConnection KafkaConnection connection) { - connection.send("my-topic-name", Event.ofValue("{\"name\":\"User\"}")); + void test() { + var consumer = connection.subscribe("my-topic-name"); + connection.send("my-topic-name", Event.ofValue("value1"), Event.ofValue("value2")); + consumer.assertReceivedAtLeast(2, Duration.ofSeconds(5)); } } ``` -## Container - -Library provides special `KafkaContainerExtra` with ability for migration and connection. -It can be used with [Testcontainers JUnit Extension](https://java.testcontainers.org/test_framework_integration/junit_5/). - -```java -class ExampleTests { - - @Test - void test() { - try (var container = new KafkaContainerExtra(DockerImageName.parse("cp-kafka:7.5.3"))) { - container.start(); - } - } -} -``` +## Connection -### Container Connection +`KafkaConnection` is an abstraction with asserting data in database container and easily manipulate container connection settings. +You can inject connection via `@ConnectionKafka` as field or method argument or manually create it from container or manual settings. -`RedisConnection` provides connection parameters, useful asserts, checks, etc. for easier testing. +`KafkaConnection` allow you to create consumers and send messages to Kafka for easier testing and asserting. ```java class ExampleTests { + private static final KafkaContainer container = new KafkaContainer(); + @Test void test() { - try (var container = new KafkaContainerExtra(DockerImageName.parse("cp-kafka:7.5.3"))) { container.start(); - var connection = container.connection(); - + KafkaConnection connection = KafkaConnection.forContainer(container); + var consumer = connection.subscribe("my-topic-name"); connection.send("my-topic-name", Event.ofValue("value1"), Event.ofValue("value2")); consumer.assertReceivedAtLeast(2, Duration.ofSeconds(5)); - } } } ``` @@ -172,9 +162,7 @@ Example: class ExampleTests { @ContainerKafka - private static final KafkaContainer container = new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:7.5.3")) - .withLogConsumer(new Slf4jLogConsumer(LoggerFactory.getLogger(KafkaContainer.class))) - .withNetwork(Network.SHARED); + private static final KafkaContainer container = new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:7.5.3")); @Test void checkParams(@ContainerKafkaConnection KafkaConnection connection) { diff --git a/kafka/src/main/java/io/goodforgod/testcontainers/extensions/kafka/ContainerKafkaConnection.java b/kafka/src/main/java/io/goodforgod/testcontainers/extensions/kafka/ConnectionKafka.java similarity index 94% rename from kafka/src/main/java/io/goodforgod/testcontainers/extensions/kafka/ContainerKafkaConnection.java rename to kafka/src/main/java/io/goodforgod/testcontainers/extensions/kafka/ConnectionKafka.java index 1ea8eb0..f5a6255 100644 --- a/kafka/src/main/java/io/goodforgod/testcontainers/extensions/kafka/ContainerKafkaConnection.java +++ b/kafka/src/main/java/io/goodforgod/testcontainers/extensions/kafka/ConnectionKafka.java @@ -11,7 +11,7 @@ @Documented @Target({ ElementType.FIELD, ElementType.PARAMETER }) @Retention(RetentionPolicy.RUNTIME) -public @interface ContainerKafkaConnection { +public @interface ConnectionKafka { /** * @return {@link KafkaConnection} properties that will be used {@link ConsumerConfig} and diff --git a/kafka/src/main/java/io/goodforgod/testcontainers/extensions/kafka/ContainerKafka.java b/kafka/src/main/java/io/goodforgod/testcontainers/extensions/kafka/ContainerKafka.java index 4ab579b..2178349 100644 --- a/kafka/src/main/java/io/goodforgod/testcontainers/extensions/kafka/ContainerKafka.java +++ b/kafka/src/main/java/io/goodforgod/testcontainers/extensions/kafka/ContainerKafka.java @@ -1,9 +1,10 @@ package io.goodforgod.testcontainers.extensions.kafka; import java.lang.annotation.*; +import org.testcontainers.containers.KafkaContainer; /** - * Indicates that annotated field containers {@link KafkaContainerExtra} instance + * Indicates that annotated field containers {@link KafkaContainer} instance * that should be used by {@link TestcontainersKafka} rather than creating default container */ @Documented diff --git a/kafka/src/main/java/io/goodforgod/testcontainers/extensions/kafka/KafkaConnection.java b/kafka/src/main/java/io/goodforgod/testcontainers/extensions/kafka/KafkaConnection.java index d280cd3..2d4c388 100644 --- a/kafka/src/main/java/io/goodforgod/testcontainers/extensions/kafka/KafkaConnection.java +++ b/kafka/src/main/java/io/goodforgod/testcontainers/extensions/kafka/KafkaConnection.java @@ -3,9 +3,11 @@ import java.time.Duration; import java.util.*; import org.apache.kafka.clients.admin.Admin; +import org.apache.kafka.clients.consumer.ConsumerConfig; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.Assertions; import org.opentest4j.AssertionFailedError; +import org.testcontainers.containers.KafkaContainer; /** * Kafka Connection to {@link TestcontainersKafka} @@ -16,7 +18,7 @@ * Consumer functionality * KafkaConsumer */ -public interface KafkaConnection { +public interface KafkaConnection extends AutoCloseable { /** * Kafka connection parameters @@ -51,14 +53,14 @@ interface Params { void dropTopics(@NotNull Set topics); @NotNull - default KafkaConnectionClosable withProperties(@NotNull Map properties) { + default KafkaConnection withProperties(@NotNull Map properties) { final Properties props = new Properties(); props.putAll(properties); return withProperties(props); } @NotNull - KafkaConnectionClosable withProperties(@NotNull Properties properties); + KafkaConnection withProperties(@NotNull Properties properties); void send(@NotNull String topic, @NotNull Event... events); @@ -81,7 +83,7 @@ default KafkaConnectionClosable withProperties(@NotNull Map prop /** * KafkaConsumer that is capable of testing/asserting specified topics */ - interface Consumer { + interface Consumer extends AutoCloseable { /** * Reset consumer state and wipe out all already consumed messages @@ -196,5 +198,43 @@ default boolean checkReceivedAtLeast(int expectedAtLeast) { * @return true if received exactly N events during specified time frame or false */ boolean checkReceivedEqualsInTime(int expected, @NotNull Duration timeToWait); + + @Override + void close(); } + + @NotNull + static KafkaConnection forContainer(@NotNull KafkaContainer container) { + if (!container.isRunning()) { + throw new IllegalStateException(container.getClass().getSimpleName() + " container is not running"); + } + + final Properties properties = new Properties(); + properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, container.getBootstrapServers()); + + final Properties networkProperties = new Properties(); + networkProperties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, + String.format("%s:%s", container.getNetworkAliases().get(0), "9092")); + + return new KafkaConnectionClosableImpl(properties, networkProperties); + } + + @NotNull + static KafkaConnection forBootstrapServers(@NotNull String bootstrapServers) { + final Properties properties = new Properties(); + properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers); + return new KafkaConnectionClosableImpl(properties, null); + } + + /** + * @param properties are {@link ConsumerConfig} properties + * @return kafka connection + */ + @NotNull + static KafkaConnection forProperties(@NotNull Properties properties) { + return new KafkaConnectionClosableImpl(properties, null); + } + + @Override + void close(); } diff --git a/kafka/src/main/java/io/goodforgod/testcontainers/extensions/kafka/KafkaConnectionClosable.java b/kafka/src/main/java/io/goodforgod/testcontainers/extensions/kafka/KafkaConnectionClosable.java deleted file mode 100644 index 6b16f59..0000000 --- a/kafka/src/main/java/io/goodforgod/testcontainers/extensions/kafka/KafkaConnectionClosable.java +++ /dev/null @@ -1,5 +0,0 @@ -package io.goodforgod.testcontainers.extensions.kafka; - -public interface KafkaConnectionClosable extends KafkaConnection, AutoCloseable { - -} diff --git a/kafka/src/main/java/io/goodforgod/testcontainers/extensions/kafka/KafkaConnectionClosableImpl.java b/kafka/src/main/java/io/goodforgod/testcontainers/extensions/kafka/KafkaConnectionClosableImpl.java index b0812f5..bbe3c9a 100644 --- a/kafka/src/main/java/io/goodforgod/testcontainers/extensions/kafka/KafkaConnectionClosableImpl.java +++ b/kafka/src/main/java/io/goodforgod/testcontainers/extensions/kafka/KafkaConnectionClosableImpl.java @@ -1,12 +1,11 @@ package io.goodforgod.testcontainers.extensions.kafka; import java.util.*; -import java.util.concurrent.*; import org.jetbrains.annotations.ApiStatus.Internal; import org.jetbrains.annotations.Nullable; @Internal -final class KafkaConnectionClosableImpl extends KafkaConnectionImpl implements KafkaConnectionClosable { +final class KafkaConnectionClosableImpl extends KafkaConnectionImpl { KafkaConnectionClosableImpl(Properties properties, @Nullable Properties propertiesInNetwork) { super(properties, propertiesInNetwork); @@ -14,6 +13,6 @@ final class KafkaConnectionClosableImpl extends KafkaConnectionImpl implements K @Override public void close() { - super.close(); + super.stop(); } } diff --git a/kafka/src/main/java/io/goodforgod/testcontainers/extensions/kafka/KafkaConnectionImpl.java b/kafka/src/main/java/io/goodforgod/testcontainers/extensions/kafka/KafkaConnectionImpl.java index 7214a1b..b593b0a 100644 --- a/kafka/src/main/java/io/goodforgod/testcontainers/extensions/kafka/KafkaConnectionImpl.java +++ b/kafka/src/main/java/io/goodforgod/testcontainers/extensions/kafka/KafkaConnectionImpl.java @@ -36,6 +36,8 @@ @Internal class KafkaConnectionImpl implements KafkaConnection { + private static final Duration POLL_TIMEOUT = Duration.ofMillis(500); + private static final class ParamsImpl implements Params { private final String bootstrapServers; @@ -83,7 +85,7 @@ public String toString() { private volatile KafkaProducer producer; private volatile Admin admin; - private final List consumers = new CopyOnWriteArrayList<>(); + private final Map consumerByTopic = new ConcurrentHashMap<>(); private final ParamsImpl params; @Nullable private final ParamsImpl paramsInNetwork; @@ -137,7 +139,7 @@ private void launch() { logger.info("KafkaConsumer '{}' started consuming events from topics: {}", clientId, topics); while (isActive.get()) { try { - poll(Duration.ofMillis(100)); + poll(POLL_TIMEOUT); } catch (WakeupException | InterruptException ignore) { // do nothing } catch (Exception e) { @@ -153,7 +155,7 @@ private void poll(Duration maxPollTimeout) { if (!records.isEmpty()) { logger.info("KafkaConsumer '{}' polled '{}' records from topics: {}", clientId, records.count(), topics); } else { - logger.trace("KafkaConsumer '{}' polled '{}' records...", clientId, records.count()); + logger.trace("KafkaConsumer '{}' polled '{}' records from topics {}...", clientId, records.count(), topics); } for (var record : records) { @@ -318,7 +320,16 @@ public void reset() { messageQueue.clear(); } - void close() { + boolean isClosed() { + return !isActive.get(); + } + + @Override + public void close() { + stop(); + } + + void stop() { if (isActive.compareAndSet(true, false)) { logger.debug("Stopping KafkaConsumer '{}' for {} topics...", clientId, topics); final long started = System.nanoTime(); @@ -350,7 +361,7 @@ void close() { } @Override - public @NotNull KafkaConnectionClosable withProperties(@NotNull Properties properties) { + public @NotNull KafkaConnection withProperties(@NotNull Properties properties) { final Properties kafkaProperties = new Properties(); kafkaProperties.putAll(params.properties()); kafkaProperties.putAll(properties); @@ -401,8 +412,9 @@ public void send(@NotNull String topic, @NotNull List events) { logger.trace("KafkaProducer sending event: {}", event); var result = producer.send(new ProducerRecord<>(topic, null, key, event.value().asBytes(), headers)) .get(10, TimeUnit.SECONDS); - logger.info("KafkaProducer sent event with offset '{}' with partition '{}' with timestamp '{}' event: {}", - result.offset(), result.partition(), result.timestamp(), event); + logger.info( + "KafkaProducer sent event to topic '{}' with offset '{}' with partition '{}' with timestamp '{}' event: {}", + topic, result.offset(), result.partition(), result.timestamp(), event); } catch (Exception e) { throw new KafkaConnectionException("KafkaProducer sent event failed: " + event, e); } @@ -443,11 +455,24 @@ public void send(@NotNull String topic, @NotNull List events) { .map(p -> new TopicPartition(e.getValue().name(), p.partition()))) .collect(Collectors.toSet()); - final String id = "testcontainers-kafka-" + UUID.randomUUID().toString().substring(0, 8); - var kafkaConsumer = getConsumer(id, params.properties()); - var consumer = new ConsumerImpl(kafkaConsumer, id, topicPartition); - consumers.add(consumer); - return consumer; + final String id = UUID.randomUUID().toString().substring(0, 8); + final String consumerTopicKey = topics.stream() + .sorted() + .collect(Collectors.joining(":")); + + final ConsumerImpl consumer = consumerByTopic.computeIfAbsent(consumerTopicKey, k -> { + var kafkaConsumer = getConsumer(id, params.properties()); + return new ConsumerImpl(kafkaConsumer, id, topicPartition); + }); + + if (consumer.isClosed()) { + var kafkaConsumer = getConsumer(id, params.properties()); + ConsumerImpl activeConsumer = new ConsumerImpl(kafkaConsumer, id, topicPartition); + consumerByTopic.put(consumerTopicKey, activeConsumer); + return activeConsumer; + } else { + return consumer; + } } catch (Exception e) { throw new KafkaConnectionException("Can't create KafkaConsumer", e); } @@ -612,17 +637,17 @@ public void dropTopics(@NotNull Set topics) { } void clear() { - for (var consumer : consumers) { + for (var consumer : consumerByTopic.values()) { try { - consumer.close(); + consumer.stop(); } catch (Exception e) { // do nothing } } - consumers.clear(); + consumerByTopic.clear(); } - void close() { + void stop() { if (!isClosed) { isClosed = true; @@ -648,6 +673,11 @@ void close() { } } + @Override + public void close() { + // do nothing + } + @Override public boolean equals(Object o) { if (this == o) diff --git a/kafka/src/main/java/io/goodforgod/testcontainers/extensions/kafka/KafkaContainerExtra.java b/kafka/src/main/java/io/goodforgod/testcontainers/extensions/kafka/KafkaContainerExtra.java deleted file mode 100644 index 65c15d7..0000000 --- a/kafka/src/main/java/io/goodforgod/testcontainers/extensions/kafka/KafkaContainerExtra.java +++ /dev/null @@ -1,128 +0,0 @@ -package io.goodforgod.testcontainers.extensions.kafka; - -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.time.Duration; -import java.util.*; -import org.apache.kafka.clients.consumer.ConsumerConfig; -import org.jetbrains.annotations.NotNull; -import org.slf4j.LoggerFactory; -import org.testcontainers.containers.KafkaContainer; -import org.testcontainers.containers.output.Slf4jLogConsumer; -import org.testcontainers.containers.wait.strategy.Wait; -import org.testcontainers.utility.ComparableVersion; -import org.testcontainers.utility.DockerImageName; - -public class KafkaContainerExtra extends KafkaContainer { - - // https://docs.confluent.io/platform/7.0.0/release-notes/index.html#ak-raft-kraft - private static final String MIN_KRAFT_TAG = "7.0.0"; - - private static final String EXTERNAL_TEST_KAFKA_BOOTSTRAP = "EXTERNAL_TEST_KAFKA_BOOTSTRAP_SERVERS"; - private static final String EXTERNAL_TEST_KAFKA_PREFIX = "EXTERNAL_TEST_KAFKA_"; - - private volatile KafkaConnectionImpl connection; - - public KafkaContainerExtra(String dockerImageName) { - this(DockerImageName.parse(dockerImageName)); - } - - public KafkaContainerExtra(DockerImageName dockerImageName) { - super(dockerImageName); - - final String alias = "kafka-" + System.currentTimeMillis(); - - this.withLogConsumer(new Slf4jLogConsumer(LoggerFactory.getLogger(KafkaContainerExtra.class)) - .withMdc("image", dockerImageName.asCanonicalNameString()) - .withMdc("alias", alias)); - this.withEnv("KAFKA_CONFLUENT_SUPPORT_METRICS_ENABLE", "false"); - this.withEnv("AUTO_CREATE_TOPICS", "true"); - this.withEnv("KAFKA_LOG4J_LOGGERS", - "org.apache.zookeeper=ERROR,org.kafka.zookeeper=ERROR,kafka.zookeeper=ERROR,org.apache.kafka=ERROR,kafka=ERROR,kafka.network=ERROR,kafka.cluster=ERROR,kafka.controller=ERROR,kafka.coordinator=INFO,kafka.log=ERROR,kafka.server=ERROR,state.change.logger=ERROR"); - this.withEnv("ZOOKEEPER_LOG4J_LOGGERS", - "org.apache.zookeeper=ERROR,org.kafka.zookeeper=ERROR,org.kafka.zookeeper.server=ERROR,kafka.zookeeper=ERROR,org.apache.kafka=ERROR"); - this.withExposedPorts(9092, KafkaContainer.KAFKA_PORT); - this.waitingFor(Wait.forListeningPort()); - this.withStartupTimeout(Duration.ofMinutes(5)); - - var actualVersion = new ComparableVersion(DockerImageName.parse(getDockerImageName()).getVersionPart()); - if (!actualVersion.isLessThan(MIN_KRAFT_TAG)) { - final Optional withKraft = Arrays.stream(KafkaContainer.class.getDeclaredMethods()) - .filter(m -> m.getName().equals("withKraft")) - .findFirst(); - - if (withKraft.isPresent()) { - withKraft.get().setAccessible(true); - try { - withKraft.get().invoke(this); - logger().info("Kraft is enabled"); - } catch (IllegalAccessException | InvocationTargetException e) { - this.withEmbeddedZookeeper(); - } - } else { - this.withEmbeddedZookeeper(); - } - } - - this.setNetworkAliases(new ArrayList<>(List.of(alias))); - } - - @NotNull - public KafkaConnection connection() { - if (connection == null) { - final Optional connectionExternal = getConnectionExternal(); - if (connectionExternal.isEmpty() && !isRunning()) { - throw new IllegalStateException("KafkaConnection can't be create for container that is not running"); - } - - final KafkaConnection jdbcConnection = connectionExternal.orElseGet(() -> { - final String alias = getNetworkAliases().get(getNetworkAliases().size() - 1); - - final Properties properties = new Properties(); - properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, getBootstrapServers()); - - final Properties networkProperties = new Properties(); - networkProperties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, String.format("%s:%s", alias, "9092")); - - return new KafkaConnectionImpl(properties, networkProperties); - }); - - this.connection = (KafkaConnectionImpl) jdbcConnection; - } - - return connection; - } - - @Override - public void start() { - final Optional connectionExternal = getConnectionExternal(); - if (connectionExternal.isEmpty()) { - super.start(); - } - } - - @Override - public void stop() { - connection.close(); - connection = null; - super.stop(); - } - - @NotNull - private static Optional getConnectionExternal() { - var bootstrap = System.getenv(EXTERNAL_TEST_KAFKA_BOOTSTRAP); - if (bootstrap != null) { - final Properties properties = new Properties(); - System.getenv().forEach((k, v) -> { - if (k.startsWith(EXTERNAL_TEST_KAFKA_PREFIX)) { - var name = k.replace(EXTERNAL_TEST_KAFKA_PREFIX, "").replace("_", ".").toLowerCase(); - properties.put(name, v); - } - }); - - return Optional.of(new KafkaConnectionImpl(properties, null)); - } else { - return Optional.empty(); - } - } -} diff --git a/kafka/src/main/java/io/goodforgod/testcontainers/extensions/kafka/KafkaContext.java b/kafka/src/main/java/io/goodforgod/testcontainers/extensions/kafka/KafkaContext.java new file mode 100644 index 0000000..fb61371 --- /dev/null +++ b/kafka/src/main/java/io/goodforgod/testcontainers/extensions/kafka/KafkaContext.java @@ -0,0 +1,130 @@ +package io.goodforgod.testcontainers.extensions.kafka; + +import io.goodforgod.testcontainers.extensions.ContainerContext; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.Properties; +import org.apache.kafka.clients.consumer.ConsumerConfig; +import org.jetbrains.annotations.ApiStatus.Internal; +import org.jetbrains.annotations.NotNull; +import org.testcontainers.containers.KafkaContainer; + +@Internal +final class KafkaContext implements ContainerContext { + + static final class KafkaConnectionPool { + + private final List connections = new ArrayList<>(); + + void add(KafkaConnectionImpl connection) { + connections.add(connection); + } + + void clear() { + for (KafkaConnectionImpl connection : connections) { + try { + connection.clear(); + } catch (Exception e) { + // do nothing + } + } + } + + void close() { + for (KafkaConnectionImpl connection : connections) { + try { + connection.stop(); + } catch (Exception e) { + // do nothing + } + } + + connections.clear(); + } + } + + private static final String EXTERNAL_TEST_KAFKA_BOOTSTRAP = "EXTERNAL_TEST_KAFKA_BOOTSTRAP_SERVERS"; + private static final String EXTERNAL_TEST_KAFKA_PREFIX = "EXTERNAL_TEST_KAFKA_"; + + private final KafkaConnectionPool pool = new KafkaConnectionPool(); + private final KafkaContainer container; + + private volatile KafkaConnectionImpl connection; + + KafkaContext(KafkaContainer container) { + this.container = container; + } + + @NotNull + public KafkaConnection connection() { + if (connection == null) { + final Optional connectionExternal = getConnectionExternal(); + if (connectionExternal.isEmpty() && !container.isRunning()) { + throw new IllegalStateException("KafkaConnection can't be create for container that is not running"); + } + + final KafkaConnection containerConnection = connectionExternal.orElseGet(() -> { + final String alias = container.getNetworkAliases().get(container.getNetworkAliases().size() - 1); + + final Properties properties = new Properties(); + properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, container.getBootstrapServers()); + + final Properties networkProperties = new Properties(); + networkProperties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, String.format("%s:%s", alias, "9092")); + + return new KafkaConnectionImpl(properties, networkProperties); + }); + + this.connection = (KafkaConnectionImpl) containerConnection; + } + + return connection; + } + + @Override + public void start() { + final Optional connectionExternal = getConnectionExternal(); + if (connectionExternal.isEmpty()) { + container.start(); + } + } + + @Override + public void stop() { + if (connection != null) { + connection.stop(); + connection = null; + } + pool.close(); + container.stop(); + } + + @NotNull + KafkaConnectionPool pool() { + return pool; + } + + @NotNull + private static Optional getConnectionExternal() { + var bootstrap = System.getenv(EXTERNAL_TEST_KAFKA_BOOTSTRAP); + if (bootstrap != null) { + final Properties properties = new Properties(); + System.getenv().forEach((k, v) -> { + if (k.startsWith(EXTERNAL_TEST_KAFKA_PREFIX)) { + var name = k.replace(EXTERNAL_TEST_KAFKA_PREFIX, "").replace("_", ".").toLowerCase(); + properties.put(name, v); + } + }); + + return Optional.of(new KafkaConnectionImpl(properties, null)); + } else { + return Optional.empty(); + } + } + + @Override + public String toString() { + return container.getDockerImageName(); + } +} diff --git a/kafka/src/main/java/io/goodforgod/testcontainers/extensions/kafka/KafkaExtensionContainer.java b/kafka/src/main/java/io/goodforgod/testcontainers/extensions/kafka/KafkaExtensionContainer.java deleted file mode 100644 index cf0d340..0000000 --- a/kafka/src/main/java/io/goodforgod/testcontainers/extensions/kafka/KafkaExtensionContainer.java +++ /dev/null @@ -1,74 +0,0 @@ -package io.goodforgod.testcontainers.extensions.kafka; - -import io.goodforgod.testcontainers.extensions.ExtensionContainer; -import java.util.ArrayList; -import java.util.List; -import org.jetbrains.annotations.ApiStatus.Internal; -import org.jetbrains.annotations.NotNull; - -@Internal -final class KafkaExtensionContainer implements - ExtensionContainer { - - static final class KafkaConnectionPool { - - private final List connections = new ArrayList<>(); - - void add(KafkaConnectionImpl connection) { - connections.add(connection); - } - - void clear() { - for (KafkaConnectionImpl connection : connections) { - try { - connection.clear(); - } catch (Exception e) { - // do nothing - } - } - } - - void close() { - for (KafkaConnectionImpl connection : connections) { - try { - connection.close(); - } catch (Exception e) { - // do nothing - } - } - - connections.clear(); - } - } - - private final KafkaContainerExtra container; - private final KafkaConnection connection; - private final KafkaConnectionPool pool = new KafkaConnectionPool(); - - KafkaExtensionContainer(KafkaContainerExtra container, KafkaConnection connection) { - this.container = container; - this.connection = connection; - this.pool.add((KafkaConnectionImpl) connection); - } - - @NotNull - public KafkaConnectionPool pool() { - return pool; - } - - @Override - public KafkaContainerExtra container() { - return container; - } - - @Override - public KafkaConnection connection() { - return connection; - } - - @Override - public void stop() { - pool.close(); - container.stop(); - } -} diff --git a/kafka/src/main/java/io/goodforgod/testcontainers/extensions/kafka/KafkaMetadata.java b/kafka/src/main/java/io/goodforgod/testcontainers/extensions/kafka/KafkaMetadata.java index 182514a..5259628 100644 --- a/kafka/src/main/java/io/goodforgod/testcontainers/extensions/kafka/KafkaMetadata.java +++ b/kafka/src/main/java/io/goodforgod/testcontainers/extensions/kafka/KafkaMetadata.java @@ -4,7 +4,6 @@ import io.goodforgod.testcontainers.extensions.ContainerMode; import java.util.Set; import org.jetbrains.annotations.ApiStatus.Internal; -import org.jetbrains.annotations.NotNull; @Internal final class KafkaMetadata extends AbstractContainerMetadata { @@ -18,11 +17,6 @@ final class KafkaMetadata extends AbstractContainerMetadata { this.reset = reset; } - @Override - public @NotNull String networkAliasDefault() { - return "kafka-" + System.currentTimeMillis(); - } - Set topics() { return topics; } diff --git a/kafka/src/main/java/io/goodforgod/testcontainers/extensions/kafka/TestcontainersKafka.java b/kafka/src/main/java/io/goodforgod/testcontainers/extensions/kafka/TestcontainersKafka.java index 9dc4790..e994074 100644 --- a/kafka/src/main/java/io/goodforgod/testcontainers/extensions/kafka/TestcontainersKafka.java +++ b/kafka/src/main/java/io/goodforgod/testcontainers/extensions/kafka/TestcontainersKafka.java @@ -5,9 +5,10 @@ import java.lang.annotation.*; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.extension.ExtendWith; +import org.testcontainers.containers.KafkaContainer; /** - * Extension that is running {@link KafkaContainerExtra} for tests in different modes with database + * Extension that is running {@link KafkaContainer} for tests in different modes with database * schema migration support between test executions */ @Order(Order.DEFAULT - 100) // Run before other extensions @@ -18,14 +19,12 @@ public @interface TestcontainersKafka { /** - * @see TestcontainersKafkaExtension#getContainerDefault(KafkaMetadata) * @return Kafka image *

* 1) Image can have static value: "confluentinc/cp-kafka:7.5.3" * 2) Image can be provided via environment variable using syntax: "${MY_IMAGE_ENV}" * 3) Image environment variable can have default value if empty using syntax: * "${MY_IMAGE_ENV|confluentinc/cp-kafka:7.5.3}" - *

*/ String image() default "confluentinc/cp-kafka:7.5.3"; diff --git a/kafka/src/main/java/io/goodforgod/testcontainers/extensions/kafka/TestcontainersKafkaExtension.java b/kafka/src/main/java/io/goodforgod/testcontainers/extensions/kafka/TestcontainersKafkaExtension.java index aaa4d14..d78d074 100644 --- a/kafka/src/main/java/io/goodforgod/testcontainers/extensions/kafka/TestcontainersKafkaExtension.java +++ b/kafka/src/main/java/io/goodforgod/testcontainers/extensions/kafka/TestcontainersKafkaExtension.java @@ -1,11 +1,13 @@ package io.goodforgod.testcontainers.extensions.kafka; import io.goodforgod.testcontainers.extensions.AbstractTestcontainersExtension; +import io.goodforgod.testcontainers.extensions.ContainerContext; import io.goodforgod.testcontainers.extensions.ContainerMode; -import io.goodforgod.testcontainers.extensions.ExtensionContainer; import java.lang.annotation.Annotation; import java.lang.reflect.Field; -import java.lang.reflect.Modifier; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.time.Duration; import java.util.*; import org.jetbrains.annotations.ApiStatus.Internal; import org.jetbrains.annotations.NotNull; @@ -13,13 +15,20 @@ import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.ParameterContext; import org.junit.jupiter.api.extension.ParameterResolutionException; -import org.junit.platform.commons.util.ReflectionUtils; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.KafkaContainer; import org.testcontainers.containers.Network; +import org.testcontainers.containers.output.Slf4jLogConsumer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.utility.ComparableVersion; import org.testcontainers.utility.DockerImageName; @Internal final class TestcontainersKafkaExtension extends - AbstractTestcontainersExtension { + AbstractTestcontainersExtension { + + // https://docs.confluent.io/platform/7.0.0/release-notes/index.html#ak-raft-kraft + private static final String MIN_KRAFT_TAG = "7.0.0"; private static final ExtensionContext.Namespace NAMESPACE = ExtensionContext.Namespace .create(TestcontainersKafkaExtension.class); @@ -31,7 +40,7 @@ protected Class getContainerAnnotation() { @Override protected Class getConnectionAnnotation() { - return ContainerKafkaConnection.class; + return ConnectionKafka.class; } @Override @@ -40,18 +49,56 @@ protected Class getConnectionType() { } @Override - protected Class getContainerType() { - return KafkaContainerExtra.class; + protected Class getContainerType() { + return KafkaContainer.class; } @Override - protected KafkaContainerExtra getContainerDefault(KafkaMetadata metadata) { - var dockerImage = DockerImageName.parse(metadata.image()) + protected ExtensionContext.Namespace getNamespace() { + return NAMESPACE; + } + + @Override + protected KafkaContainer createContainerDefault(KafkaMetadata metadata) { + var image = DockerImageName.parse(metadata.image()) .asCompatibleSubstituteFor(DockerImageName.parse("confluentinc/cp-kafka")); - var container = new KafkaContainerExtra(dockerImage); + var container = new KafkaContainer(image); + final String alias = Optional.ofNullable(metadata.networkAlias()).orElseGet(() -> "kafka-" + System.currentTimeMillis()); + + container.withLogConsumer(new Slf4jLogConsumer(LoggerFactory.getLogger(KafkaContainer.class)) + .withMdc("image", image.asCanonicalNameString()) + .withMdc("alias", alias)); + container.withEnv("KAFKA_CONFLUENT_SUPPORT_METRICS_ENABLE", "false"); + container.withEnv("AUTO_CREATE_TOPICS", "true"); + container.withEnv("KAFKA_LOG4J_LOGGERS", + "org.apache.zookeeper=ERROR,org.kafka.zookeeper=ERROR,kafka.zookeeper=ERROR,org.apache.kafka=ERROR,kafka=ERROR,kafka.network=ERROR,kafka.cluster=ERROR,kafka.controller=ERROR,kafka.coordinator=INFO,kafka.log=ERROR,kafka.server=ERROR,state.change.logger=ERROR"); + container.withEnv("ZOOKEEPER_LOG4J_LOGGERS", + "org.apache.zookeeper=ERROR,org.kafka.zookeeper=ERROR,org.kafka.zookeeper.server=ERROR,kafka.zookeeper=ERROR,org.apache.kafka=ERROR"); + container.withExposedPorts(9092, KafkaContainer.KAFKA_PORT); + container.waitingFor(Wait.forListeningPort()); + container.withStartupTimeout(Duration.ofMinutes(5)); + + var actualVersion = new ComparableVersion(DockerImageName.parse(container.getDockerImageName()).getVersionPart()); + if (!actualVersion.isLessThan(MIN_KRAFT_TAG)) { + final Optional withKraft = Arrays.stream(KafkaContainer.class.getDeclaredMethods()) + .filter(m -> m.getName().equals("withKraft")) + .findFirst(); + + if (withKraft.isPresent()) { + withKraft.get().setAccessible(true); + try { + withKraft.get().invoke(this); + LoggerFactory.getLogger(KafkaContainer.class).info("Kraft is enabled"); + } catch (IllegalAccessException | InvocationTargetException e) { + container.withEmbeddedZookeeper(); + } + } else { + container.withEmbeddedZookeeper(); + } + } - container.setNetworkAliases(new ArrayList<>(List.of(metadata.networkAliasOrDefault()))); + container.setNetworkAliases(new ArrayList<>(List.of(alias))); if (metadata.networkShared()) { container.withNetwork(Network.SHARED); } @@ -59,16 +106,6 @@ protected KafkaContainerExtra getContainerDefault(KafkaMetadata metadata) { return container; } - @Override - protected KafkaConnection getConnectionForContainer(KafkaMetadata metadata, KafkaContainerExtra container) { - return container.connection(); - } - - @Override - protected ExtensionContext.Namespace getNamespace() { - return NAMESPACE; - } - @NotNull protected Optional findMetadata(@NotNull ExtensionContext context) { return findAnnotation(TestcontainersKafka.class, context) @@ -77,50 +114,33 @@ protected Optional findMetadata(@NotNull ExtensionContext context } @Override - protected void injectConnection(KafkaConnection kafkaConnection, ExtensionContext context) { - var annotationProducer = getConnectionAnnotation(); - var connectionFields = ReflectionUtils.findFields(context.getRequiredTestClass(), - f -> !f.isSynthetic() - && !Modifier.isFinal(f.getModifiers()) - && !Modifier.isStatic(f.getModifiers()) - && f.getAnnotation(annotationProducer) != null, - ReflectionUtils.HierarchyTraversalMode.TOP_DOWN); - - logger.debug("Starting @ContainerKafkaConnection field injection for container properties: {}", kafkaConnection); - - var metadata = getMetadata(context); - var storage = getStorage(context); - var extensionContainer = storage.get(metadata.runMode(), KafkaExtensionContainer.class); - context.getTestInstance().ifPresent(instance -> { - for (Field field : connectionFields) { - try { - final ContainerKafkaConnection annotation = field.getAnnotation(ContainerKafkaConnection.class); - final KafkaConnectionImpl fieldKafkaConnection; - if (annotation.properties().length == 0) { - fieldKafkaConnection = (KafkaConnectionImpl) kafkaConnection; - } else { - final Properties fieldProperties = new Properties(); - fieldProperties.putAll(kafkaConnection.params().properties()); - Arrays.stream(annotation.properties()) - .forEach(property -> fieldProperties.put(property.name(), property.value())); - fieldKafkaConnection = (KafkaConnectionImpl) kafkaConnection.withProperties(fieldProperties); - extensionContainer.pool().add(fieldKafkaConnection); - } - - field.setAccessible(true); - field.set(instance, fieldKafkaConnection); - } catch (IllegalAccessException e) { - throw new IllegalStateException(String.format("Field '%s' annotated with @%s can't set kafka connection", - field.getName(), annotationProducer.getSimpleName()), e); - } + protected void + injectContextIntoField(ContainerContext containerContext, Field field, Object testClassInstance) { + try { + final ConnectionKafka annotation = field.getAnnotation(ConnectionKafka.class); + final KafkaConnectionImpl fieldKafkaConnection; + if (annotation.properties().length == 0) { + fieldKafkaConnection = (KafkaConnectionImpl) containerContext.connection(); + } else { + final Properties fieldProperties = new Properties(); + fieldProperties.putAll(containerContext.connection().params().properties()); + Arrays.stream(annotation.properties()) + .forEach(property -> fieldProperties.put(property.name(), property.value())); + fieldKafkaConnection = (KafkaConnectionImpl) containerContext.connection().withProperties(fieldProperties); + ((KafkaContext) containerContext).pool().add(fieldKafkaConnection); } - }); + + field.setAccessible(true); + field.set(testClassInstance, fieldKafkaConnection); + } catch (IllegalAccessException e) { + throw new IllegalStateException(String.format("Field '%s' annotated with @%s can't set kafka connection", + field.getName(), getConnectionAnnotation().getSimpleName()), e); + } } @Override - protected ExtensionContainer getExtensionContainer(KafkaContainerExtra container, - KafkaConnection connection) { - return new KafkaExtensionContainer(container, connection); + protected ContainerContext createContainerContext(KafkaContainer container) { + return new KafkaContext(container); } @Override @@ -129,16 +149,19 @@ public void beforeAll(ExtensionContext context) { var metadata = getMetadata(context); if (!metadata.topics().isEmpty()) { - var connectionCurrent = getConnectionCurrent(context); - var storage = getStorage(context); - if (metadata.runMode() == ContainerMode.PER_RUN) { - connectionCurrent.createTopics(metadata.topics()); - ((KafkaConnectionImpl) connectionCurrent).createTopicsIfNeeded(metadata.topics(), - metadata.reset() != Topics.Mode.NONE); - storage.put(Topics.class, metadata.reset()); - } else if (metadata.runMode() == ContainerMode.PER_CLASS) { - ((KafkaConnectionImpl) connectionCurrent).createTopicsIfNeeded(metadata.topics(), false); - storage.put(Topics.class, metadata.reset()); + ContainerContext containerContext = getContainerContext(context); + if (containerContext != null) { + var connectionCurrent = containerContext.connection(); + var storage = getStorage(context); + if (metadata.runMode() == ContainerMode.PER_RUN) { + connectionCurrent.createTopics(metadata.topics()); + ((KafkaConnectionImpl) connectionCurrent).createTopicsIfNeeded(metadata.topics(), + metadata.reset() != Topics.Mode.NONE); + storage.put(Topics.class, metadata.reset()); + } else if (metadata.runMode() == ContainerMode.PER_CLASS) { + ((KafkaConnectionImpl) connectionCurrent).createTopicsIfNeeded(metadata.topics(), false); + storage.put(Topics.class, metadata.reset()); + } } } } @@ -155,14 +178,17 @@ public void beforeEach(ExtensionContext context) { super.beforeEach(context); if (!metadata.topics().isEmpty()) { - var connectionCurrent = getConnectionCurrent(context); - if (metadata.runMode() == ContainerMode.PER_METHOD) { - ((KafkaConnectionImpl) connectionCurrent).createTopicsIfNeeded(metadata.topics(), false); - } else if (metadata.reset() == Topics.Mode.PER_METHOD) { - var storage = getStorage(context); - var createdTopicsFlag = storage.get(Topics.class, Topics.Mode.class); - if (createdTopicsFlag == null) { - ((KafkaConnectionImpl) connectionCurrent).createTopicsIfNeeded(metadata.topics(), true); + ContainerContext containerContext = getContainerContext(context); + if (containerContext != null) { + var connectionCurrent = containerContext.connection(); + if (metadata.runMode() == ContainerMode.PER_METHOD) { + ((KafkaConnectionImpl) connectionCurrent).createTopicsIfNeeded(metadata.topics(), false); + } else if (metadata.reset() == Topics.Mode.PER_METHOD) { + var storage = getStorage(context); + var createdTopicsFlag = storage.get(Topics.class, Topics.Mode.class); + if (createdTopicsFlag == null) { + ((KafkaConnectionImpl) connectionCurrent).createTopicsIfNeeded(metadata.topics(), true); + } } } } @@ -173,9 +199,9 @@ public void afterEach(ExtensionContext context) { var metadata = getMetadata(context); var storage = getStorage(context); storage.remove(Topics.class); - var extensionContainer = storage.get(metadata.runMode(), KafkaExtensionContainer.class); + var containerContext = getContainerContext(context); if (metadata.runMode() != ContainerMode.PER_METHOD) { - extensionContainer.pool().clear(); + ((KafkaContext) containerContext).pool().clear(); } super.afterEach(context); @@ -194,21 +220,19 @@ public Object resolveParameter(ParameterContext parameterContext, ExtensionConte return null; } - final ContainerKafkaConnection annotation = parameterContext.getParameter().getAnnotation(ContainerKafkaConnection.class); + final ConnectionKafka annotation = parameterContext.getParameter().getAnnotation(ConnectionKafka.class); if (annotation.properties().length == 0) { return connection; } var properties = connection.params().properties(); - for (ContainerKafkaConnection.Property property : annotation.properties()) { + for (ConnectionKafka.Property property : annotation.properties()) { properties.put(property.name(), property.value()); } - var metadata = getMetadata(context); - var storage = getStorage(context); - var extensionContainer = storage.get(metadata.runMode(), KafkaExtensionContainer.class); + var extensionContainer = getContainerContext(context); var paramConnection = connection.withProperties(properties); - extensionContainer.pool().add((KafkaConnectionImpl) paramConnection); + ((KafkaContext) extensionContainer).pool().add((KafkaConnectionImpl) paramConnection); return paramConnection; } } diff --git a/kafka/src/test/java/io/goodforgod/testcontainers/extensions/kafka/KafkaConnectionAssertsTests.java b/kafka/src/test/java/io/goodforgod/testcontainers/extensions/kafka/KafkaConnectionAssertsTests.java index 800c32b..747b025 100644 --- a/kafka/src/test/java/io/goodforgod/testcontainers/extensions/kafka/KafkaConnectionAssertsTests.java +++ b/kafka/src/test/java/io/goodforgod/testcontainers/extensions/kafka/KafkaConnectionAssertsTests.java @@ -13,7 +13,7 @@ @TestcontainersKafka(mode = ContainerMode.PER_CLASS, image = "confluentinc/cp-kafka:7.5.3") class KafkaConnectionAssertsTests { - @ContainerKafkaConnection + @ConnectionKafka private KafkaConnection connection; @Test @@ -31,155 +31,170 @@ void admin() throws Exception { void getReceived() { // given var topic = "example"; - var consumer = connection.subscribe(topic); - - // when - var event = Event.builder() - .withKey("1") - .withValue(new JSONObject().put("name", "bob")) - .withHeader("1", "1") - .withHeader("2", "2") - .build(); - connection.send(topic, event); - - // then - var received = consumer.getReceived(Duration.ofSeconds(1)); - assertTrue(received.isPresent()); - assertNotEquals(-1, received.get().offset()); - assertNotEquals(-1, received.get().partition()); - assertNotEquals(-1, received.get().timestamp()); - assertEquals(topic, received.get().topic()); - assertNotNull(received.get().datetime()); - assertEquals(event.key(), received.get().key()); - assertEquals(event.key().toString(), received.get().key().toString()); - assertEquals(event.value(), received.get().value()); - assertEquals(event.value().toString(), received.get().value().toString()); - assertEquals(event.value().asString(), received.get().value().asString()); - assertEquals(event.value().asJson().toString(), received.get().value().asJson().toString()); - assertEquals(2, event.headers().size()); - assertEquals(event.headers(), received.get().headers()); - assertEquals(event.headers().get(0), received.get().headers().get(0)); - assertEquals(event.headers().get(0).toString(), received.get().headers().get(0).toString()); - assertNotNull(received.get().toString()); + try (var consumer = connection.subscribe(topic)) { + // when + var event = Event.builder() + .withKey("1") + .withValue(new JSONObject().put("name", "bob")) + .withHeader("1", "1") + .withHeader("2", "2") + .build(); + connection.send(topic, event); + + // then + var received = consumer.getReceived(Duration.ofSeconds(1)); + assertTrue(received.isPresent()); + assertNotEquals(-1, received.get().offset()); + assertNotEquals(-1, received.get().partition()); + assertNotEquals(-1, received.get().timestamp()); + assertEquals(topic, received.get().topic()); + assertNotNull(received.get().datetime()); + assertEquals(event.key(), received.get().key()); + assertEquals(event.key().toString(), received.get().key().toString()); + assertEquals(event.value(), received.get().value()); + assertEquals(event.value().toString(), received.get().value().toString()); + assertEquals(event.value().asString(), received.get().value().asString()); + assertEquals(event.value().asJson().toString(), received.get().value().asJson().toString()); + assertEquals(2, event.headers().size()); + assertEquals(event.headers(), received.get().headers()); + assertEquals(event.headers().get(0), received.get().headers().get(0)); + assertEquals(event.headers().get(0).toString(), received.get().headers().get(0).toString()); + assertNotNull(received.get().toString()); + } } @Test void getReceivedAtLeast() { var topic = "example"; - var consumer = connection.subscribe(topic); - connection.send(topic, Event.ofValue("value1"), Event.ofValue("value2")); - var received = consumer.getReceivedAtLeast(2, Duration.ofSeconds(1)); - assertEquals(2, received.size()); - assertNotEquals(received.get(0), received.get(1)); - assertNotEquals(received.get(0).toString(), received.get(1).toString()); + try (var consumer = connection.subscribe(topic)) { + connection.send(topic, Event.ofValue("value1"), Event.ofValue("value2")); + var received = consumer.getReceivedAtLeast(2, Duration.ofSeconds(1)); + assertEquals(2, received.size()); + assertNotEquals(received.get(0), received.get(1)); + assertNotEquals(received.get(0).toString(), received.get(1).toString()); + } } @Test void assertReceivedNone() { var topic = "example"; - var consumer = connection.subscribe(topic); - consumer.assertReceivedNone(Duration.ofSeconds(1)); + try (var consumer = connection.subscribe(topic)) { + consumer.assertReceivedNone(Duration.ofSeconds(1)); + } } @Test void assertReceivedNoneThrows() { var topic = "example"; - var consumer = connection.subscribe(topic); - connection.send(topic, Event.ofValue("value")); - assertThrows(AssertionFailedError.class, () -> consumer.assertReceivedNone(Duration.ofSeconds(1))); + try (var consumer = connection.subscribe(topic)) { + connection.send(topic, Event.ofValue("value")); + assertThrows(AssertionFailedError.class, () -> consumer.assertReceivedNone(Duration.ofSeconds(1))); + } } @Test void assertReceived() { var topic = "example"; - var consumer = connection.subscribe(topic); - connection.send(topic, Event.builder().withValue("value").withHeader("1", "1").build()); - var receivedEvent = consumer.assertReceivedAtLeast(1, Duration.ofSeconds(1)); - assertNotNull(receivedEvent.toString()); + try (var consumer = connection.subscribe(topic)) { + connection.send(topic, Event.builder().withValue("value").withHeader("1", "1").build()); + var receivedEvent = consumer.assertReceivedAtLeast(1, Duration.ofSeconds(1)); + assertNotNull(receivedEvent.toString()); + } } @Test void assertReceivedThrows() { var topic = "example"; - var consumer = connection.subscribe(topic); - assertThrows(AssertionFailedError.class, () -> consumer.assertReceivedAtLeast(1, Duration.ofSeconds(1))); + try (var consumer = connection.subscribe(topic)) { + assertThrows(AssertionFailedError.class, () -> consumer.assertReceivedAtLeast(1, Duration.ofSeconds(1))); + } } @Test void assertReceivedAtLeast() { var topic = "example"; - var consumer = connection.subscribe(topic); - connection.send(topic, Event.ofValue("value1"), Event.ofValue("value2")); - consumer.assertReceivedAtLeast(2, Duration.ofSeconds(1)); + try (var consumer = connection.subscribe(topic)) { + connection.send(topic, Event.ofValue("value1"), Event.ofValue("value2")); + consumer.assertReceivedAtLeast(2, Duration.ofSeconds(1)); + } } @Test void assertReceivedAtLeastThrows() { var topic = "example"; - var consumer = connection.subscribe(topic); - assertThrows(AssertionFailedError.class, () -> consumer.assertReceivedAtLeast(2, Duration.ofSeconds(1))); + try (var consumer = connection.subscribe(topic)) { + assertThrows(AssertionFailedError.class, () -> consumer.assertReceivedAtLeast(2, Duration.ofSeconds(1))); + } } @Test void assertReceivedEquals() { var topic = "example"; - var consumer = connection.subscribe(topic); - connection.send(topic, Event.builder().withValue("value1").withKey("1").build(), - Event.ofValue("value2")); - var receivedEvents = consumer.assertReceivedEqualsInTime(2, Duration.ofSeconds(1)); - assertNotEquals(receivedEvents.get(0), receivedEvents.get(1)); - assertNotEquals(receivedEvents.get(0).toString(), receivedEvents.get(1).toString()); + try (var consumer = connection.subscribe(topic)) { + connection.send(topic, Event.builder().withValue("value1").withKey("1").build(), + Event.ofValue("value2")); + var receivedEvents = consumer.assertReceivedEqualsInTime(2, Duration.ofSeconds(1)); + assertNotEquals(receivedEvents.get(0), receivedEvents.get(1)); + assertNotEquals(receivedEvents.get(0).toString(), receivedEvents.get(1).toString()); + } } @Test void assertReceivedEqualsThrows() { var topic = "example"; - var consumer = connection.subscribe(topic); - assertThrows(AssertionFailedError.class, () -> consumer.assertReceivedEqualsInTime(2, Duration.ofSeconds(1))); + try (var consumer = connection.subscribe(topic)) { + assertThrows(AssertionFailedError.class, () -> consumer.assertReceivedEqualsInTime(2, Duration.ofSeconds(1))); + } } @Test void checkReceivedNone() { var topic = "example"; - var consumer = connection.subscribe(topic); - assertTrue(consumer.checkReceivedNone(Duration.ofSeconds(1))); + try (var consumer = connection.subscribe(topic)) { + assertTrue(consumer.checkReceivedNone(Duration.ofSeconds(1))); + } } @Test void checkReceivedNoneThrows() { var topic = "example"; - var consumer = connection.subscribe(topic); - connection.send(topic, Event.ofValue("value")); - assertFalse(consumer.checkReceivedNone(Duration.ofSeconds(1))); + try (var consumer = connection.subscribe(topic)) { + connection.send(topic, Event.ofValue("value")); + assertFalse(consumer.checkReceivedNone(Duration.ofSeconds(1))); + } } @Test void checkReceivedAtLeast() { var topic = "example"; - var consumer = connection.subscribe(topic); - connection.send(topic, Event.ofValue("value1"), Event.ofValue("value2")); - assertTrue(consumer.checkReceivedAtLeast(2, Duration.ofSeconds(1))); + try (var consumer = connection.subscribe(topic)) { + connection.send(topic, Event.ofValue("value1"), Event.ofValue("value2")); + assertTrue(consumer.checkReceivedAtLeast(2, Duration.ofSeconds(1))); + } } @Test void checkReceivedAtLeastThrows() { var topic = "example"; - var consumer = connection.subscribe(topic); - assertFalse(consumer.checkReceivedAtLeast(2, Duration.ofSeconds(1))); + try (var consumer = connection.subscribe(topic)) { + assertFalse(consumer.checkReceivedAtLeast(2, Duration.ofSeconds(1))); + } } @Test void checkReceivedEquals() { var topic = "example"; - var consumer = connection.subscribe(topic); - connection.send(topic, Event.ofValue("value1"), Event.ofValue("value2")); - assertTrue(consumer.checkReceivedEqualsInTime(2, Duration.ofSeconds(1))); + try (var consumer = connection.subscribe(topic)) { + connection.send(topic, Event.ofValue("value1"), Event.ofValue("value2")); + assertTrue(consumer.checkReceivedEqualsInTime(2, Duration.ofSeconds(1))); + } } @Test void checkReceivedEqualsThrows() { var topic = "example"; - var consumer = connection.subscribe(topic); - assertFalse(consumer.checkReceivedEqualsInTime(2, Duration.ofSeconds(1))); + try (var consumer = connection.subscribe(topic)) { + assertFalse(consumer.checkReceivedEqualsInTime(2, Duration.ofSeconds(1))); + } } } diff --git a/kafka/src/test/java/io/goodforgod/testcontainers/extensions/kafka/KafkaConnectionTopicResetTests.java b/kafka/src/test/java/io/goodforgod/testcontainers/extensions/kafka/KafkaConnectionTopicResetTests.java index 250c365..072babd 100644 --- a/kafka/src/test/java/io/goodforgod/testcontainers/extensions/kafka/KafkaConnectionTopicResetTests.java +++ b/kafka/src/test/java/io/goodforgod/testcontainers/extensions/kafka/KafkaConnectionTopicResetTests.java @@ -3,7 +3,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import io.goodforgod.testcontainers.extensions.ContainerMode; -import io.goodforgod.testcontainers.extensions.kafka.ContainerKafkaConnection.Property; +import io.goodforgod.testcontainers.extensions.kafka.ConnectionKafka.Property; import java.time.Duration; import org.apache.kafka.clients.consumer.ConsumerConfig; import org.junit.jupiter.api.MethodOrderer; @@ -17,7 +17,7 @@ @TestMethodOrder(MethodOrderer.OrderAnnotation.class) class KafkaConnectionTopicResetTests { - @ContainerKafkaConnection(properties = @Property(name = ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, value = "earliest")) + @ConnectionKafka(properties = @Property(name = ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, value = "earliest")) private KafkaConnection connection; @Order(1) @@ -25,13 +25,13 @@ class KafkaConnectionTopicResetTests { void firstConnection() { // given assertTrue(connection.params().properties().containsKey(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG)); - var consumer = connection.subscribe("my-topic"); + try (var consumer = connection.subscribe("my-topic")) { + // when + connection.send("my-topic", Event.ofValue("1")); - // when - connection.send("my-topic", Event.ofValue("1")); - - // then - consumer.assertReceivedEqualsInTime(1, Duration.ofSeconds(2)); + // then + consumer.assertReceivedEqualsInTime(1, Duration.ofSeconds(2)); + } } @Order(2) @@ -39,12 +39,12 @@ void firstConnection() { void secondConnection() { // given assertTrue(connection.params().properties().containsKey(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG)); - var consumer = connection.subscribe("my-topic"); - - // when - connection.send("my-topic", Event.ofValue("1")); + try (var consumer = connection.subscribe("my-topic")) { + // when + connection.send("my-topic", Event.ofValue("1")); - // then - consumer.assertReceivedEqualsInTime(1, Duration.ofSeconds(2)); + // then + consumer.assertReceivedEqualsInTime(1, Duration.ofSeconds(2)); + } } } diff --git a/kafka/src/test/java/io/goodforgod/testcontainers/extensions/kafka/KafkaContainerPerClassConstructorTests.java b/kafka/src/test/java/io/goodforgod/testcontainers/extensions/kafka/KafkaContainerPerClassConstructorTests.java index 4afbc7e..053afd7 100644 --- a/kafka/src/test/java/io/goodforgod/testcontainers/extensions/kafka/KafkaContainerPerClassConstructorTests.java +++ b/kafka/src/test/java/io/goodforgod/testcontainers/extensions/kafka/KafkaContainerPerClassConstructorTests.java @@ -14,13 +14,13 @@ class KafkaContainerPerClassConstructorTests { private static KafkaConnection firstConnection; - KafkaContainerPerClassConstructorTests(@ContainerKafkaConnection KafkaConnection sameConnection) { + KafkaContainerPerClassConstructorTests(@ConnectionKafka KafkaConnection sameConnection) { this.sameConnection = sameConnection; assertNotNull(sameConnection); } @BeforeAll - public static void setupAll(@ContainerKafkaConnection KafkaConnection paramConnection) { + public static void setupAll(@ConnectionKafka KafkaConnection paramConnection) { var consumer = paramConnection.subscribe("my-topic"); paramConnection.send("my-topic", Event.ofValue("my-value")); consumer.assertReceivedAtLeast(1); @@ -28,7 +28,7 @@ public static void setupAll(@ContainerKafkaConnection KafkaConnection paramConne } @BeforeEach - public void setupEach(@ContainerKafkaConnection KafkaConnection paramConnection) { + public void setupEach(@ConnectionKafka KafkaConnection paramConnection) { var consumer = paramConnection.subscribe("my-topic"); paramConnection.send("my-topic", Event.ofValue("my-value")); consumer.assertReceivedAtLeast(1); @@ -37,7 +37,7 @@ public void setupEach(@ContainerKafkaConnection KafkaConnection paramConnection) @Order(1) @Test - void firstConnection(@ContainerKafkaConnection KafkaConnection connection) { + void firstConnection(@ConnectionKafka KafkaConnection connection) { assertNull(firstConnection); assertNotNull(connection); assertNotNull(sameConnection); @@ -47,7 +47,7 @@ void firstConnection(@ContainerKafkaConnection KafkaConnection connection) { @Order(2) @Test - void secondConnection(@ContainerKafkaConnection KafkaConnection connection) { + void secondConnection(@ConnectionKafka KafkaConnection connection) { assertNotNull(connection); assertNotNull(firstConnection); assertNotNull(sameConnection); diff --git a/kafka/src/test/java/io/goodforgod/testcontainers/extensions/kafka/KafkaContainerPerMethodConstructorTests.java b/kafka/src/test/java/io/goodforgod/testcontainers/extensions/kafka/KafkaContainerPerMethodConstructorTests.java index 8cc2a7d..949e507 100644 --- a/kafka/src/test/java/io/goodforgod/testcontainers/extensions/kafka/KafkaContainerPerMethodConstructorTests.java +++ b/kafka/src/test/java/io/goodforgod/testcontainers/extensions/kafka/KafkaContainerPerMethodConstructorTests.java @@ -14,13 +14,13 @@ class KafkaContainerPerMethodConstructorTests { private static KafkaConnection firstConnection; - KafkaContainerPerMethodConstructorTests(@ContainerKafkaConnection KafkaConnection sameConnection) { + KafkaContainerPerMethodConstructorTests(@ConnectionKafka KafkaConnection sameConnection) { this.sameConnection = sameConnection; assertNotNull(sameConnection); } @BeforeEach - public void setupEach(@ContainerKafkaConnection KafkaConnection paramConnection) { + public void setupEach(@ConnectionKafka KafkaConnection paramConnection) { var consumer = paramConnection.subscribe("my-topic"); paramConnection.send("my-topic", Event.ofValue("my-value")); consumer.assertReceivedAtLeast(1); @@ -29,7 +29,7 @@ public void setupEach(@ContainerKafkaConnection KafkaConnection paramConnection) @Order(1) @Test - void firstConnection(@ContainerKafkaConnection KafkaConnection connection) { + void firstConnection(@ConnectionKafka KafkaConnection connection) { assertNull(firstConnection); assertNotNull(connection); assertNotNull(sameConnection); @@ -39,7 +39,7 @@ void firstConnection(@ContainerKafkaConnection KafkaConnection connection) { @Order(2) @Test - void secondConnection(@ContainerKafkaConnection KafkaConnection connection) { + void secondConnection(@ConnectionKafka KafkaConnection connection) { assertNotNull(connection); assertNotNull(firstConnection); assertNotNull(sameConnection); diff --git a/kafka/src/test/java/io/goodforgod/testcontainers/extensions/kafka/KafkaContainerPerRunFirstTests.java b/kafka/src/test/java/io/goodforgod/testcontainers/extensions/kafka/KafkaContainerPerRunFirstTests.java index e913e14..3e8fe4f 100644 --- a/kafka/src/test/java/io/goodforgod/testcontainers/extensions/kafka/KafkaContainerPerRunFirstTests.java +++ b/kafka/src/test/java/io/goodforgod/testcontainers/extensions/kafka/KafkaContainerPerRunFirstTests.java @@ -15,12 +15,12 @@ class KafkaContainerPerRunFirstTests { static volatile KafkaConnection perRunConnection; - @ContainerKafkaConnection + @ConnectionKafka private KafkaConnection sameConnection; @Order(1) @Test - void firstConnection(@ContainerKafkaConnection KafkaConnection connection) { + void firstConnection(@ConnectionKafka KafkaConnection connection) { assertNotNull(connection); assertNotNull(connection.params().bootstrapServers()); assertNotNull(sameConnection); @@ -40,7 +40,7 @@ void firstConnection(@ContainerKafkaConnection KafkaConnection connection) { @Order(2) @Test - void secondConnection(@ContainerKafkaConnection KafkaConnection connection) { + void secondConnection(@ConnectionKafka KafkaConnection connection) { assertNotNull(connection); assertNotNull(connection.params().bootstrapServers()); assertNotNull(sameConnection); diff --git a/kafka/src/test/java/io/goodforgod/testcontainers/extensions/kafka/KafkaContainerPerRunSecondTests.java b/kafka/src/test/java/io/goodforgod/testcontainers/extensions/kafka/KafkaContainerPerRunSecondTests.java index 269e88c..a202ff6 100644 --- a/kafka/src/test/java/io/goodforgod/testcontainers/extensions/kafka/KafkaContainerPerRunSecondTests.java +++ b/kafka/src/test/java/io/goodforgod/testcontainers/extensions/kafka/KafkaContainerPerRunSecondTests.java @@ -15,12 +15,12 @@ class KafkaContainerPerRunSecondTests { static volatile KafkaConnection perRunConnection; - @ContainerKafkaConnection + @ConnectionKafka private KafkaConnection sameConnection; @Order(1) @Test - void firstConnection(@ContainerKafkaConnection KafkaConnection connection) { + void firstConnection(@ConnectionKafka KafkaConnection connection) { assertNotNull(connection); assertNotNull(connection.params().bootstrapServers()); assertNotNull(sameConnection); @@ -40,7 +40,7 @@ void firstConnection(@ContainerKafkaConnection KafkaConnection connection) { @Order(2) @Test - void secondConnection(@ContainerKafkaConnection KafkaConnection connection) { + void secondConnection(@ConnectionKafka KafkaConnection connection) { assertNotNull(connection); assertNotNull(connection.params().bootstrapServers()); assertNotNull(sameConnection); diff --git a/mariadb/README.md b/mariadb/README.md index b6fcc84..3a179c1 100644 --- a/mariadb/README.md +++ b/mariadb/README.md @@ -18,7 +18,7 @@ Features: **Gradle** ```groovy -testImplementation "io.goodforgod:testcontainers-extensions-mariadb:0.9.6" +testImplementation "io.goodforgod:testcontainers-extensions-mariadb:0.10.0" ``` **Maven** @@ -26,7 +26,7 @@ testImplementation "io.goodforgod:testcontainers-extensions-mariadb:0.9.6" io.goodforgod testcontainers-extensions-mariadb - 0.9.6 + 0.10.0 test ``` @@ -52,9 +52,8 @@ testRuntimeOnly "org.mariadb.jdbc:mariadb-java-client:3.1.4" ## Content - [Usage](#usage) -- [Container](#container) - - [Connection](#container-connection) - - [Migration](#container-migration) +- [Connection](#connection) + - [Migration](#connection-migration) - [Annotation](#annotation) - [Manual Container](#manual-container) - [Connection](#annotation-connection) @@ -66,15 +65,18 @@ testRuntimeOnly "org.mariadb.jdbc:mariadb-java-client:3.1.4" Test with container start in `PER_RUN` mode and migration per method will look like: ```java -@TestcontainersMysql(mode = ContainerMode.PER_RUN, +@TestcontainersMariaDB(mode = ContainerMode.PER_RUN, migration = @Migration( engine = Migration.Engines.FLYWAY, apply = Migration.Mode.PER_METHOD, drop = Migration.Mode.PER_METHOD)) class ExampleTests { + @ConnectionMariaDB + private JdbcConnection connection; + @Test - void test(@ContainerMysqlConnection JdbcConnection connection) { + void test() { connection.execute("INSERT INTO users VALUES(1);"); var usersFound = connection.queryMany("SELECT * FROM users;", r -> r.getInt(1)); assertEquals(1, usersFound.size()); @@ -82,56 +84,50 @@ class ExampleTests { } ``` -## Container +## Connection -Library provides special `MariaDBContainerExtra` with ability for migration and connection. -It can be used with [Testcontainers JUnit Extension](https://java.testcontainers.org/test_framework_integration/junit_5/). +`JdbcConnection` is an abstraction with asserting data in database container and easily manipulate container connection settings. +You can inject connection via `@ConnectionMariaDB` as field or method argument or manually create it from container or manual settings. ```java class ExampleTests { + private static final MariaDBContainer container = new MariaDBContainer<>(); + @Test void test() { - try (var container = new MariaDBContainerExtra<>(DockerImageName.parse("mariadb:11.2-jammy"))) { - container.start(); - } + container.start(); + JdbcConnection connection = JdbcConnection.forContainer(container); + connection.execute("INSERT INTO users VALUES(1);"); } } ``` -### Container Connection +### Connection Migration -`JdbcConnection` provides connection parameters, useful asserts, checks, etc. for easier testing. +`Migrations` allow easily migrate database between test executions and drop after tests. +You can migrate container via `@TestcontainersMariaDB#migration` annotation parameter or manually using `JdbcConnection`. ```java +@TestcontainersMariaDB class ExampleTests { - @Test - void test() { - try (var container = new MariaDBContainerExtra<>(DockerImageName.parse("mariadb:11.2-jammy"))) { - container.start(); - container.connection().assertQueriesNone("SELECT * FROM users;"); + @Test + void test(@ConnectionMariaDB JdbcConnection connection) { + connection.migrationEngine(Migration.Engines.FLYWAY).apply("db/migration"); + connection.execute("INSERT INTO users VALUES(1);"); + connection.migrationEngine(Migration.Engines.FLYWAY).drop("db/migration"); } - } } ``` -### Container Migration - -`Migrations` allow easily migrate database between test executions and drop after tests. - -Annotation parameters: -- `engine` - to use for migration. -- `apply` - parameter configures migration mode. -- `drop` - configures when to reset/drop/clear database. - Available migration engines: - [Flyway](https://documentation.red-gate.com/fd/cockroachdb-184127591.html) - [Liquibase](https://www.liquibase.com/databases/cockroachdb-2) ## Annotation -`@TestcontainersMariadb` - allow **automatically start container** with specified image in different modes without the need to configure it. +`@TestcontainersMariaDB` - allow **automatically start container** with specified image in different modes without the need to configure it. Available containers modes: @@ -141,11 +137,11 @@ Available containers modes: Simple example on how to start container per class, **no need to configure** container: ```java -@TestcontainersMariadb(mode = ContainerMode.PER_CLASS) +@TestcontainersMariaDB(mode = ContainerMode.PER_CLASS) class ExampleTests { @Test - void test(@ContainerMariadbConnection JdbcConnection connection) { + void test(@ConnectionMariaDB JdbcConnection connection) { assertNotNull(connection); } } @@ -157,7 +153,7 @@ It is possible to customize image with annotation `image` parameter. Image also can be provided from environment variable: ```java -@TestcontainersMariadb(image = "${MY_IMAGE_ENV|mariadb:11.2-jammy}") +@TestcontainersMariaDB(image = "${MY_IMAGE_ENV|mariadb:11.2-jammy}") class ExampleTests { @Test @@ -175,22 +171,22 @@ Image syntax: ### Manual Container -When you need to **manually configure container** with specific options, you can provide such container as instance that will be used by `@TestcontainersMariadb`, -this can be done using `@ContainerMariadb` annotation for container. +When you need to **manually configure container** with specific options, you can provide such container as instance that will be used by `@TestcontainersMariaDB`, +this can be done using `@ContainerMariaDB` annotation for container. Example: ```java -@TestcontainersMariadb(mode = ContainerMode.PER_CLASS) +@TestcontainersMariaDB(mode = ContainerMode.PER_CLASS) class ExampleTests { - @ContainerMariadb + @ContainerMariaDB private static final MariaDBContainer container = new MariaDBContainer<>() .withDatabaseName("user") .withUsername("user") .withPassword("user"); @Test - void test(@ContainerMariadbConnection JdbcConnection connection) { + void test(@ConnectionMariaDB JdbcConnection connection) { assertEquals("user", connection.params().database()); assertEquals("user", connection.params().username()); assertEquals("user", connection.params().password()); @@ -202,7 +198,7 @@ class ExampleTests { In case you want to enable [Network.SHARED](https://java.testcontainers.org/features/networking/) for containers you can do this using `network` & `shared` parameter in annotation: ```java -@TestcontainersMariadb(network = @Network(shared = true)) +@TestcontainersMariaDB(network = @Network(shared = true)) class ExampleTests { @Test @@ -219,7 +215,7 @@ Alias can be extracted from environment variable also or default value can be pr In case specified environment variable is missing `default alias` will be created: ```java -@TestcontainersMariadb(network = @Network(alias = "${MY_ALIAS_ENV|my_default_alias}")) +@TestcontainersMariaDB(network = @Network(alias = "${MY_ALIAS_ENV|my_default_alias}")) class ExampleTests { @Test @@ -237,19 +233,19 @@ Image syntax: ### Annotation Connection -`JdbcConnection` - can be injected to field or method parameter and used to communicate with running container via `@ContainerMariadbConnection` annotation. +`JdbcConnection` - can be injected to field or method parameter and used to communicate with running container via `@ConnectionMariaDB` annotation. `JdbcConnection` provides connection parameters, useful asserts, checks, etc. for easier testing. Example: ```java -@TestcontainersMariadb(mode = ContainerMode.PER_CLASS, image = "mariadb:11.2-jammy") +@TestcontainersMariaDB(mode = ContainerMode.PER_CLASS, image = "mariadb:11.2-jammy") class ExampleTests { - @ContainerMariadbConnection - private JdbcConnection connectionInField; + @ConnectionMariaDB + private JdbcConnection connection; @Test - void test(@ContainerMariadbConnection JdbcConnection connection) { + void test() { connection.execute("CREATE TABLE users (id INT NOT NULL PRIMARY KEY);"); connection.execute("INSERT INTO users VALUES(1);"); connection.assertInserted("INSERT INTO users VALUES(2);"); @@ -286,6 +282,7 @@ Annotation parameters: - `engine` - to use for migration. - `apply` - parameter configures migration mode. - `drop` - configures when to reset/drop/clear database. +- `locations` - configures locations where migrations are placed. Available migration engines: - [Flyway](https://documentation.red-gate.com/fd/mariadb-184127600.html) @@ -301,7 +298,7 @@ CREATE TABLE IF NOT EXISTS users Test with container and migration per method will look like: ```java -@TestcontainersMariadb(mode = ContainerMode.PER_CLASS, +@TestcontainersMariaDB(mode = ContainerMode.PER_CLASS, migration = @Migration( engine = Migration.Engines.FLYWAY, apply = Migration.Mode.PER_METHOD, @@ -309,7 +306,7 @@ Test with container and migration per method will look like: class ExampleTests { @Test - void test(@ContainerMariadbConnection JdbcConnection connection) { + void test(@ConnectionMariaDB JdbcConnection connection) { connection.execute("INSERT INTO users VALUES(1);"); var usersFound = connection.queryMany("SELECT * FROM users;", r -> r.getInt(1)); assertEquals(1, usersFound.size()); diff --git a/oracle/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/ContainerOracleConnection.java b/mariadb/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/ConnectionMariaDB.java similarity index 87% rename from oracle/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/ContainerOracleConnection.java rename to mariadb/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/ConnectionMariaDB.java index 118be6f..9e0ee8e 100644 --- a/oracle/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/ContainerOracleConnection.java +++ b/mariadb/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/ConnectionMariaDB.java @@ -9,4 +9,4 @@ @Documented @Target({ ElementType.FIELD, ElementType.PARAMETER }) @Retention(RetentionPolicy.RUNTIME) -public @interface ContainerOracleConnection {} +public @interface ConnectionMariaDB {} diff --git a/mariadb/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/ContainerMariaDB.java b/mariadb/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/ContainerMariaDB.java new file mode 100644 index 0000000..9405f1f --- /dev/null +++ b/mariadb/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/ContainerMariaDB.java @@ -0,0 +1,13 @@ +package io.goodforgod.testcontainers.extensions.jdbc; + +import java.lang.annotation.*; + +/** + * Indicates that annotated field containers {@link org.testcontainers.containers.MariaDBContainer} + * instance + * that should be used by {@link TestcontainersMariaDB} rather than creating default container + */ +@Documented +@Target({ ElementType.FIELD }) +@Retention(RetentionPolicy.RUNTIME) +public @interface ContainerMariaDB {} diff --git a/mariadb/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/ContainerMariadb.java b/mariadb/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/ContainerMariadb.java deleted file mode 100644 index f0dbdd9..0000000 --- a/mariadb/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/ContainerMariadb.java +++ /dev/null @@ -1,12 +0,0 @@ -package io.goodforgod.testcontainers.extensions.jdbc; - -import java.lang.annotation.*; - -/** - * Indicates that annotated field containers {@link MariaDBContainerExtra} instance - * that should be used by {@link TestcontainersMariadb} rather than creating default container - */ -@Documented -@Target({ ElementType.FIELD }) -@Retention(RetentionPolicy.RUNTIME) -public @interface ContainerMariadb {} diff --git a/mariadb/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/MariaDBContainerExtra.java b/mariadb/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/MariaDBContainerExtra.java deleted file mode 100644 index b98af67..0000000 --- a/mariadb/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/MariaDBContainerExtra.java +++ /dev/null @@ -1,142 +0,0 @@ -package io.goodforgod.testcontainers.extensions.jdbc; - -import java.time.Duration; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import org.jetbrains.annotations.ApiStatus.Internal; -import org.jetbrains.annotations.NotNull; -import org.slf4j.LoggerFactory; -import org.testcontainers.containers.MariaDBContainer; -import org.testcontainers.containers.output.Slf4jLogConsumer; -import org.testcontainers.containers.wait.strategy.Wait; -import org.testcontainers.utility.DockerImageName; - -public class MariaDBContainerExtra> extends MariaDBContainer { - - private static final String PROTOCOL = "mariadb"; - private static final String DATABASE_NAME = "mariadb"; - private static final Integer MARIADB_PORT = 3306; - - private static final String EXTERNAL_TEST_MARIADB_JDBC_URL = "EXTERNAL_TEST_MARIADB_JDBC_URL"; - private static final String EXTERNAL_TEST_MARIADB_USERNAME = "EXTERNAL_TEST_MARIADB_USERNAME"; - private static final String EXTERNAL_TEST_MARIADB_PASSWORD = "EXTERNAL_TEST_MARIADB_PASSWORD"; - private static final String EXTERNAL_TEST_MARIADB_HOST = "EXTERNAL_TEST_MARIADB_HOST"; - private static final String EXTERNAL_TEST_MARIADB_PORT = "EXTERNAL_TEST_MARIADB_PORT"; - private static final String EXTERNAL_TEST_MARIADB_DATABASE = "EXTERNAL_TEST_MARIADB_DATABASE"; - - private volatile JdbcConnectionImpl connection; - private volatile FlywayJdbcMigrationEngine flywayJdbcMigrationEngine; - private volatile LiquibaseJdbcMigrationEngine liquibaseJdbcMigrationEngine; - - public MariaDBContainerExtra(String dockerImageName) { - this(DockerImageName.parse(dockerImageName)); - } - - public MariaDBContainerExtra(DockerImageName dockerImageName) { - super(dockerImageName); - - final String alias = "mariadb-" + System.currentTimeMillis(); - - this.withDatabaseName(DATABASE_NAME); - this.withUsername("mariadb"); - this.withPassword("mariadb"); - this.withLogConsumer(new Slf4jLogConsumer(LoggerFactory.getLogger(MariaDBContainerExtra.class)) - .withMdc("image", dockerImageName.asCanonicalNameString()) - .withMdc("alias", alias)); - this.waitingFor(Wait.forListeningPort()); - this.withStartupTimeout(Duration.ofMinutes(5)); - - this.setNetworkAliases(new ArrayList<>(List.of(alias))); - } - - @Internal - JdbcMigrationEngine getMigrationEngine(@NotNull Migration.Engines engine) { - if (engine == Migration.Engines.FLYWAY) { - if (flywayJdbcMigrationEngine == null) { - this.flywayJdbcMigrationEngine = new FlywayJdbcMigrationEngine(connection()); - } - return this.flywayJdbcMigrationEngine; - } else if (engine == Migration.Engines.LIQUIBASE) { - if (liquibaseJdbcMigrationEngine == null) { - this.liquibaseJdbcMigrationEngine = new LiquibaseJdbcMigrationEngine(connection()); - } - return this.liquibaseJdbcMigrationEngine; - } else { - throw new UnsupportedOperationException("Unsupported engine: " + engine); - } - } - - @NotNull - public JdbcConnection connection() { - if (connection == null) { - final Optional connectionExternal = getConnectionExternal(); - if (connectionExternal.isEmpty() && !isRunning()) { - throw new IllegalStateException("MariadbConnection can't be create for container that is not running"); - } - - final JdbcConnection jdbcConnection = connectionExternal.orElseGet(() -> { - final String alias = getNetworkAliases().get(getNetworkAliases().size() - 1); - return JdbcConnectionImpl.forJDBC(getJdbcUrl(), - getHost(), - getMappedPort(MARIADB_PORT), - alias, - MARIADB_PORT, - getDatabaseName(), - getUsername(), - getPassword()); - }); - - this.connection = (JdbcConnectionImpl) jdbcConnection; - } - - return connection; - } - - @Override - public void start() { - final Optional connectionExternal = getConnectionExternal(); - if (connectionExternal.isEmpty()) { - super.start(); - } - } - - @Override - public void stop() { - if (flywayJdbcMigrationEngine != null) { - flywayJdbcMigrationEngine.close(); - flywayJdbcMigrationEngine = null; - } - if (liquibaseJdbcMigrationEngine != null) { - liquibaseJdbcMigrationEngine.close(); - liquibaseJdbcMigrationEngine = null; - } - if (connection != null) { - connection.close(); - connection = null; - } - super.stop(); - } - - @NotNull - private static Optional getConnectionExternal() { - var url = System.getenv(EXTERNAL_TEST_MARIADB_JDBC_URL); - var host = System.getenv(EXTERNAL_TEST_MARIADB_HOST); - var port = System.getenv(EXTERNAL_TEST_MARIADB_PORT); - var user = System.getenv(EXTERNAL_TEST_MARIADB_USERNAME); - var password = System.getenv(EXTERNAL_TEST_MARIADB_PASSWORD); - var db = Optional.ofNullable(System.getenv(EXTERNAL_TEST_MARIADB_DATABASE)).orElse(DATABASE_NAME); - - if (url != null) { - if (host != null && port != null) { - return Optional.of(JdbcConnectionImpl.forJDBC(url, host, Integer.parseInt(port), null, null, db, user, password)); - } else { - return Optional.of(JdbcConnectionImpl.forExternal(url, user, password)); - } - } else if (host != null && port != null) { - return Optional.of(JdbcConnectionImpl.forProtocol(PROTOCOL, host, Integer.parseInt(port), db, user, password)); - } else { - return Optional.empty(); - } - } -} diff --git a/mariadb/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/MariaDBContext.java b/mariadb/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/MariaDBContext.java new file mode 100644 index 0000000..6aee229 --- /dev/null +++ b/mariadb/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/MariaDBContext.java @@ -0,0 +1,100 @@ +package io.goodforgod.testcontainers.extensions.jdbc; + +import io.goodforgod.testcontainers.extensions.ContainerContext; +import java.util.Optional; +import org.jetbrains.annotations.ApiStatus.Internal; +import org.jetbrains.annotations.NotNull; +import org.testcontainers.containers.MariaDBContainer; + +@Internal +final class MariaDBContext implements ContainerContext { + + static final String DATABASE_NAME = "mariadb"; + private static final String PROTOCOL = "mariadb"; + private static final Integer MARIADB_PORT = 3306; + + private static final String EXTERNAL_TEST_MARIADB_JDBC_URL = "EXTERNAL_TEST_MARIADB_JDBC_URL"; + private static final String EXTERNAL_TEST_MARIADB_USERNAME = "EXTERNAL_TEST_MARIADB_USERNAME"; + private static final String EXTERNAL_TEST_MARIADB_PASSWORD = "EXTERNAL_TEST_MARIADB_PASSWORD"; + private static final String EXTERNAL_TEST_MARIADB_HOST = "EXTERNAL_TEST_MARIADB_HOST"; + private static final String EXTERNAL_TEST_MARIADB_PORT = "EXTERNAL_TEST_MARIADB_PORT"; + private static final String EXTERNAL_TEST_MARIADB_DATABASE = "EXTERNAL_TEST_MARIADB_DATABASE"; + + private volatile JdbcConnectionImpl connection; + + private final MariaDBContainer container; + + MariaDBContext(MariaDBContainer container) { + this.container = container; + } + + @NotNull + public JdbcConnection connection() { + if (connection == null) { + final Optional connectionExternal = getConnectionExternal(); + if (connectionExternal.isEmpty() && !container.isRunning()) { + throw new IllegalStateException("MariadbConnection can't be create for container that is not running"); + } + + final JdbcConnection containerConnection = connectionExternal.orElseGet(() -> { + final String alias = container.getNetworkAliases().get(container.getNetworkAliases().size() - 1); + return JdbcConnectionImpl.forJDBC(container.getJdbcUrl(), + container.getHost(), + container.getMappedPort(MARIADB_PORT), + alias, + MARIADB_PORT, + container.getDatabaseName(), + container.getUsername(), + container.getPassword()); + }); + + this.connection = (JdbcConnectionImpl) containerConnection; + } + + return connection; + } + + @Override + public void start() { + final Optional connectionExternal = getConnectionExternal(); + if (connectionExternal.isEmpty()) { + container.start(); + } + } + + @Override + public void stop() { + if (connection != null) { + connection.stop(); + connection = null; + } + container.stop(); + } + + @NotNull + private static Optional getConnectionExternal() { + var url = System.getenv(EXTERNAL_TEST_MARIADB_JDBC_URL); + var host = System.getenv(EXTERNAL_TEST_MARIADB_HOST); + var port = System.getenv(EXTERNAL_TEST_MARIADB_PORT); + var user = System.getenv(EXTERNAL_TEST_MARIADB_USERNAME); + var password = System.getenv(EXTERNAL_TEST_MARIADB_PASSWORD); + var db = Optional.ofNullable(System.getenv(EXTERNAL_TEST_MARIADB_DATABASE)).orElse(DATABASE_NAME); + + if (url != null) { + if (host != null && port != null) { + return Optional.of(JdbcConnectionImpl.forJDBC(url, host, Integer.parseInt(port), null, null, db, user, password)); + } else { + return Optional.of(JdbcConnectionImpl.forExternal(url, user, password)); + } + } else if (host != null && port != null) { + return Optional.of(JdbcConnectionImpl.forProtocol(PROTOCOL, host, Integer.parseInt(port), db, user, password)); + } else { + return Optional.empty(); + } + } + + @Override + public String toString() { + return container.getDockerImageName(); + } +} diff --git a/mariadb/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/MariadbMetadata.java b/mariadb/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/MariadbMetadata.java deleted file mode 100644 index 7ffae48..0000000 --- a/mariadb/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/MariadbMetadata.java +++ /dev/null @@ -1,18 +0,0 @@ -package io.goodforgod.testcontainers.extensions.jdbc; - -import io.goodforgod.testcontainers.extensions.ContainerMode; -import org.jetbrains.annotations.ApiStatus.Internal; -import org.jetbrains.annotations.NotNull; - -@Internal -final class MariadbMetadata extends JdbcMetadata { - - public MariadbMetadata(boolean network, String alias, String image, ContainerMode runMode, Migration migration) { - super(network, alias, image, runMode, migration); - } - - @Override - public @NotNull String networkAliasDefault() { - return "mariadb-" + System.currentTimeMillis(); - } -} diff --git a/mariadb/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/TestcontainersMariadb.java b/mariadb/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/TestcontainersMariaDB.java similarity index 82% rename from mariadb/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/TestcontainersMariadb.java rename to mariadb/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/TestcontainersMariaDB.java index 74f85d7..e98a200 100644 --- a/mariadb/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/TestcontainersMariadb.java +++ b/mariadb/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/TestcontainersMariaDB.java @@ -5,28 +5,27 @@ import java.lang.annotation.*; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.extension.ExtendWith; +import org.testcontainers.containers.MariaDBContainer; /** - * Extension that is running {@link MariaDBContainerExtra} for tests in different modes with + * Extension that is running {@link MariaDBContainer} for tests in different modes with * database * schema migration support between test executions */ @Order(Order.DEFAULT - 100) // Run before other extensions -@ExtendWith(TestcontainersMariadbExtension.class) +@ExtendWith(TestcontainersMariaDBExtension.class) @Documented @Target({ ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) -public @interface TestcontainersMariadb { +public @interface TestcontainersMariaDB { /** - * @see TestcontainersMariadbExtension#getContainerDefault(MariadbMetadata) * @return MariaDB image *

* 1) Image can have static value: "mariadb:11.2-jammy" * 2) Image can be provided via environment variable using syntax: "${MY_IMAGE_ENV}" * 3) Image environment variable can have default value if empty using syntax: * "${MY_IMAGE_ENV|mariadb:11.2-jammy}" - *

*/ String image() default "mariadb:11.2-jammy"; diff --git a/mariadb/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/TestcontainersMariaDBExtension.java b/mariadb/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/TestcontainersMariaDBExtension.java new file mode 100644 index 0000000..47e0c8f --- /dev/null +++ b/mariadb/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/TestcontainersMariaDBExtension.java @@ -0,0 +1,80 @@ +package io.goodforgod.testcontainers.extensions.jdbc; + +import io.goodforgod.testcontainers.extensions.ContainerContext; +import java.lang.annotation.Annotation; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.MariaDBContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.containers.output.Slf4jLogConsumer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.utility.DockerImageName; + +final class TestcontainersMariaDBExtension extends + AbstractTestcontainersJdbcExtension, JdbcMetadata> { + + private static final ExtensionContext.Namespace NAMESPACE = ExtensionContext.Namespace + .create(TestcontainersMariaDBExtension.class); + + @SuppressWarnings("unchecked") + @Override + protected Class> getContainerType() { + return (Class>) ((Class) org.testcontainers.containers.MariaDBContainer.class); + } + + @Override + protected Class getContainerAnnotation() { + return ContainerMariaDB.class; + } + + @Override + protected Class getConnectionAnnotation() { + return ConnectionMariaDB.class; + } + + @Override + protected ExtensionContext.Namespace getNamespace() { + return NAMESPACE; + } + + @Override + protected org.testcontainers.containers.MariaDBContainer createContainerDefault(JdbcMetadata metadata) { + var image = DockerImageName.parse(metadata.image()) + .asCompatibleSubstituteFor(DockerImageName.parse(org.testcontainers.containers.MariaDBContainer.NAME)); + + final MariaDBContainer container = new MariaDBContainer<>(image); + final String alias = Optional.ofNullable(metadata.networkAlias()) + .orElseGet(() -> "mariadb-" + System.currentTimeMillis()); + container.withDatabaseName(MariaDBContext.DATABASE_NAME); + container.withUsername("mariadb"); + container.withPassword("mariadb"); + container.withLogConsumer(new Slf4jLogConsumer(LoggerFactory.getLogger(MariaDBContainer.class)) + .withMdc("image", image.asCanonicalNameString()) + .withMdc("alias", alias)); + container.waitingFor(Wait.forListeningPort()); + container.withStartupTimeout(Duration.ofMinutes(5)); + container.setNetworkAliases(new ArrayList<>(List.of(alias))); + if (metadata.networkShared()) { + container.withNetwork(Network.SHARED); + } + + return container; + } + + @Override + protected ContainerContext + createContainerContext(org.testcontainers.containers.MariaDBContainer container) { + return new MariaDBContext(container); + } + + @NotNull + protected Optional findMetadata(@NotNull ExtensionContext context) { + return findAnnotation(TestcontainersMariaDB.class, context) + .map(a -> new JdbcMetadata(a.network().shared(), a.network().alias(), a.image(), a.mode(), a.migration())); + } +} diff --git a/mariadb/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/TestcontainersMariadbExtension.java b/mariadb/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/TestcontainersMariadbExtension.java deleted file mode 100644 index a519823..0000000 --- a/mariadb/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/TestcontainersMariadbExtension.java +++ /dev/null @@ -1,70 +0,0 @@ -package io.goodforgod.testcontainers.extensions.jdbc; - -import java.lang.annotation.Annotation; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import org.jetbrains.annotations.NotNull; -import org.junit.jupiter.api.extension.ExtensionContext; -import org.testcontainers.containers.MariaDBContainer; -import org.testcontainers.containers.Network; -import org.testcontainers.utility.DockerImageName; - -final class TestcontainersMariadbExtension extends - AbstractTestcontainersJdbcExtension, MariadbMetadata> { - - private static final ExtensionContext.Namespace NAMESPACE = ExtensionContext.Namespace - .create(TestcontainersMariadbExtension.class); - - @SuppressWarnings("unchecked") - @Override - protected Class> getContainerType() { - return (Class>) ((Class) MariaDBContainerExtra.class); - } - - @Override - protected Class getContainerAnnotation() { - return ContainerMariadb.class; - } - - @Override - protected Class getConnectionAnnotation() { - return ContainerMariadbConnection.class; - } - - @Override - protected MariaDBContainerExtra getContainerDefault(MariadbMetadata metadata) { - var dockerImage = DockerImageName.parse(metadata.image()) - .asCompatibleSubstituteFor(DockerImageName.parse(MariaDBContainer.NAME)); - - var container = new MariaDBContainerExtra<>(dockerImage); - container.setNetworkAliases(new ArrayList<>(List.of(metadata.networkAliasOrDefault()))); - if (metadata.networkShared()) { - container.withNetwork(Network.SHARED); - } - - return container; - } - - @Override - protected JdbcMigrationEngine getMigrationEngine(Migration.Engines engine, ExtensionContext context) { - var containerCurrent = getContainerCurrent(context); - return containerCurrent.getMigrationEngine(engine); - } - - @Override - protected ExtensionContext.Namespace getNamespace() { - return NAMESPACE; - } - - @NotNull - protected Optional findMetadata(@NotNull ExtensionContext context) { - return findAnnotation(TestcontainersMariadb.class, context) - .map(a -> new MariadbMetadata(a.network().shared(), a.network().alias(), a.image(), a.mode(), a.migration())); - } - - @NotNull - protected JdbcConnection getConnectionForContainer(MariadbMetadata metadata, @NotNull MariaDBContainerExtra container) { - return container.connection(); - } -} diff --git a/mariadb/src/test/java/io/goodforgod/testcontainers/extensions/mariadb/MariadbFlywayPerMethodMigrationTests.java b/mariadb/src/test/java/io/goodforgod/testcontainers/extensions/mariadb/MariadbFlywayPerMethodMigrationTests.java index e8ba171..e95f3ca 100644 --- a/mariadb/src/test/java/io/goodforgod/testcontainers/extensions/mariadb/MariadbFlywayPerMethodMigrationTests.java +++ b/mariadb/src/test/java/io/goodforgod/testcontainers/extensions/mariadb/MariadbFlywayPerMethodMigrationTests.java @@ -3,16 +3,16 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import io.goodforgod.testcontainers.extensions.ContainerMode; -import io.goodforgod.testcontainers.extensions.jdbc.ContainerMariadbConnection; +import io.goodforgod.testcontainers.extensions.jdbc.ConnectionMariaDB; import io.goodforgod.testcontainers.extensions.jdbc.JdbcConnection; import io.goodforgod.testcontainers.extensions.jdbc.Migration; -import io.goodforgod.testcontainers.extensions.jdbc.TestcontainersMariadb; +import io.goodforgod.testcontainers.extensions.jdbc.TestcontainersMariaDB; import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestMethodOrder; -@TestcontainersMariadb(mode = ContainerMode.PER_CLASS, +@TestcontainersMariaDB(mode = ContainerMode.PER_CLASS, image = "mariadb:11.2-jammy", migration = @Migration( engine = Migration.Engines.FLYWAY, @@ -23,13 +23,13 @@ class MariadbFlywayPerMethodMigrationTests { @Order(1) @Test - void firstRun(@ContainerMariadbConnection JdbcConnection connection) { + void firstRun(@ConnectionMariaDB JdbcConnection connection) { connection.execute("INSERT INTO users VALUES(1);"); } @Order(2) @Test - void secondRun(@ContainerMariadbConnection JdbcConnection connection) { + void secondRun(@ConnectionMariaDB JdbcConnection connection) { var usersFound = connection.queryOne("SELECT * FROM users;", r -> r.getInt(1)); assertTrue(usersFound.isEmpty()); } diff --git a/mariadb/src/test/java/io/goodforgod/testcontainers/extensions/mariadb/MariadbLiquibaseMigrationPerMethodTests.java b/mariadb/src/test/java/io/goodforgod/testcontainers/extensions/mariadb/MariadbLiquibaseMigrationPerMethodTests.java index cffb358..6b28601 100644 --- a/mariadb/src/test/java/io/goodforgod/testcontainers/extensions/mariadb/MariadbLiquibaseMigrationPerMethodTests.java +++ b/mariadb/src/test/java/io/goodforgod/testcontainers/extensions/mariadb/MariadbLiquibaseMigrationPerMethodTests.java @@ -3,16 +3,16 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import io.goodforgod.testcontainers.extensions.ContainerMode; -import io.goodforgod.testcontainers.extensions.jdbc.ContainerMariadbConnection; +import io.goodforgod.testcontainers.extensions.jdbc.ConnectionMariaDB; import io.goodforgod.testcontainers.extensions.jdbc.JdbcConnection; import io.goodforgod.testcontainers.extensions.jdbc.Migration; -import io.goodforgod.testcontainers.extensions.jdbc.TestcontainersMariadb; +import io.goodforgod.testcontainers.extensions.jdbc.TestcontainersMariaDB; import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestMethodOrder; -@TestcontainersMariadb(mode = ContainerMode.PER_CLASS, +@TestcontainersMariaDB(mode = ContainerMode.PER_CLASS, image = "mariadb:11.2-jammy", migration = @Migration( engine = Migration.Engines.LIQUIBASE, @@ -23,13 +23,13 @@ class MariadbLiquibaseMigrationPerMethodTests { @Order(1) @Test - void firstRun(@ContainerMariadbConnection JdbcConnection connection) { + void firstRun(@ConnectionMariaDB JdbcConnection connection) { connection.execute("INSERT INTO users VALUES(1);"); } @Order(2) @Test - void secondRun(@ContainerMariadbConnection JdbcConnection connection) { + void secondRun(@ConnectionMariaDB JdbcConnection connection) { var usersFound = connection.queryOne("SELECT * FROM users;", r -> r.getInt(1)); assertTrue(usersFound.isEmpty()); } diff --git a/mockserver/README.md b/mockserver/README.md index c66da35..6c32462 100644 --- a/mockserver/README.md +++ b/mockserver/README.md @@ -17,7 +17,7 @@ Features: **Gradle** ```groovy -testImplementation "io.goodforgod:testcontainers-extensions-mockserver:0.9.6" +testImplementation "io.goodforgod:testcontainers-extensions-mockserver:0.10.0" ``` **Maven** @@ -25,15 +25,14 @@ testImplementation "io.goodforgod:testcontainers-extensions-mockserver:0.9.6" io.goodforgod testcontainers-extensions-mockserver - 0.9.6 + 0.10.0 test ``` ## Content - [Usage](#usage) -- [Container](#container) - - [Connection](#container-connection) +- [Connection](#connection) - [Annotation](#annotation) - [Manual Container](#manual-container) - [Connection](#annotation-connection) @@ -44,11 +43,14 @@ testImplementation "io.goodforgod:testcontainers-extensions-mockserver:0.9.6" Test with container start in `PER_RUN` mode will look like: ```java -@TestcontainersMockserver(mode = ContainerMode.PER_RUN) +@TestcontainersMockServer(mode = ContainerMode.PER_RUN) class ExampleTests { + @ContainerMockServerConnection + private MockServerConnection connection; + @Test - void test(@ContainerMockserverConnection MockserverConnection connection) { + void test() { connection.client().when(HttpRequest.request() .withMethod("GET") .withPath("/get")) @@ -59,48 +61,32 @@ class ExampleTests { } ``` -## Container - -Library provides special `MockServerContainerExtra` with ability for migration and connection. -It can be used with [Testcontainers JUnit Extension](https://java.testcontainers.org/test_framework_integration/junit_5/). - -```java -class ExampleTests { - - @Test - void test() { - try (var container = new MockServerContainerExtra(DockerImageName.parse("mockserver/mockserver:5.15.0"))) { - container.start(); - } - } -} -``` - -### Container Connection +## Connection -`MockserverConnection` provides connection parameters, useful asserts, checks, etc. for easier testing. +`MockServerConnection` is an abstraction with asserting data in database container and easily manipulate container connection settings. +You can inject connection via `@ConnectionMockServer` as field or method argument or manually create it from container or manual settings. ```java class ExampleTests { + @ContainerMockServerConnection + private MockServerConnection connection; + @Test void test() { - try (var container = new MockServerContainerExtra(DockerImageName.parse("mockserver/mockserver:5.15.0"))) { - container.start(); - container.connection().client().when(HttpRequest.request() - .withMethod("GET") - .withPath("/get")) - .respond(HttpResponse.response() - .withStatusCode(200) - .withBody("OK")); - } + connection().client().when(HttpRequest.request() + .withMethod("GET") + .withPath("/get")) + .respond(HttpResponse.response() + .withStatusCode(200) + .withBody("OK")); } } ``` ## Annotation -`@TestcontainersMockserver` - allow **automatically start container** with specified image in different modes without the need to configure it. +`@TestcontainersMockServer` - allow **automatically start container** with specified image in different modes without the need to configure it. Available containers modes: @@ -110,11 +96,11 @@ Available containers modes: Simple example on how to start container per class, **no need to configure** container: ```java -@TestcontainersMockserver(mode = ContainerMode.PER_CLASS) +@TestcontainersMockServer(mode = ContainerMode.PER_CLASS) class ExampleTests { @Test - void test(@ContainerMockserverConnection MockserverConnection connection) { + void test(@ContainerMockServerConnection MockServerConnection connection) { assertNotNull(connection); } } @@ -126,7 +112,7 @@ It is possible to customize image with annotation `image` parameter. Image also can be provided from environment variable: ```java -@TestcontainersMockserver(image = "${MY_IMAGE_ENV|mockserver/mockserver:5.15.0}") +@TestcontainersMockServer(image = "${MY_IMAGE_ENV|mockserver/mockserver:5.15.0}") class ExampleTests { @Test @@ -144,22 +130,19 @@ Image syntax: ### Manual Container -When you need to **manually configure container** with specific options, you can provide such container as instance that will be used by `@TestcontainersMockserver`, -this can be done using `@ContainerMockserver` annotation for container. +When you need to **manually configure container** with specific options, you can provide such container as instance that will be used by `@TestcontainersMockServer`, +this can be done using `@ContainerMockServer` annotation for container. Example: ```java -@TestcontainersMockserver(mode = ContainerMode.PER_CLASS) +@TestcontainersMockServer(mode = ContainerMode.PER_CLASS) class ExampleTests { - @ContainerMockserver - private static final MockServerContainer container = new MockServerContainer() - .withNetworkAliases("mymockserver") - .withLogConsumer(new Slf4jLogConsumer(LoggerFactory.getLogger(MockServerContainer.class))) - .withNetwork(Network.SHARED); + @ContainerMockServer + private static final MockServerContainer container = new MockServerContainer().withNetworkAliases("mymockserver"); @Test - void test(@ContainerMockserverConnection MockserverConnection connection) { + void test(@ContainerMockServerConnection MockServerConnection connection) { assertEquals("mymockserver", connection.paramsInNetwork().get().host()); } } @@ -169,7 +152,7 @@ class ExampleTests { In case you want to enable [Network.SHARED](https://java.testcontainers.org/features/networking/) for containers you can do this using `network` & `shared` parameter in annotation: ```java -@TestcontainersMockserver(network = @Network(shared = true)) +@TestcontainersMockServer(network = @Network(shared = true)) class ExampleTests { @Test @@ -186,7 +169,7 @@ Alias can be extracted from environment variable also or default value can be pr In case specified environment variable is missing `default alias` will be created: ```java -@TestcontainersMockserver(network = @Network(alias = "${MY_ALIAS_ENV|my_default_alias}")) +@TestcontainersMockServer(network = @Network(alias = "${MY_ALIAS_ENV|my_default_alias}")) class ExampleTests { @Test @@ -204,19 +187,19 @@ Image syntax: ### Annotation Connection -`MockserverConnection` - can be injected to field or method parameter and used to communicate with running container via `@ContainerMockserverConnection` annotation. -`MockserverConnection` provides connection parameters, useful asserts, checks, etc. for easier testing. +`MockServerConnection` - can be injected to field or method parameter and used to communicate with running container via `@ContainerMockServerConnection` annotation. +`MockServerConnection` provides connection parameters, useful asserts, checks, etc. for easier testing. Example: ```java -@TestcontainersMockserver(mode = ContainerMode.PER_CLASS, image = "mockserver/mockserver:5.15.0") +@TestcontainersMockServer(mode = ContainerMode.PER_CLASS, image = "mockserver/mockserver:5.15.0") class ExampleTests { - @ContainerMockserverConnection - private MockserverConnection connectionInField; + @ContainerMockServerConnection + private MockServerConnection connection; @Test - void test(@ContainerMockserverConnection MockserverConnection connection) { + void test() { connection.client().when(HttpRequest.request() .withMethod("GET") .withPath("/get")) @@ -229,12 +212,12 @@ class ExampleTests { ### External Connection -In case you want to use some external Mockserver instance that is running in CI or other place for tests (due to docker limitations or other), -you can use special *environment variables* and extension will use them to propagate connection and no Mockserver containers will be running in such case. +In case you want to use some external MockServer instance that is running in CI or other place for tests (due to docker limitations or other), +you can use special *environment variables* and extension will use them to propagate connection and no MockServer containers will be running in such case. Special environment variables: -- `EXTERNAL_TEST_MOCKSERVER_HOST` - Mockserver instance host. -- `EXTERNAL_TEST_MOCKSERVER_PORT` - Mockserver instance port. +- `EXTERNAL_TEST_MOCKSERVER_HOST` - MockServer instance host. +- `EXTERNAL_TEST_MOCKSERVER_PORT` - MockServer instance port. ## License diff --git a/mockserver/src/main/java/io/goodforgod/testcontainers/extensions/mockserver/ContainerMockserverConnection.java b/mockserver/src/main/java/io/goodforgod/testcontainers/extensions/mockserver/ConnectionMockServer.java similarity index 77% rename from mockserver/src/main/java/io/goodforgod/testcontainers/extensions/mockserver/ContainerMockserverConnection.java rename to mockserver/src/main/java/io/goodforgod/testcontainers/extensions/mockserver/ConnectionMockServer.java index 1bd4cef..cc1b4fd 100644 --- a/mockserver/src/main/java/io/goodforgod/testcontainers/extensions/mockserver/ContainerMockserverConnection.java +++ b/mockserver/src/main/java/io/goodforgod/testcontainers/extensions/mockserver/ConnectionMockServer.java @@ -3,11 +3,11 @@ import java.lang.annotation.*; /** - * Indicates that annotated field or parameter should be injected with {@link MockserverConnection} + * Indicates that annotated field or parameter should be injected with {@link MockServerConnection} * value * of current active container */ @Documented @Target({ ElementType.FIELD, ElementType.PARAMETER }) @Retention(RetentionPolicy.RUNTIME) -public @interface ContainerMockserverConnection {} +public @interface ConnectionMockServer {} diff --git a/mockserver/src/main/java/io/goodforgod/testcontainers/extensions/mockserver/ContainerMockserver.java b/mockserver/src/main/java/io/goodforgod/testcontainers/extensions/mockserver/ContainerMockServer.java similarity index 60% rename from mockserver/src/main/java/io/goodforgod/testcontainers/extensions/mockserver/ContainerMockserver.java rename to mockserver/src/main/java/io/goodforgod/testcontainers/extensions/mockserver/ContainerMockServer.java index 43d36b0..8b35819 100644 --- a/mockserver/src/main/java/io/goodforgod/testcontainers/extensions/mockserver/ContainerMockserver.java +++ b/mockserver/src/main/java/io/goodforgod/testcontainers/extensions/mockserver/ContainerMockServer.java @@ -1,12 +1,13 @@ package io.goodforgod.testcontainers.extensions.mockserver; import java.lang.annotation.*; +import org.testcontainers.containers.MockServerContainer; /** - * Indicates that annotated field containers {@link MockServerContainerExtra} instance - * that should be used by {@link TestcontainersMockserver} rather than creating default container + * Indicates that annotated field containers {@link MockServerContainer} instance + * that should be used by {@link TestcontainersMockServer} rather than creating default container */ @Documented @Target({ ElementType.FIELD }) @Retention(RetentionPolicy.RUNTIME) -public @interface ContainerMockserver {} +public @interface ContainerMockServer {} diff --git a/mockserver/src/main/java/io/goodforgod/testcontainers/extensions/mockserver/MockserverConnection.java b/mockserver/src/main/java/io/goodforgod/testcontainers/extensions/mockserver/MockServerConnection.java similarity index 71% rename from mockserver/src/main/java/io/goodforgod/testcontainers/extensions/mockserver/MockserverConnection.java rename to mockserver/src/main/java/io/goodforgod/testcontainers/extensions/mockserver/MockServerConnection.java index 22365dc..1d19ad9 100644 --- a/mockserver/src/main/java/io/goodforgod/testcontainers/extensions/mockserver/MockserverConnection.java +++ b/mockserver/src/main/java/io/goodforgod/testcontainers/extensions/mockserver/MockServerConnection.java @@ -4,15 +4,15 @@ import java.util.Optional; import org.jetbrains.annotations.NotNull; import org.mockserver.client.MockServerClient; +import org.testcontainers.containers.MockServerContainer; /** - * Describes active Mockserver connection of currently running - * {@link MockServerContainerExtra} + * Describes active MockServer connection of currently running {@link MockServerContainer} */ -public interface MockserverConnection { +public interface MockServerConnection { /** - * Mockserver connection parameters + * MockServer connection parameters */ interface Params { @@ -33,7 +33,7 @@ interface Params { /** * @return connection parameters inside docker network, can be useful when one container require - * params to connect to Mockserver container inside docker network + * params to connect to MockServer container inside docker network */ @NotNull Optional paramsInNetwork(); diff --git a/mockserver/src/main/java/io/goodforgod/testcontainers/extensions/mockserver/MockserverConnectionImpl.java b/mockserver/src/main/java/io/goodforgod/testcontainers/extensions/mockserver/MockServerConnectionImpl.java similarity index 83% rename from mockserver/src/main/java/io/goodforgod/testcontainers/extensions/mockserver/MockserverConnectionImpl.java rename to mockserver/src/main/java/io/goodforgod/testcontainers/extensions/mockserver/MockServerConnectionImpl.java index c6a9515..d405369 100644 --- a/mockserver/src/main/java/io/goodforgod/testcontainers/extensions/mockserver/MockserverConnectionImpl.java +++ b/mockserver/src/main/java/io/goodforgod/testcontainers/extensions/mockserver/MockServerConnectionImpl.java @@ -8,7 +8,7 @@ import org.mockserver.client.MockServerClient; @Internal -final class MockserverConnectionImpl implements MockserverConnection { +class MockServerConnectionImpl implements MockServerConnection { private static final class ParamsImpl implements Params { @@ -46,13 +46,13 @@ public String toString() { private final MockServerClient client; - MockserverConnectionImpl(Params params, Params network) { + MockServerConnectionImpl(Params params, Params network) { this.params = params; this.network = network; this.client = new MockServerClient(params.host(), params.port()); } - static MockserverConnection forContainer(String host, int port, String hostInNetwork, Integer portInNetwork) { + static MockServerConnection forContainer(String host, int port, String hostInNetwork, Integer portInNetwork) { var params = new ParamsImpl(host, port); final Params network; if (hostInNetwork == null) { @@ -61,12 +61,12 @@ static MockserverConnection forContainer(String host, int port, String hostInNet network = new ParamsImpl(hostInNetwork, portInNetwork); } - return new MockserverConnectionImpl(params, network); + return new MockServerConnectionImpl(params, network); } - static MockserverConnection forExternal(String host, int port) { + static MockServerConnection forExternal(String host, int port) { var params = new ParamsImpl(host, port); - return new MockserverConnectionImpl(params, null); + return new MockServerConnectionImpl(params, null); } @Override @@ -84,7 +84,7 @@ static MockserverConnection forExternal(String host, int port) { return client; } - void close() { + void stop() { client.close(); } @@ -94,7 +94,7 @@ public boolean equals(Object o) { return true; if (o == null || getClass() != o.getClass()) return false; - MockserverConnectionImpl that = (MockserverConnectionImpl) o; + MockServerConnectionImpl that = (MockServerConnectionImpl) o; return Objects.equals(params, that.params) && Objects.equals(network, that.network); } diff --git a/mockserver/src/main/java/io/goodforgod/testcontainers/extensions/mockserver/MockServerContainerExtra.java b/mockserver/src/main/java/io/goodforgod/testcontainers/extensions/mockserver/MockServerContainerExtra.java deleted file mode 100644 index 28777cb..0000000 --- a/mockserver/src/main/java/io/goodforgod/testcontainers/extensions/mockserver/MockServerContainerExtra.java +++ /dev/null @@ -1,83 +0,0 @@ -package io.goodforgod.testcontainers.extensions.mockserver; - -import java.time.Duration; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import org.jetbrains.annotations.NotNull; -import org.slf4j.LoggerFactory; -import org.testcontainers.containers.MockServerContainer; -import org.testcontainers.containers.output.Slf4jLogConsumer; -import org.testcontainers.utility.DockerImageName; - -public class MockServerContainerExtra extends MockServerContainer { - - private static final String EXTERNAL_TEST_MOCKSERVER_HOST = "EXTERNAL_TEST_MOCKSERVER_HOST"; - private static final String EXTERNAL_TEST_MOCKSERVER_PORT = "EXTERNAL_TEST_MOCKSERVER_PORT"; - - private volatile MockserverConnectionImpl connection; - - public MockServerContainerExtra(String dockerImageName) { - this(DockerImageName.parse(dockerImageName)); - } - - public MockServerContainerExtra(DockerImageName dockerImageName) { - super(dockerImageName); - - final String alias = "mockserver-" + System.currentTimeMillis(); - this.withLogConsumer(new Slf4jLogConsumer(LoggerFactory.getLogger(MockServerContainerExtra.class)) - .withMdc("image", dockerImageName.asCanonicalNameString()) - .withMdc("alias", alias)); - this.withStartupTimeout(Duration.ofMinutes(5)); - this.setNetworkAliases(new ArrayList<>(List.of(alias))); - } - - @NotNull - public MockserverConnection connection() { - if (connection == null) { - final Optional connectionExternal = getConnectionExternal(); - if (connectionExternal.isEmpty() && !isRunning()) { - throw new IllegalStateException("MockserverConnection can't be create for container that is not running"); - } - - final MockserverConnection jdbcConnection = connectionExternal.orElseGet(() -> { - final String alias = getNetworkAliases().get(getNetworkAliases().size() - 1); - return MockserverConnectionImpl.forContainer(getHost(), - getMappedPort(MockServerContainer.PORT), - alias, - MockServerContainer.PORT); - }); - - this.connection = (MockserverConnectionImpl) jdbcConnection; - } - - return connection; - } - - @Override - public void start() { - final Optional connectionExternal = getConnectionExternal(); - if (connectionExternal.isEmpty()) { - super.start(); - } - } - - @Override - public void stop() { - connection.close(); - connection = null; - super.stop(); - } - - @NotNull - private static Optional getConnectionExternal() { - var host = System.getenv(EXTERNAL_TEST_MOCKSERVER_HOST); - var port = System.getenv(EXTERNAL_TEST_MOCKSERVER_PORT); - - if (host != null && port != null) { - return Optional.of(MockserverConnectionImpl.forExternal(host, Integer.parseInt(port))); - } else { - return Optional.empty(); - } - } -} diff --git a/mockserver/src/main/java/io/goodforgod/testcontainers/extensions/mockserver/MockServerContext.java b/mockserver/src/main/java/io/goodforgod/testcontainers/extensions/mockserver/MockServerContext.java new file mode 100644 index 0000000..2ff8832 --- /dev/null +++ b/mockserver/src/main/java/io/goodforgod/testcontainers/extensions/mockserver/MockServerContext.java @@ -0,0 +1,78 @@ +package io.goodforgod.testcontainers.extensions.mockserver; + +import io.goodforgod.testcontainers.extensions.ContainerContext; +import java.util.Optional; +import org.jetbrains.annotations.ApiStatus.Internal; +import org.jetbrains.annotations.NotNull; +import org.testcontainers.containers.MockServerContainer; + +@Internal +final class MockServerContext implements ContainerContext { + + private static final String EXTERNAL_TEST_MOCKSERVER_HOST = "EXTERNAL_TEST_MOCKSERVER_HOST"; + private static final String EXTERNAL_TEST_MOCKSERVER_PORT = "EXTERNAL_TEST_MOCKSERVER_PORT"; + + private volatile MockServerConnectionImpl connection; + + private final MockServerContainer container; + + MockServerContext(MockServerContainer container) { + this.container = container; + } + + @NotNull + public MockServerConnection connection() { + if (connection == null) { + final Optional connectionExternal = getConnectionExternal(); + if (connectionExternal.isEmpty() && !container.isRunning()) { + throw new IllegalStateException("MockServerConnection can't be create for container that is not running"); + } + + final MockServerConnection jdbcConnection = connectionExternal.orElseGet(() -> { + final String alias = container.getNetworkAliases().get(container.getNetworkAliases().size() - 1); + return MockServerConnectionImpl.forContainer(container.getHost(), + container.getMappedPort(MockServerContainer.PORT), + alias, + MockServerContainer.PORT); + }); + + this.connection = (MockServerConnectionImpl) jdbcConnection; + } + + return connection; + } + + @Override + public void start() { + final Optional connectionExternal = getConnectionExternal(); + if (connectionExternal.isEmpty()) { + container.start(); + } + } + + @Override + public void stop() { + if (connection != null) { + connection.stop(); + connection = null; + } + container.stop(); + } + + @NotNull + private static Optional getConnectionExternal() { + var host = System.getenv(EXTERNAL_TEST_MOCKSERVER_HOST); + var port = System.getenv(EXTERNAL_TEST_MOCKSERVER_PORT); + + if (host != null && port != null) { + return Optional.of(MockServerConnectionImpl.forExternal(host, Integer.parseInt(port))); + } else { + return Optional.empty(); + } + } + + @Override + public String toString() { + return container.getDockerImageName(); + } +} diff --git a/mockserver/src/main/java/io/goodforgod/testcontainers/extensions/mockserver/MockserverMetadata.java b/mockserver/src/main/java/io/goodforgod/testcontainers/extensions/mockserver/MockServerMetadata.java similarity index 53% rename from mockserver/src/main/java/io/goodforgod/testcontainers/extensions/mockserver/MockserverMetadata.java rename to mockserver/src/main/java/io/goodforgod/testcontainers/extensions/mockserver/MockServerMetadata.java index 9e43989..a563832 100644 --- a/mockserver/src/main/java/io/goodforgod/testcontainers/extensions/mockserver/MockserverMetadata.java +++ b/mockserver/src/main/java/io/goodforgod/testcontainers/extensions/mockserver/MockServerMetadata.java @@ -3,17 +3,11 @@ import io.goodforgod.testcontainers.extensions.AbstractContainerMetadata; import io.goodforgod.testcontainers.extensions.ContainerMode; import org.jetbrains.annotations.ApiStatus.Internal; -import org.jetbrains.annotations.NotNull; @Internal -final class MockserverMetadata extends AbstractContainerMetadata { +final class MockServerMetadata extends AbstractContainerMetadata { - MockserverMetadata(boolean network, String alias, String image, ContainerMode runMode) { + MockServerMetadata(boolean network, String alias, String image, ContainerMode runMode) { super(network, alias, image, runMode); } - - @Override - public @NotNull String networkAliasDefault() { - return "mockserver-" + System.currentTimeMillis(); - } } diff --git a/mockserver/src/main/java/io/goodforgod/testcontainers/extensions/mockserver/TestcontainersMockserver.java b/mockserver/src/main/java/io/goodforgod/testcontainers/extensions/mockserver/TestcontainersMockServer.java similarity index 77% rename from mockserver/src/main/java/io/goodforgod/testcontainers/extensions/mockserver/TestcontainersMockserver.java rename to mockserver/src/main/java/io/goodforgod/testcontainers/extensions/mockserver/TestcontainersMockServer.java index f0a7573..fe6a379 100644 --- a/mockserver/src/main/java/io/goodforgod/testcontainers/extensions/mockserver/TestcontainersMockserver.java +++ b/mockserver/src/main/java/io/goodforgod/testcontainers/extensions/mockserver/TestcontainersMockServer.java @@ -5,28 +5,27 @@ import java.lang.annotation.*; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.extension.ExtendWith; +import org.testcontainers.containers.MockServerContainer; /** - * Extension that is running {@link MockServerContainerExtra} for tests in different modes with + * Extension that is running {@link MockServerContainer} for tests in different modes with * database * schema migration support between test executions */ @Order(Order.DEFAULT - 100) // Run before other extensions -@ExtendWith(TestcontainersMockserverExtension.class) +@ExtendWith(TestcontainersMockServerExtension.class) @Documented @Target({ ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) -public @interface TestcontainersMockserver { +public @interface TestcontainersMockServer { /** - * @see TestcontainersMockserverExtension#getContainerDefault(MockserverMetadata) - * @return Mockserver image + * @return MockServer image *

* 1) Image can have static value: "mockserver/mockserver:5.15.0" * 2) Image can be provided via environment variable using syntax: "${MY_IMAGE_ENV}" * 3) Image environment variable can have default value if empty using syntax: * "${MY_IMAGE_ENV|mockserver/mockserver:5.15.0}" - *

*/ String image() default "mockserver/mockserver:5.15.0"; diff --git a/mockserver/src/main/java/io/goodforgod/testcontainers/extensions/mockserver/TestcontainersMockServerExtension.java b/mockserver/src/main/java/io/goodforgod/testcontainers/extensions/mockserver/TestcontainersMockServerExtension.java new file mode 100644 index 0000000..7f0f9ea --- /dev/null +++ b/mockserver/src/main/java/io/goodforgod/testcontainers/extensions/mockserver/TestcontainersMockServerExtension.java @@ -0,0 +1,86 @@ +package io.goodforgod.testcontainers.extensions.mockserver; + +import io.goodforgod.testcontainers.extensions.AbstractTestcontainersExtension; +import io.goodforgod.testcontainers.extensions.ContainerContext; +import java.lang.annotation.Annotation; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import org.jetbrains.annotations.ApiStatus.Internal; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.MockServerContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.containers.output.Slf4jLogConsumer; +import org.testcontainers.utility.DockerImageName; + +@Internal +class TestcontainersMockServerExtension extends + AbstractTestcontainersExtension { + + private static final ExtensionContext.Namespace NAMESPACE = ExtensionContext.Namespace + .create(TestcontainersMockServerExtension.class); + + protected Class getContainerType() { + return MockServerContainer.class; + } + + protected Class getContainerAnnotation() { + return ContainerMockServer.class; + } + + protected Class getConnectionAnnotation() { + return ConnectionMockServer.class; + } + + @Override + protected Class getConnectionType() { + return MockServerConnection.class; + } + + @Override + protected ExtensionContext.Namespace getNamespace() { + return NAMESPACE; + } + + @Override + protected MockServerContainer createContainerDefault(MockServerMetadata metadata) { + var image = DockerImageName.parse(metadata.image()) + .asCompatibleSubstituteFor(DockerImageName.parse("mockserver/mockserver")); + + final MockServerContainer container = new MockServerContainer(image); + final String alias = Optional.ofNullable(metadata.networkAlias()) + .orElseGet(() -> "mockserver-" + System.currentTimeMillis()); + container.withLogConsumer(new Slf4jLogConsumer(LoggerFactory.getLogger(MockServerContainer.class)) + .withMdc("image", image.asCanonicalNameString()) + .withMdc("alias", alias)); + container.withStartupTimeout(Duration.ofMinutes(5)); + container.setNetworkAliases(new ArrayList<>(List.of(alias))); + if (metadata.networkShared()) { + container.withNetwork(Network.SHARED); + } + + return container; + } + + @Override + protected ContainerContext createContainerContext(MockServerContainer container) { + return new MockServerContext(container); + } + + @NotNull + protected Optional findMetadata(@NotNull ExtensionContext context) { + return findAnnotation(TestcontainersMockServer.class, context) + .map(a -> new MockServerMetadata(a.network().shared(), a.network().alias(), a.image(), a.mode())); + } + + @Override + public void beforeEach(ExtensionContext context) { + super.beforeEach(context); + + var connection = getContainerContext(context).connection(); + connection.client().reset(); + } +} diff --git a/mockserver/src/main/java/io/goodforgod/testcontainers/extensions/mockserver/TestcontainersMockserverExtension.java b/mockserver/src/main/java/io/goodforgod/testcontainers/extensions/mockserver/TestcontainersMockserverExtension.java deleted file mode 100644 index a7e6fc5..0000000 --- a/mockserver/src/main/java/io/goodforgod/testcontainers/extensions/mockserver/TestcontainersMockserverExtension.java +++ /dev/null @@ -1,76 +0,0 @@ -package io.goodforgod.testcontainers.extensions.mockserver; - -import io.goodforgod.testcontainers.extensions.AbstractTestcontainersExtension; -import java.lang.annotation.Annotation; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import org.jetbrains.annotations.ApiStatus.Internal; -import org.jetbrains.annotations.NotNull; -import org.junit.jupiter.api.extension.ExtensionContext; -import org.testcontainers.containers.Network; -import org.testcontainers.utility.DockerImageName; - -@Internal -class TestcontainersMockserverExtension extends - AbstractTestcontainersExtension { - - private static final ExtensionContext.Namespace NAMESPACE = ExtensionContext.Namespace - .create(TestcontainersMockserverExtension.class); - - protected Class getContainerType() { - return MockServerContainerExtra.class; - } - - protected Class getContainerAnnotation() { - return ContainerMockserver.class; - } - - protected Class getConnectionAnnotation() { - return ContainerMockserverConnection.class; - } - - @Override - protected Class getConnectionType() { - return MockserverConnection.class; - } - - @Override - protected MockServerContainerExtra getContainerDefault(MockserverMetadata metadata) { - var dockerImage = DockerImageName.parse(metadata.image()) - .asCompatibleSubstituteFor(DockerImageName.parse("mockserver/mockserver")); - - var container = new MockServerContainerExtra(dockerImage); - - container.setNetworkAliases(new ArrayList<>(List.of(metadata.networkAliasOrDefault()))); - if (metadata.networkShared()) { - container.withNetwork(Network.SHARED); - } - - return container; - } - - @Override - protected ExtensionContext.Namespace getNamespace() { - return NAMESPACE; - } - - @NotNull - protected Optional findMetadata(@NotNull ExtensionContext context) { - return findAnnotation(TestcontainersMockserver.class, context) - .map(a -> new MockserverMetadata(a.network().shared(), a.network().alias(), a.image(), a.mode())); - } - - @NotNull - protected MockserverConnection getConnectionForContainer(MockserverMetadata metadata, MockServerContainerExtra container) { - return container.connection(); - } - - @Override - public void beforeEach(ExtensionContext context) { - super.beforeEach(context); - - var connection = getConnectionCurrent(context); - connection.client().reset(); - } -} diff --git a/mockserver/src/test/java/io/goodforgod/testcontainers/extensions/mockserver/MockserverConnectionAssertsTests.java b/mockserver/src/test/java/io/goodforgod/testcontainers/extensions/mockserver/MockServerConnectionAssertsTests.java similarity index 83% rename from mockserver/src/test/java/io/goodforgod/testcontainers/extensions/mockserver/MockserverConnectionAssertsTests.java rename to mockserver/src/test/java/io/goodforgod/testcontainers/extensions/mockserver/MockServerConnectionAssertsTests.java index 28ea1d6..ab292f0 100644 --- a/mockserver/src/test/java/io/goodforgod/testcontainers/extensions/mockserver/MockserverConnectionAssertsTests.java +++ b/mockserver/src/test/java/io/goodforgod/testcontainers/extensions/mockserver/MockServerConnectionAssertsTests.java @@ -5,11 +5,11 @@ import org.mockserver.model.HttpRequest; import org.mockserver.model.HttpResponse; -@TestcontainersMockserver(mode = ContainerMode.PER_CLASS) -class MockserverConnectionAssertsTests { +@TestcontainersMockServer(mode = ContainerMode.PER_CLASS) +class MockServerConnectionAssertsTests { - @ContainerMockserverConnection - private MockserverConnection connection; + @ConnectionMockServer + private MockServerConnection connection; @Test void assertCountsAtLeastWhenEquals() { diff --git a/mysql/README.md b/mysql/README.md index 21c1060..e94bcfa 100644 --- a/mysql/README.md +++ b/mysql/README.md @@ -18,7 +18,7 @@ Features: **Gradle** ```groovy -testImplementation "io.goodforgod:testcontainers-extensions-mysql:0.9.6" +testImplementation "io.goodforgod:testcontainers-extensions-mysql:0.10.0" ``` **Maven** @@ -26,7 +26,7 @@ testImplementation "io.goodforgod:testcontainers-extensions-mysql:0.9.6" io.goodforgod testcontainers-extensions-mysql - 0.9.6 + 0.10.0 test ``` @@ -52,9 +52,8 @@ testRuntimeOnly "mysql:mysql-connector-java:8.0.33" ## Content - [Usage](#usage) -- [Container](#container) - - [Connection](#container-connection) - - [Migration](#container-migration) +- [Connection](#connection) + - [Migration](#connection-migration) - [Annotation](#annotation) - [Manual Container](#manual-container) - [Connection](#annotation-connection) @@ -66,15 +65,18 @@ testRuntimeOnly "mysql:mysql-connector-java:8.0.33" Test with container start in `PER_RUN` mode and migration per method will look like: ```java -@TestcontainersMysql(mode = ContainerMode.PER_RUN, +@TestcontainersMySQL(mode = ContainerMode.PER_RUN, migration = @Migration( engine = Migration.Engines.FLYWAY, apply = Migration.Mode.PER_METHOD, drop = Migration.Mode.PER_METHOD)) class ExampleTests { + @ConnectionMySQL + private JdbcConnection connection; + @Test - void test(@ContainerMysqlConnection JdbcConnection connection) { + void test() { connection.execute("INSERT INTO users VALUES(1);"); var usersFound = connection.queryMany("SELECT * FROM users;", r -> r.getInt(1)); assertEquals(1, usersFound.size()); @@ -82,56 +84,50 @@ class ExampleTests { } ``` -## Container +## Connection -Library provides special `MySQLContainerExtra` with ability for migration and connection. -It can be used with [Testcontainers JUnit Extension](https://java.testcontainers.org/test_framework_integration/junit_5/). +`JdbcConnection` is an abstraction with asserting data in database container and easily manipulate container connection settings. +You can inject connection via `@ConnectionOracle` as field or method argument or manually create it from container or manual settings. ```java class ExampleTests { + private static final MySQLContainer container = new MySQLContainer<>(); + @Test void test() { - try (var container = new MySQLContainerExtra<>(DockerImageName.parse("mysql:8.0-debian"))) { - container.start(); - } + container.start(); + JdbcConnection connection = JdbcConnection.forContainer(container); + connection.execute("INSERT INTO users VALUES(1);"); } } ``` -### Container Connection +### Connection Migration -`JdbcConnection` provides connection parameters, useful asserts, checks, etc. for easier testing. +`Migrations` allow easily migrate database between test executions and drop after tests. +You can migrate container via `@TestcontainersOracle#migration` annotation parameter or manually using `JdbcConnection`. ```java +@TestcontainersOracle class ExampleTests { - @Test - void test() { - try (var container = new MySQLContainerExtra<>(DockerImageName.parse("mysql:8.0-debian"))) { - container.start(); - container.connection().assertQueriesNone("SELECT * FROM users;"); + @Test + void test(@ConnectionOracle JdbcConnection connection) { + connection.migrationEngine(Migration.Engines.FLYWAY).apply("db/migration"); + connection.execute("INSERT INTO users VALUES(1);"); + connection.migrationEngine(Migration.Engines.FLYWAY).drop("db/migration"); } - } } ``` -### Container Migration - -`Migrations` allow easily migrate database between test executions and drop after tests. - -Annotation parameters: -- `engine` - to use for migration. -- `apply` - parameter configures migration mode. -- `drop` - configures when to reset/drop/clear database. - Available migration engines: - [Flyway](https://documentation.red-gate.com/fd/cockroachdb-184127591.html) - [Liquibase](https://www.liquibase.com/databases/cockroachdb-2) ## Annotation -`@TestcontainersMysql` - allow **automatically start container** with specified image in different modes without the need to configure it. +`@TestcontainersMySQL` - allow **automatically start container** with specified image in different modes without the need to configure it. Available containers modes: @@ -141,11 +137,11 @@ Available containers modes: Simple example on how to start container per class, **no need to configure** container: ```java -@TestcontainersMysql(mode = ContainerMode.PER_CLASS) +@TestcontainersMySQL(mode = ContainerMode.PER_CLASS) class ExampleTests { @Test - void test(@ContainerMysqlConnection JdbcConnection connection) { + void test(@ConnectionMySQL JdbcConnection connection) { assertNotNull(connection); } } @@ -157,7 +153,7 @@ It is possible to customize image with annotation `image` parameter. Image also can be provided from environment variable: ```java -@TestcontainersMysql(image = "${MY_IMAGE_ENV|mysql:8.0-debian}") +@TestcontainersMySQL(image = "${MY_IMAGE_ENV|mysql:8.0-debian}") class ExampleTests { @Test @@ -175,22 +171,21 @@ Image syntax: ### Manual Container -When you need to **manually configure container** with specific options, you can provide such container as instance that will be used by `@TestcontainersMysql`, -this can be done using `@ContainerMysql` annotation for container. +When you need to **manually configure container** with specific options, you can provide such container as instance that will be used by `@TestcontainersMySQL`, +this can be done using `@ContainerMySQL` annotation for container. -Example: ```java -@TestcontainersMysql(mode = ContainerMode.PER_CLASS) +@TestcontainersMySQL(mode = ContainerMode.PER_CLASS) class ExampleTests { - @ContainerMysql + @ContainerMySQL private static final MySQLContainer container = new MySQLContainer<>() .withDatabaseName("user") .withUsername("user") .withPassword("user"); @Test - void test(@ContainerMysqlConnection JdbcConnection connection) { + void test(@ConnectionMySQL JdbcConnection connection) { assertEquals("user", connection.params().database()); assertEquals("user", connection.params().username()); assertEquals("user", connection.params().password()); @@ -202,7 +197,7 @@ class ExampleTests { In case you want to enable [Network.SHARED](https://java.testcontainers.org/features/networking/) for containers you can do this using `network` & `shared` parameter in annotation: ```java -@TestcontainersMysql(network = @Network(shared = true)) +@TestcontainersMySQL(network = @Network(shared = true)) class ExampleTests { @Test @@ -219,7 +214,7 @@ Alias can be extracted from environment variable also or default value can be pr In case specified environment variable is missing `default alias` will be created: ```java -@TestcontainersMysql(network = @Network(alias = "${MY_ALIAS_ENV|my_default_alias}")) +@TestcontainersMySQL(network = @Network(alias = "${MY_ALIAS_ENV|my_default_alias}")) class ExampleTests { @Test @@ -237,19 +232,18 @@ Image syntax: ### Annotation Connection -`JdbcConnection` - can be injected to field or method parameter and used to communicate with running container via `@ContainerMysqlConnection` annotation. +`JdbcConnection` - can be injected to field or method parameter and used to communicate with running container via `@ConnectionMySQL` annotation. `JdbcConnection` provides connection parameters, useful asserts, checks, etc. for easier testing. -Example: ```java -@TestcontainersMysql(mode = ContainerMode.PER_CLASS, image = "mysql:8.0-debian") +@TestcontainersMySQL(mode = ContainerMode.PER_CLASS, image = "mysql:8.0-debian") class ExampleTests { - @ContainerMysqlConnection - private JdbcConnection connectionInField; + @ConnectionMySQL + private JdbcConnection connection; @Test - void test(@ContainerMysqlConnection JdbcConnection connection) { + void test() { connection.execute("CREATE TABLE users (id INT NOT NULL PRIMARY KEY);"); connection.execute("INSERT INTO users VALUES(1);"); connection.assertInserted("INSERT INTO users VALUES(2);"); @@ -286,6 +280,7 @@ Annotation parameters: - `engine` - to use for migration. - `apply` - parameter configures migration mode. - `drop` - configures when to reset/drop/clear database. +- `locations` - configures locations where migrations are placed. Available migration engines: - [Flyway](https://documentation.red-gate.com/fd/mysql-184127601.html) @@ -301,7 +296,7 @@ CREATE TABLE IF NOT EXISTS users Test with container and migration per method will look like: ```java -@TestcontainersMysql(mode = ContainerMode.PER_CLASS, +@TestcontainersMySQL(mode = ContainerMode.PER_CLASS, migration = @Migration( engine = Migration.Engines.FLYWAY, apply = Migration.Mode.PER_METHOD, @@ -309,7 +304,7 @@ Test with container and migration per method will look like: class ExampleTests { @Test - void test(@ContainerMysqlConnection JdbcConnection connection) { + void test(@ConnectionMySQL JdbcConnection connection) { connection.execute("INSERT INTO users VALUES(1);"); var usersFound = connection.queryMany("SELECT * FROM users;", r -> r.getInt(1)); assertEquals(1, usersFound.size()); diff --git a/mariadb/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/ContainerMariadbConnection.java b/mysql/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/ConnectionMySQL.java similarity index 87% rename from mariadb/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/ContainerMariadbConnection.java rename to mysql/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/ConnectionMySQL.java index 365904f..60cf459 100644 --- a/mariadb/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/ContainerMariadbConnection.java +++ b/mysql/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/ConnectionMySQL.java @@ -9,4 +9,4 @@ @Documented @Target({ ElementType.FIELD, ElementType.PARAMETER }) @Retention(RetentionPolicy.RUNTIME) -public @interface ContainerMariadbConnection {} +public @interface ConnectionMySQL {} diff --git a/mysql/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/ContainerMySQL.java b/mysql/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/ContainerMySQL.java new file mode 100644 index 0000000..00cdcff --- /dev/null +++ b/mysql/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/ContainerMySQL.java @@ -0,0 +1,13 @@ +package io.goodforgod.testcontainers.extensions.jdbc; + +import java.lang.annotation.*; + +/** + * Indicates that annotated field containers {@link org.testcontainers.containers.MySQLContainer} + * instance + * that should be used by {@link TestcontainersMySQL} rather than creating default container + */ +@Documented +@Target({ ElementType.FIELD }) +@Retention(RetentionPolicy.RUNTIME) +public @interface ContainerMySQL {} diff --git a/mysql/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/ContainerMysql.java b/mysql/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/ContainerMysql.java deleted file mode 100644 index 9249514..0000000 --- a/mysql/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/ContainerMysql.java +++ /dev/null @@ -1,13 +0,0 @@ -package io.goodforgod.testcontainers.extensions.jdbc; - -import java.lang.annotation.*; -import org.testcontainers.containers.MySQLContainer; - -/** - * Indicates that annotated field containers {@link MySQLContainer} instance - * that should be used by {@link TestcontainersMysql} rather than creating default container - */ -@Documented -@Target({ ElementType.FIELD }) -@Retention(RetentionPolicy.RUNTIME) -public @interface ContainerMysql {} diff --git a/mysql/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/MySQLContainerExtra.java b/mysql/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/MySQLContainerExtra.java deleted file mode 100644 index c391bee..0000000 --- a/mysql/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/MySQLContainerExtra.java +++ /dev/null @@ -1,141 +0,0 @@ -package io.goodforgod.testcontainers.extensions.jdbc; - -import java.time.Duration; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import org.jetbrains.annotations.ApiStatus.Internal; -import org.jetbrains.annotations.NotNull; -import org.slf4j.LoggerFactory; -import org.testcontainers.containers.MySQLContainer; -import org.testcontainers.containers.output.Slf4jLogConsumer; -import org.testcontainers.containers.wait.strategy.Wait; -import org.testcontainers.utility.DockerImageName; - -public class MySQLContainerExtra> extends MySQLContainer { - - private static final String PROTOCOL = "mysql"; - private static final String DATABASE_NAME = "test"; - - private static final String EXTERNAL_TEST_MYSQL_JDBC_URL = "EXTERNAL_TEST_MYSQL_JDBC_URL"; - private static final String EXTERNAL_TEST_MYSQL_USERNAME = "EXTERNAL_TEST_MYSQL_USERNAME"; - private static final String EXTERNAL_TEST_MYSQL_PASSWORD = "EXTERNAL_TEST_MYSQL_PASSWORD"; - private static final String EXTERNAL_TEST_MYSQL_HOST = "EXTERNAL_TEST_MYSQL_HOST"; - private static final String EXTERNAL_TEST_MYSQL_PORT = "EXTERNAL_TEST_MYSQL_PORT"; - private static final String EXTERNAL_TEST_MYSQL_DATABASE = "EXTERNAL_TEST_MYSQL_DATABASE"; - - private volatile JdbcConnectionImpl connection; - private volatile FlywayJdbcMigrationEngine flywayJdbcMigrationEngine; - private volatile LiquibaseJdbcMigrationEngine liquibaseJdbcMigrationEngine; - - public MySQLContainerExtra(String dockerImageName) { - this(DockerImageName.parse(dockerImageName)); - } - - public MySQLContainerExtra(DockerImageName dockerImageName) { - super(dockerImageName); - - final String alias = "mysql-" + System.currentTimeMillis(); - - this.withDatabaseName(DATABASE_NAME); - this.withUsername("mysql"); - this.withPassword("mysql"); - this.withLogConsumer(new Slf4jLogConsumer(LoggerFactory.getLogger(MySQLContainerExtra.class)) - .withMdc("image", dockerImageName.asCanonicalNameString()) - .withMdc("alias", alias)); - this.waitingFor(Wait.forListeningPort()); - this.withStartupTimeout(Duration.ofMinutes(5)); - - this.setNetworkAliases(new ArrayList<>(List.of(alias))); - } - - @Internal - JdbcMigrationEngine getMigrationEngine(@NotNull Migration.Engines engine) { - if (engine == Migration.Engines.FLYWAY) { - if (flywayJdbcMigrationEngine == null) { - this.flywayJdbcMigrationEngine = new FlywayJdbcMigrationEngine(connection()); - } - return this.flywayJdbcMigrationEngine; - } else if (engine == Migration.Engines.LIQUIBASE) { - if (liquibaseJdbcMigrationEngine == null) { - this.liquibaseJdbcMigrationEngine = new LiquibaseJdbcMigrationEngine(connection()); - } - return this.liquibaseJdbcMigrationEngine; - } else { - throw new UnsupportedOperationException("Unsupported engine: " + engine); - } - } - - @NotNull - public JdbcConnection connection() { - if (connection == null) { - final Optional connectionExternal = getConnectionExternal(); - if (connectionExternal.isEmpty() && !isRunning()) { - throw new IllegalStateException("MySqlConnection can't be create for container that is not running"); - } - - final JdbcConnection jdbcConnection = connectionExternal.orElseGet(() -> { - final String alias = getNetworkAliases().get(getNetworkAliases().size() - 1); - return JdbcConnectionImpl.forJDBC(getJdbcUrl(), - getHost(), - getMappedPort(MySQLContainerExtra.MYSQL_PORT), - alias, - MySQLContainerExtra.MYSQL_PORT, - getDatabaseName(), - getUsername(), - getPassword()); - }); - - this.connection = (JdbcConnectionImpl) jdbcConnection; - } - - return connection; - } - - @Override - public void start() { - final Optional connectionExternal = getConnectionExternal(); - if (connectionExternal.isEmpty()) { - super.start(); - } - } - - @Override - public void stop() { - if (flywayJdbcMigrationEngine != null) { - flywayJdbcMigrationEngine.close(); - flywayJdbcMigrationEngine = null; - } - if (liquibaseJdbcMigrationEngine != null) { - liquibaseJdbcMigrationEngine.close(); - liquibaseJdbcMigrationEngine = null; - } - if (connection != null) { - connection.close(); - connection = null; - } - super.stop(); - } - - @NotNull - private static Optional getConnectionExternal() { - var url = System.getenv(EXTERNAL_TEST_MYSQL_JDBC_URL); - var host = System.getenv(EXTERNAL_TEST_MYSQL_HOST); - var port = System.getenv(EXTERNAL_TEST_MYSQL_PORT); - var user = System.getenv(EXTERNAL_TEST_MYSQL_USERNAME); - var password = System.getenv(EXTERNAL_TEST_MYSQL_PASSWORD); - var db = Optional.ofNullable(System.getenv(EXTERNAL_TEST_MYSQL_DATABASE)).orElse(DATABASE_NAME); - - if (url != null) { - if (host != null && port != null) { - return Optional.of(JdbcConnectionImpl.forJDBC(url, host, Integer.parseInt(port), null, null, db, user, password)); - } else { - return Optional.of(JdbcConnectionImpl.forExternal(url, user, password)); - } - } else if (host != null && port != null) { - return Optional.of(JdbcConnectionImpl.forProtocol(PROTOCOL, host, Integer.parseInt(port), db, user, password)); - } else { - return Optional.empty(); - } - } -} diff --git a/mysql/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/MySQLContext.java b/mysql/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/MySQLContext.java new file mode 100644 index 0000000..3bfe81d --- /dev/null +++ b/mysql/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/MySQLContext.java @@ -0,0 +1,99 @@ +package io.goodforgod.testcontainers.extensions.jdbc; + +import io.goodforgod.testcontainers.extensions.ContainerContext; +import java.util.Optional; +import org.jetbrains.annotations.ApiStatus.Internal; +import org.jetbrains.annotations.NotNull; +import org.testcontainers.containers.MySQLContainer; + +@Internal +final class MySQLContext implements ContainerContext { + + private static final String PROTOCOL = "mysql"; + static final String DATABASE_NAME = "test"; + + private static final String EXTERNAL_TEST_MYSQL_JDBC_URL = "EXTERNAL_TEST_MYSQL_JDBC_URL"; + private static final String EXTERNAL_TEST_MYSQL_USERNAME = "EXTERNAL_TEST_MYSQL_USERNAME"; + private static final String EXTERNAL_TEST_MYSQL_PASSWORD = "EXTERNAL_TEST_MYSQL_PASSWORD"; + private static final String EXTERNAL_TEST_MYSQL_HOST = "EXTERNAL_TEST_MYSQL_HOST"; + private static final String EXTERNAL_TEST_MYSQL_PORT = "EXTERNAL_TEST_MYSQL_PORT"; + private static final String EXTERNAL_TEST_MYSQL_DATABASE = "EXTERNAL_TEST_MYSQL_DATABASE"; + + private volatile JdbcConnectionImpl connection; + + private final MySQLContainer container; + + MySQLContext(MySQLContainer container) { + this.container = container; + } + + @NotNull + public JdbcConnection connection() { + if (connection == null) { + final Optional connectionExternal = getConnectionExternal(); + if (connectionExternal.isEmpty() && !container.isRunning()) { + throw new IllegalStateException("MysqlConnection can't be create for container that is not running"); + } + + final JdbcConnection containerConnection = connectionExternal.orElseGet(() -> { + final String alias = container.getNetworkAliases().get(container.getNetworkAliases().size() - 1); + return JdbcConnectionImpl.forJDBC(container.getJdbcUrl(), + container.getHost(), + container.getMappedPort(MySQLContainer.MYSQL_PORT), + alias, + MySQLContainer.MYSQL_PORT, + container.getDatabaseName(), + container.getUsername(), + container.getPassword()); + }); + + this.connection = (JdbcConnectionImpl) containerConnection; + } + + return connection; + } + + @Override + public void start() { + final Optional connectionExternal = getConnectionExternal(); + if (connectionExternal.isEmpty()) { + container.start(); + } + } + + @Override + public void stop() { + if (connection != null) { + connection.stop(); + connection = null; + } + container.stop(); + } + + @NotNull + private static Optional getConnectionExternal() { + var url = System.getenv(EXTERNAL_TEST_MYSQL_JDBC_URL); + var host = System.getenv(EXTERNAL_TEST_MYSQL_HOST); + var port = System.getenv(EXTERNAL_TEST_MYSQL_PORT); + var user = System.getenv(EXTERNAL_TEST_MYSQL_USERNAME); + var password = System.getenv(EXTERNAL_TEST_MYSQL_PASSWORD); + var db = Optional.ofNullable(System.getenv(EXTERNAL_TEST_MYSQL_DATABASE)).orElse(DATABASE_NAME); + + if (url != null) { + if (host != null && port != null) { + return Optional.of(JdbcConnectionImpl.forJDBC(url, host, Integer.parseInt(port), null, null, db, user, password)); + } else { + return Optional.of(JdbcConnectionImpl.forExternal(url, user, password)); + } + } else if (host != null && port != null) { + return Optional.of(JdbcConnectionImpl.forProtocol(PROTOCOL, host, Integer.parseInt(port), db, user, password)); + } else { + return Optional.empty(); + } + } + + @Override + public String toString() { + return container.getDockerImageName(); + } +} diff --git a/mysql/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/MysqlMetadata.java b/mysql/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/MysqlMetadata.java deleted file mode 100644 index f067f33..0000000 --- a/mysql/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/MysqlMetadata.java +++ /dev/null @@ -1,18 +0,0 @@ -package io.goodforgod.testcontainers.extensions.jdbc; - -import io.goodforgod.testcontainers.extensions.ContainerMode; -import org.jetbrains.annotations.ApiStatus.Internal; -import org.jetbrains.annotations.NotNull; - -@Internal -final class MysqlMetadata extends JdbcMetadata { - - public MysqlMetadata(boolean network, String alias, String image, ContainerMode runMode, Migration migration) { - super(network, alias, image, runMode, migration); - } - - @Override - public @NotNull String networkAliasDefault() { - return "mysql-" + System.currentTimeMillis(); - } -} diff --git a/mysql/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/TestcontainersMysql.java b/mysql/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/TestcontainersMySQL.java similarity index 88% rename from mysql/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/TestcontainersMysql.java rename to mysql/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/TestcontainersMySQL.java index c718f71..ef954de 100644 --- a/mysql/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/TestcontainersMysql.java +++ b/mysql/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/TestcontainersMySQL.java @@ -12,21 +12,19 @@ * schema migration support between test executions */ @Order(Order.DEFAULT - 100) // Run before other extensions -@ExtendWith(TestcontainersMysqlExtension.class) +@ExtendWith(TestcontainersMySQLExtension.class) @Documented @Target({ ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) -public @interface TestcontainersMysql { +public @interface TestcontainersMySQL { /** - * @see TestcontainersMysqlExtension#getContainerDefault(MysqlMetadata) * @return MySQL image *

* 1) Image can have static value: "mysql:8.0-debian" * 2) Image can be provided via environment variable using syntax: "${MY_IMAGE_ENV}" * 3) Image environment variable can have default value if empty using syntax: * "${MY_IMAGE_ENV|mysql:8.0-debian}" - *

*/ String image() default "mysql:8.0-debian"; diff --git a/mysql/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/TestcontainersMySQLExtension.java b/mysql/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/TestcontainersMySQLExtension.java new file mode 100644 index 0000000..5f9efbf --- /dev/null +++ b/mysql/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/TestcontainersMySQLExtension.java @@ -0,0 +1,77 @@ +package io.goodforgod.testcontainers.extensions.jdbc; + +import io.goodforgod.testcontainers.extensions.ContainerContext; +import java.lang.annotation.Annotation; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.MySQLContainer; +import org.testcontainers.containers.Network; +import org.testcontainers.containers.output.Slf4jLogConsumer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.utility.DockerImageName; + +final class TestcontainersMySQLExtension extends AbstractTestcontainersJdbcExtension, JdbcMetadata> { + + private static final ExtensionContext.Namespace NAMESPACE = ExtensionContext.Namespace + .create(TestcontainersMySQLExtension.class); + + @SuppressWarnings("unchecked") + @Override + protected Class> getContainerType() { + return (Class>) ((Class) MySQLContainer.class); + } + + @Override + protected Class getContainerAnnotation() { + return ContainerMySQL.class; + } + + @Override + protected Class getConnectionAnnotation() { + return ConnectionMySQL.class; + } + + @Override + protected ExtensionContext.Namespace getNamespace() { + return NAMESPACE; + } + + @Override + protected MySQLContainer createContainerDefault(JdbcMetadata metadata) { + var image = DockerImageName.parse(metadata.image()) + .asCompatibleSubstituteFor(DockerImageName.parse(org.testcontainers.containers.MySQLContainer.NAME)); + + final MySQLContainer container = new MySQLContainer<>(image); + final String alias = Optional.ofNullable(metadata.networkAlias()).orElseGet(() -> "mysql-" + System.currentTimeMillis()); + container.withDatabaseName(MySQLContext.DATABASE_NAME); + container.withUsername("mysql"); + container.withPassword("mysql"); + container.withLogConsumer(new Slf4jLogConsumer(LoggerFactory.getLogger(MySQLContainer.class)) + .withMdc("image", image.asCanonicalNameString()) + .withMdc("alias", alias)); + container.waitingFor(Wait.forListeningPort()); + container.withStartupTimeout(Duration.ofMinutes(5)); + container.setNetworkAliases(new ArrayList<>(List.of(alias))); + if (metadata.networkShared()) { + container.withNetwork(Network.SHARED); + } + + return container; + } + + @Override + protected ContainerContext createContainerContext(MySQLContainer container) { + return new MySQLContext(container); + } + + @NotNull + protected Optional findMetadata(@NotNull ExtensionContext context) { + return findAnnotation(TestcontainersMySQL.class, context) + .map(a -> new JdbcMetadata(a.network().shared(), a.network().alias(), a.image(), a.mode(), a.migration())); + } +} diff --git a/mysql/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/TestcontainersMysqlExtension.java b/mysql/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/TestcontainersMysqlExtension.java deleted file mode 100644 index 1b1e1d0..0000000 --- a/mysql/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/TestcontainersMysqlExtension.java +++ /dev/null @@ -1,69 +0,0 @@ -package io.goodforgod.testcontainers.extensions.jdbc; - -import java.lang.annotation.Annotation; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import org.jetbrains.annotations.NotNull; -import org.junit.jupiter.api.extension.ExtensionContext; -import org.testcontainers.containers.MySQLContainer; -import org.testcontainers.containers.Network; -import org.testcontainers.utility.DockerImageName; - -final class TestcontainersMysqlExtension extends AbstractTestcontainersJdbcExtension, MysqlMetadata> { - - private static final ExtensionContext.Namespace NAMESPACE = ExtensionContext.Namespace - .create(TestcontainersMysqlExtension.class); - - @SuppressWarnings("unchecked") - @Override - protected Class> getContainerType() { - return (Class>) ((Class) MySQLContainerExtra.class); - } - - @Override - protected Class getContainerAnnotation() { - return ContainerMysql.class; - } - - @Override - protected Class getConnectionAnnotation() { - return ContainerMysqlConnection.class; - } - - @Override - protected MySQLContainerExtra getContainerDefault(MysqlMetadata metadata) { - var dockerImage = DockerImageName.parse(metadata.image()) - .asCompatibleSubstituteFor(DockerImageName.parse(MySQLContainer.NAME)); - - var container = new MySQLContainerExtra<>(dockerImage); - container.setNetworkAliases(new ArrayList<>(List.of(metadata.networkAliasOrDefault()))); - if (metadata.networkShared()) { - container.withNetwork(Network.SHARED); - } - - return container; - } - - @Override - protected JdbcMigrationEngine getMigrationEngine(Migration.Engines engine, ExtensionContext context) { - var containerCurrent = getContainerCurrent(context); - return containerCurrent.getMigrationEngine(engine); - } - - @Override - protected ExtensionContext.Namespace getNamespace() { - return NAMESPACE; - } - - @NotNull - protected Optional findMetadata(@NotNull ExtensionContext context) { - return findAnnotation(TestcontainersMysql.class, context) - .map(a -> new MysqlMetadata(a.network().shared(), a.network().alias(), a.image(), a.mode(), a.migration())); - } - - @NotNull - protected JdbcConnection getConnectionForContainer(MysqlMetadata metadata, @NotNull MySQLContainerExtra container) { - return container.connection(); - } -} diff --git a/mysql/src/test/java/io/goodforgod/testcontainers/extensions/mysql/MysqlFlywayPerMethodMigrationTests.java b/mysql/src/test/java/io/goodforgod/testcontainers/extensions/mysql/MysqlFlywayPerMethodMigrationTests.java index af635ce..701768a 100644 --- a/mysql/src/test/java/io/goodforgod/testcontainers/extensions/mysql/MysqlFlywayPerMethodMigrationTests.java +++ b/mysql/src/test/java/io/goodforgod/testcontainers/extensions/mysql/MysqlFlywayPerMethodMigrationTests.java @@ -3,16 +3,16 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import io.goodforgod.testcontainers.extensions.ContainerMode; -import io.goodforgod.testcontainers.extensions.jdbc.ContainerMysqlConnection; +import io.goodforgod.testcontainers.extensions.jdbc.ConnectionMySQL; import io.goodforgod.testcontainers.extensions.jdbc.JdbcConnection; import io.goodforgod.testcontainers.extensions.jdbc.Migration; -import io.goodforgod.testcontainers.extensions.jdbc.TestcontainersMysql; +import io.goodforgod.testcontainers.extensions.jdbc.TestcontainersMySQL; import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestMethodOrder; -@TestcontainersMysql(mode = ContainerMode.PER_CLASS, +@TestcontainersMySQL(mode = ContainerMode.PER_CLASS, image = "mysql:8.0-debian", migration = @Migration( engine = Migration.Engines.FLYWAY, @@ -23,13 +23,13 @@ class MysqlFlywayPerMethodMigrationTests { @Order(1) @Test - void firstRun(@ContainerMysqlConnection JdbcConnection connection) { + void firstRun(@ConnectionMySQL JdbcConnection connection) { connection.execute("INSERT INTO users VALUES(1);"); } @Order(2) @Test - void secondRun(@ContainerMysqlConnection JdbcConnection connection) { + void secondRun(@ConnectionMySQL JdbcConnection connection) { var usersFound = connection.queryOne("SELECT * FROM users;", r -> r.getInt(1)); assertTrue(usersFound.isEmpty()); } diff --git a/mysql/src/test/java/io/goodforgod/testcontainers/extensions/mysql/MysqlLiquibaseMigrationPerMethodTests.java b/mysql/src/test/java/io/goodforgod/testcontainers/extensions/mysql/MysqlLiquibaseMigrationPerMethodTests.java index 571f4de..d140ce7 100644 --- a/mysql/src/test/java/io/goodforgod/testcontainers/extensions/mysql/MysqlLiquibaseMigrationPerMethodTests.java +++ b/mysql/src/test/java/io/goodforgod/testcontainers/extensions/mysql/MysqlLiquibaseMigrationPerMethodTests.java @@ -3,16 +3,16 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import io.goodforgod.testcontainers.extensions.ContainerMode; -import io.goodforgod.testcontainers.extensions.jdbc.ContainerMysqlConnection; +import io.goodforgod.testcontainers.extensions.jdbc.ConnectionMySQL; import io.goodforgod.testcontainers.extensions.jdbc.JdbcConnection; import io.goodforgod.testcontainers.extensions.jdbc.Migration; -import io.goodforgod.testcontainers.extensions.jdbc.TestcontainersMysql; +import io.goodforgod.testcontainers.extensions.jdbc.TestcontainersMySQL; import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestMethodOrder; -@TestcontainersMysql(mode = ContainerMode.PER_CLASS, +@TestcontainersMySQL(mode = ContainerMode.PER_CLASS, image = "mysql:8.0-debian", migration = @Migration( engine = Migration.Engines.LIQUIBASE, @@ -23,13 +23,13 @@ class MysqlLiquibaseMigrationPerMethodTests { @Order(1) @Test - void firstRun(@ContainerMysqlConnection JdbcConnection connection) { + void firstRun(@ConnectionMySQL JdbcConnection connection) { connection.execute("INSERT INTO users VALUES(1);"); } @Order(2) @Test - void secondRun(@ContainerMysqlConnection JdbcConnection connection) { + void secondRun(@ConnectionMySQL JdbcConnection connection) { var usersFound = connection.queryOne("SELECT * FROM users;", r -> r.getInt(1)); assertTrue(usersFound.isEmpty()); } diff --git a/oracle/README.md b/oracle/README.md index 3d7167a..6010f77 100644 --- a/oracle/README.md +++ b/oracle/README.md @@ -18,7 +18,7 @@ Features: **Gradle** ```groovy -testImplementation "io.goodforgod:testcontainers-extensions-oracle:0.9.6" +testImplementation "io.goodforgod:testcontainers-extensions-oracle:0.10.0" ``` **Maven** @@ -26,7 +26,7 @@ testImplementation "io.goodforgod:testcontainers-extensions-oracle:0.9.6" io.goodforgod testcontainers-extensions-oracle - 0.9.6 + 0.10.0 test ``` @@ -55,9 +55,8 @@ Extension tested against image `gvenzl/oracle-xe:18.4.0-faststart` and driver `c ## Content - [Usage](#usage) -- [Container](#container) - - [Connection](#container-connection) - - [Migration](#container-migration) +- [Connection](#connection) + - [Migration](#connection-migration) - [Annotation](#annotation) - [Manual Container](#manual-container) - [Connection](#annotation-connection) @@ -76,8 +75,11 @@ Test with container start in `PER_RUN` mode and migration per method will look l drop = Migration.Mode.PER_METHOD)) class ExampleTests { + @ConnectionOracle + private JdbcConnection connection; + @Test - void test(@ContainerOracleConnection JdbcConnection connection) { + void test() { connection.execute("INSERT INTO users VALUES(1)"); var usersFound = connection.queryMany("SELECT * FROM users", r -> r.getInt(1)); assertEquals(1, usersFound.size()); @@ -85,49 +87,43 @@ class ExampleTests { } ``` -## Container +## Connection -Library provides special `OracleContainerExtra` with ability for migration and connection. -It can be used with [Testcontainers JUnit Extension](https://java.testcontainers.org/test_framework_integration/junit_5/). +`JdbcConnection` is an abstraction with asserting data in database container and easily manipulate container connection settings. +You can inject connection via `@ConnectionOracle` as field or method argument or manually create it from container or manual settings. ```java class ExampleTests { + private static final OracleContainer container = new OracleContainer(); + @Test void test() { - try (var container = new OracleContainerExtra(DockerImageName.parse("gvenzl/oracle-xe:18.4.0-faststart"))) { - container.start(); - } + container.start(); + JdbcConnection connection = JdbcConnection.forContainer(container); + connection.execute("INSERT INTO users VALUES(1);"); } } ``` -### Container Connection +### Connection Migration -`JdbcConnection` provides connection parameters, useful asserts, checks, etc. for easier testing. +`Migrations` allow easily migrate database between test executions and drop after tests. +You can migrate container via `@TestcontainersOracle#migration` annotation parameter or manually using `JdbcConnection`. ```java +@TestcontainersOracle class ExampleTests { - @Test - void test() { - try (var container = new OracleContainerExtra(DockerImageName.parse("gvenzl/oracle-xe:18.4.0-faststart"))) { - container.start(); - container.connection().assertQueriesNone("SELECT * FROM users;"); + @Test + void test(@ConnectionOracle JdbcConnection connection) { + connection.migrationEngine(Migration.Engines.FLYWAY).apply("db/migration"); + connection.execute("INSERT INTO users VALUES(1);"); + connection.migrationEngine(Migration.Engines.FLYWAY).drop("db/migration"); } - } } ``` -### Container Migration - -`Migrations` allow easily migrate database between test executions and drop after tests. - -Annotation parameters: -- `engine` - to use for migration. -- `apply` - parameter configures migration mode. -- `drop` - configures when to reset/drop/clear database. - Available migration engines: - [Flyway](https://documentation.red-gate.com/fd/cockroachdb-184127591.html) - [Liquibase](https://www.liquibase.com/databases/cockroachdb-2) @@ -148,7 +144,7 @@ Simple example on how to start container per class, **no need to configure** con class ExampleTests { @Test - void test(@ContainerOracleConnection JdbcConnection connection) { + void test(@ConnectionOracle JdbcConnection connection) { assertNotNull(connection); } } @@ -181,7 +177,6 @@ Image syntax: When you need to **manually configure container** with specific options, you can provide such container as instance that will be used by `@TestcontainersOracle`, this can be done using `@ContainerOracle` annotation for container. -Example: ```java @TestcontainersOracle(mode = ContainerMode.PER_CLASS) class ExampleTests { @@ -192,7 +187,7 @@ class ExampleTests { .withDatabaseName("oracle"); @Test - void test(@ContainerOracleConnection JdbcConnection connection) { + void test(@ConnectionOracle JdbcConnection connection) { assertEquals("oracle", connection.params().database()); assertEquals("test", connection.params().password()); } @@ -238,19 +233,18 @@ Image syntax: ### Annotation Connection -`JdbcConnection` - can be injected to field or method parameter and used to communicate with running container via `@ContainerOracleConnection` annotation. +`JdbcConnection` - can be injected to field or method parameter and used to communicate with running container via `@ConnectionOracle` annotation. `JdbcConnection` provides connection parameters, useful asserts, checks, etc. for easier testing. -Example: ```java @TestcontainersOracle(mode = ContainerMode.PER_CLASS, image = "gvenzl/oracle-xe:18.4.0-faststart") class ExampleTests { - @ContainerOracleConnection - private JdbcConnection connectionInField; + @ConnectionOracle + private JdbcConnection connection; @Test - void test(@ContainerOracleConnection JdbcConnection connection) { + void test() { connection.execute("CREATE TABLE users (id INT NOT NULL PRIMARY KEY)"); connection.execute("INSERT INTO users VALUES(1)"); connection.assertInserted("INSERT INTO users VALUES(2)"); @@ -287,6 +281,7 @@ Annotation parameters: - `engine` - to use for migration. - `apply` - parameter configures migration mode. - `drop` - configures when to reset/drop/clear database. +- `locations` - configures locations where migrations are placed. Available migration engines: - [Flyway](https://documentation.red-gate.com/fd/oracle-184127602.html) @@ -310,7 +305,7 @@ Test with container and migration per method will look like: class ExampleTests { @Test - void test(@ContainerOracleConnection JdbcConnection connection) { + void test(@ConnectionOracle JdbcConnection connection) { connection.execute("INSERT INTO users VALUES(1)"); var usersFound = connection.queryMany("SELECT * FROM users", r -> r.getInt(1)); assertEquals(1, usersFound.size()); diff --git a/postgres/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/ContainerPostgresConnection.java b/oracle/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/ConnectionOracle.java similarity index 87% rename from postgres/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/ContainerPostgresConnection.java rename to oracle/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/ConnectionOracle.java index 8c2c633..6a2c448 100644 --- a/postgres/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/ContainerPostgresConnection.java +++ b/oracle/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/ConnectionOracle.java @@ -9,4 +9,4 @@ @Documented @Target({ ElementType.FIELD, ElementType.PARAMETER }) @Retention(RetentionPolicy.RUNTIME) -public @interface ContainerPostgresConnection {} +public @interface ConnectionOracle {} diff --git a/oracle/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/OracleContainerExtra.java b/oracle/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/OracleContainerExtra.java deleted file mode 100644 index 74edbcd..0000000 --- a/oracle/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/OracleContainerExtra.java +++ /dev/null @@ -1,137 +0,0 @@ -package io.goodforgod.testcontainers.extensions.jdbc; - -import java.time.Duration; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import org.jetbrains.annotations.ApiStatus.Internal; -import org.jetbrains.annotations.NotNull; -import org.slf4j.LoggerFactory; -import org.testcontainers.containers.OracleContainer; -import org.testcontainers.containers.output.Slf4jLogConsumer; -import org.testcontainers.utility.DockerImageName; - -public class OracleContainerExtra extends OracleContainer { - - private static final String PROTOCOL = "oracle:thin"; - private static final int ORACLE_PORT = 1521; - - private static final String EXTERNAL_TEST_ORACLE_JDBC_URL = "EXTERNAL_TEST_ORACLE_JDBC_URL"; - private static final String EXTERNAL_TEST_ORACLE_USERNAME = "EXTERNAL_TEST_ORACLE_USERNAME"; - private static final String EXTERNAL_TEST_ORACLE_PASSWORD = "EXTERNAL_TEST_ORACLE_PASSWORD"; - private static final String EXTERNAL_TEST_ORACLE_HOST = "EXTERNAL_TEST_ORACLE_HOST"; - private static final String EXTERNAL_TEST_ORACLE_PORT = "EXTERNAL_TEST_ORACLE_PORT"; - private static final String EXTERNAL_TEST_ORACLE_DATABASE = "EXTERNAL_TEST_ORACLE_DATABASE"; - - private volatile JdbcConnectionImpl connection; - private volatile FlywayJdbcMigrationEngine flywayJdbcMigrationEngine; - private volatile LiquibaseJdbcMigrationEngine liquibaseJdbcMigrationEngine; - - public OracleContainerExtra(String dockerImageName) { - this(DockerImageName.parse(dockerImageName)); - } - - public OracleContainerExtra(DockerImageName dockerImageName) { - super(dockerImageName); - - final String alias = "oracle-" + System.currentTimeMillis(); - this.withPassword("test"); - this.withDatabaseName("oracle"); - this.withLogConsumer(new Slf4jLogConsumer(LoggerFactory.getLogger(OracleContainerExtra.class)) - .withMdc("image", dockerImageName.asCanonicalNameString()) - .withMdc("alias", alias)); - this.withStartupTimeout(Duration.ofMinutes(5)); - - this.setNetworkAliases(new ArrayList<>(List.of(alias))); - } - - @Internal - JdbcMigrationEngine getMigrationEngine(@NotNull Migration.Engines engine) { - if (engine == Migration.Engines.FLYWAY) { - if (flywayJdbcMigrationEngine == null) { - this.flywayJdbcMigrationEngine = new FlywayJdbcMigrationEngine(connection()); - } - return this.flywayJdbcMigrationEngine; - } else if (engine == Migration.Engines.LIQUIBASE) { - if (liquibaseJdbcMigrationEngine == null) { - this.liquibaseJdbcMigrationEngine = new LiquibaseJdbcMigrationEngine(connection()); - } - return this.liquibaseJdbcMigrationEngine; - } else { - throw new UnsupportedOperationException("Unsupported engine: " + engine); - } - } - - @NotNull - public JdbcConnection connection() { - if (connection == null) { - final Optional connectionExternal = getConnectionExternal(); - if (connectionExternal.isEmpty() && !isRunning()) { - throw new IllegalStateException("OracleConnection can't be create for container that is not running"); - } - - final JdbcConnection jdbcConnection = connectionExternal.orElseGet(() -> { - final String alias = getNetworkAliases().get(getNetworkAliases().size() - 1); - return JdbcConnectionImpl.forJDBC(getJdbcUrl(), - getHost(), - getMappedPort(ORACLE_PORT), - alias, - ORACLE_PORT, - getDatabaseName(), - getUsername(), - getPassword()); - }); - - this.connection = (JdbcConnectionImpl) jdbcConnection; - } - - return connection; - } - - @Override - public void start() { - final Optional connectionExternal = getConnectionExternal(); - if (connectionExternal.isEmpty()) { - super.start(); - } - } - - @Override - public void stop() { - if (flywayJdbcMigrationEngine != null) { - flywayJdbcMigrationEngine.close(); - flywayJdbcMigrationEngine = null; - } - if (liquibaseJdbcMigrationEngine != null) { - liquibaseJdbcMigrationEngine.close(); - liquibaseJdbcMigrationEngine = null; - } - if (connection != null) { - connection.close(); - connection = null; - } - super.stop(); - } - - @NotNull - private static Optional getConnectionExternal() { - var url = System.getenv(EXTERNAL_TEST_ORACLE_JDBC_URL); - var host = System.getenv(EXTERNAL_TEST_ORACLE_HOST); - var port = System.getenv(EXTERNAL_TEST_ORACLE_PORT); - var user = System.getenv(EXTERNAL_TEST_ORACLE_USERNAME); - var password = System.getenv(EXTERNAL_TEST_ORACLE_PASSWORD); - var db = Optional.ofNullable(System.getenv(EXTERNAL_TEST_ORACLE_DATABASE)).orElse("xepdb1"); - - if (url != null) { - if (host != null && port != null) { - return Optional.of(JdbcConnectionImpl.forJDBC(url, host, Integer.parseInt(port), null, null, db, user, password)); - } else { - return Optional.of(JdbcConnectionImpl.forExternal(url, user, password)); - } - } else if (host != null && port != null) { - return Optional.of(JdbcConnectionImpl.forProtocol(PROTOCOL, host, Integer.parseInt(port), db, user, password)); - } else { - return Optional.empty(); - } - } -} diff --git a/oracle/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/OracleContext.java b/oracle/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/OracleContext.java new file mode 100644 index 0000000..b94e1b7 --- /dev/null +++ b/oracle/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/OracleContext.java @@ -0,0 +1,99 @@ +package io.goodforgod.testcontainers.extensions.jdbc; + +import io.goodforgod.testcontainers.extensions.ContainerContext; +import java.util.Optional; +import org.jetbrains.annotations.ApiStatus.Internal; +import org.jetbrains.annotations.NotNull; +import org.testcontainers.containers.OracleContainer; + +@Internal +final class OracleContext implements ContainerContext { + + private static final String PROTOCOL = "oracle:thin"; + private static final int ORACLE_PORT = 1521; + + private static final String EXTERNAL_TEST_ORACLE_JDBC_URL = "EXTERNAL_TEST_ORACLE_JDBC_URL"; + private static final String EXTERNAL_TEST_ORACLE_USERNAME = "EXTERNAL_TEST_ORACLE_USERNAME"; + private static final String EXTERNAL_TEST_ORACLE_PASSWORD = "EXTERNAL_TEST_ORACLE_PASSWORD"; + private static final String EXTERNAL_TEST_ORACLE_HOST = "EXTERNAL_TEST_ORACLE_HOST"; + private static final String EXTERNAL_TEST_ORACLE_PORT = "EXTERNAL_TEST_ORACLE_PORT"; + private static final String EXTERNAL_TEST_ORACLE_DATABASE = "EXTERNAL_TEST_ORACLE_DATABASE"; + + private volatile JdbcConnectionImpl connection; + + private final OracleContainer container; + + OracleContext(OracleContainer container) { + this.container = container; + } + + @NotNull + public JdbcConnection connection() { + if (connection == null) { + final Optional connectionExternal = getConnectionExternal(); + if (connectionExternal.isEmpty() && !container.isRunning()) { + throw new IllegalStateException("MysqlConnection can't be create for container that is not running"); + } + + final JdbcConnection containerConnection = connectionExternal.orElseGet(() -> { + final String alias = container.getNetworkAliases().get(container.getNetworkAliases().size() - 1); + return JdbcConnectionImpl.forJDBC(container.getJdbcUrl(), + container.getHost(), + container.getMappedPort(ORACLE_PORT), + alias, + ORACLE_PORT, + container.getDatabaseName(), + container.getUsername(), + container.getPassword()); + }); + + this.connection = (JdbcConnectionImpl) containerConnection; + } + + return connection; + } + + @Override + public void start() { + final Optional connectionExternal = getConnectionExternal(); + if (connectionExternal.isEmpty()) { + container.start(); + } + } + + @Override + public void stop() { + if (connection != null) { + connection.stop(); + connection = null; + } + container.stop(); + } + + @NotNull + private static Optional getConnectionExternal() { + var url = System.getenv(EXTERNAL_TEST_ORACLE_JDBC_URL); + var host = System.getenv(EXTERNAL_TEST_ORACLE_HOST); + var port = System.getenv(EXTERNAL_TEST_ORACLE_PORT); + var user = System.getenv(EXTERNAL_TEST_ORACLE_USERNAME); + var password = System.getenv(EXTERNAL_TEST_ORACLE_PASSWORD); + var db = Optional.ofNullable(System.getenv(EXTERNAL_TEST_ORACLE_DATABASE)).orElse("xepdb1"); + + if (url != null) { + if (host != null && port != null) { + return Optional.of(JdbcConnectionImpl.forJDBC(url, host, Integer.parseInt(port), null, null, db, user, password)); + } else { + return Optional.of(JdbcConnectionImpl.forExternal(url, user, password)); + } + } else if (host != null && port != null) { + return Optional.of(JdbcConnectionImpl.forProtocol(PROTOCOL, host, Integer.parseInt(port), db, user, password)); + } else { + return Optional.empty(); + } + } + + @Override + public String toString() { + return container.getDockerImageName(); + } +} diff --git a/oracle/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/OracleMetadata.java b/oracle/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/OracleMetadata.java deleted file mode 100644 index 865f59d..0000000 --- a/oracle/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/OracleMetadata.java +++ /dev/null @@ -1,18 +0,0 @@ -package io.goodforgod.testcontainers.extensions.jdbc; - -import io.goodforgod.testcontainers.extensions.ContainerMode; -import org.jetbrains.annotations.ApiStatus.Internal; -import org.jetbrains.annotations.NotNull; - -@Internal -final class OracleMetadata extends JdbcMetadata { - - public OracleMetadata(boolean network, String alias, String image, ContainerMode runMode, Migration migration) { - super(network, alias, image, runMode, migration); - } - - @Override - public @NotNull String networkAliasDefault() { - return "oracle-" + System.currentTimeMillis(); - } -} diff --git a/oracle/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/TestcontainersOracle.java b/oracle/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/TestcontainersOracle.java index 4d38549..ccbd090 100644 --- a/oracle/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/TestcontainersOracle.java +++ b/oracle/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/TestcontainersOracle.java @@ -19,14 +19,12 @@ public @interface TestcontainersOracle { /** - * @see TestcontainersOracleExtension#getContainerDefault(OracleMetadata) * @return Oracle image *

* 1) Image can have static value: "gvenzl/oracle-xe:18.4.0-faststart" * 2) Image can be provided via environment variable using syntax: "${MY_IMAGE_ENV}" * 3) Image environment variable can have default value if empty using syntax: * "${MY_IMAGE_ENV|gvenzl/oracle-xe:18.4.0-faststart}" - *

*/ String image() default "gvenzl/oracle-xe:18.4.0-faststart"; diff --git a/oracle/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/TestcontainersOracleExtension.java b/oracle/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/TestcontainersOracleExtension.java index 7a85ca9..07a6bf7 100644 --- a/oracle/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/TestcontainersOracleExtension.java +++ b/oracle/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/TestcontainersOracleExtension.java @@ -1,22 +1,27 @@ package io.goodforgod.testcontainers.extensions.jdbc; +import io.goodforgod.testcontainers.extensions.ContainerContext; import java.lang.annotation.Annotation; +import java.time.Duration; import java.util.ArrayList; import java.util.List; import java.util.Optional; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.extension.ExtensionContext; +import org.slf4j.LoggerFactory; import org.testcontainers.containers.Network; +import org.testcontainers.containers.OracleContainer; +import org.testcontainers.containers.output.Slf4jLogConsumer; import org.testcontainers.utility.DockerImageName; -final class TestcontainersOracleExtension extends AbstractTestcontainersJdbcExtension { +final class TestcontainersOracleExtension extends AbstractTestcontainersJdbcExtension { private static final ExtensionContext.Namespace NAMESPACE = ExtensionContext.Namespace .create(TestcontainersOracleExtension.class); @Override - protected Class getContainerType() { - return OracleContainerExtra.class; + protected Class getContainerType() { + return OracleContainer.class; } @Override @@ -26,16 +31,28 @@ protected Class getContainerAnnotation() { @Override protected Class getConnectionAnnotation() { - return ContainerOracleConnection.class; + return ConnectionOracle.class; } @Override - protected OracleContainerExtra getContainerDefault(OracleMetadata metadata) { - var dockerImage = DockerImageName.parse(metadata.image()) + protected ExtensionContext.Namespace getNamespace() { + return NAMESPACE; + } + + @Override + protected OracleContainer createContainerDefault(JdbcMetadata metadata) { + var image = DockerImageName.parse(metadata.image()) .asCompatibleSubstituteFor(DockerImageName.parse("gvenzl/oracle-xe")); - var container = new OracleContainerExtra(dockerImage); - container.setNetworkAliases(new ArrayList<>(List.of(metadata.networkAliasOrDefault()))); + final OracleContainer container = new OracleContainer(image); + final String alias = Optional.ofNullable(metadata.networkAlias()).orElseGet(() -> "oracle-" + System.currentTimeMillis()); + container.withPassword("test"); + container.withDatabaseName("oracle"); + container.withLogConsumer(new Slf4jLogConsumer(LoggerFactory.getLogger(OracleContainer.class)) + .withMdc("image", image.asCanonicalNameString()) + .withMdc("alias", alias)); + container.withStartupTimeout(Duration.ofMinutes(5)); + container.setNetworkAliases(new ArrayList<>(List.of(alias))); if (metadata.networkShared()) { container.withNetwork(Network.SHARED); } @@ -44,24 +61,13 @@ protected OracleContainerExtra getContainerDefault(OracleMetadata metadata) { } @Override - protected JdbcMigrationEngine getMigrationEngine(Migration.Engines engine, ExtensionContext context) { - var containerCurrent = getContainerCurrent(context); - return containerCurrent.getMigrationEngine(engine); - } - - @Override - protected ExtensionContext.Namespace getNamespace() { - return NAMESPACE; + protected ContainerContext createContainerContext(OracleContainer container) { + return new OracleContext(container); } @NotNull - protected Optional findMetadata(@NotNull ExtensionContext context) { + protected Optional findMetadata(@NotNull ExtensionContext context) { return findAnnotation(TestcontainersOracle.class, context) - .map(a -> new OracleMetadata(a.network().shared(), a.network().alias(), a.image(), a.mode(), a.migration())); - } - - @NotNull - protected JdbcConnection getConnectionForContainer(OracleMetadata metadata, @NotNull OracleContainerExtra container) { - return container.connection(); + .map(a -> new JdbcMetadata(a.network().shared(), a.network().alias(), a.image(), a.mode(), a.migration())); } } diff --git a/oracle/src/test/java/io/goodforgod/testcontainers/extensions/oracle/OracleFlywayPerMethodMigrationTests.java b/oracle/src/test/java/io/goodforgod/testcontainers/extensions/oracle/OracleFlywayPerMethodMigrationTests.java index 5caf8a2..d2ff072 100644 --- a/oracle/src/test/java/io/goodforgod/testcontainers/extensions/oracle/OracleFlywayPerMethodMigrationTests.java +++ b/oracle/src/test/java/io/goodforgod/testcontainers/extensions/oracle/OracleFlywayPerMethodMigrationTests.java @@ -3,7 +3,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import io.goodforgod.testcontainers.extensions.ContainerMode; -import io.goodforgod.testcontainers.extensions.jdbc.ContainerOracleConnection; +import io.goodforgod.testcontainers.extensions.jdbc.ConnectionOracle; import io.goodforgod.testcontainers.extensions.jdbc.JdbcConnection; import io.goodforgod.testcontainers.extensions.jdbc.Migration; import io.goodforgod.testcontainers.extensions.jdbc.TestcontainersOracle; @@ -23,13 +23,13 @@ class OracleFlywayPerMethodMigrationTests { @Order(1) @Test - void firstRun(@ContainerOracleConnection JdbcConnection connection) { + void firstRun(@ConnectionOracle JdbcConnection connection) { connection.execute("INSERT INTO users VALUES(1)"); } @Order(2) @Test - void secondRun(@ContainerOracleConnection JdbcConnection connection) { + void secondRun(@ConnectionOracle JdbcConnection connection) { var usersFound = connection.queryOne("SELECT * FROM users", r -> r.getInt(1)); assertTrue(usersFound.isEmpty()); } diff --git a/oracle/src/test/java/io/goodforgod/testcontainers/extensions/oracle/OracleLiquibaseMigrationPerMethodTests.java b/oracle/src/test/java/io/goodforgod/testcontainers/extensions/oracle/OracleLiquibaseMigrationPerMethodTests.java index 7cc2f9d..2b745c4 100644 --- a/oracle/src/test/java/io/goodforgod/testcontainers/extensions/oracle/OracleLiquibaseMigrationPerMethodTests.java +++ b/oracle/src/test/java/io/goodforgod/testcontainers/extensions/oracle/OracleLiquibaseMigrationPerMethodTests.java @@ -3,7 +3,7 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import io.goodforgod.testcontainers.extensions.ContainerMode; -import io.goodforgod.testcontainers.extensions.jdbc.ContainerOracleConnection; +import io.goodforgod.testcontainers.extensions.jdbc.ConnectionOracle; import io.goodforgod.testcontainers.extensions.jdbc.JdbcConnection; import io.goodforgod.testcontainers.extensions.jdbc.Migration; import io.goodforgod.testcontainers.extensions.jdbc.TestcontainersOracle; @@ -23,13 +23,13 @@ class OracleLiquibaseMigrationPerMethodTests { @Order(1) @Test - void firstRun(@ContainerOracleConnection JdbcConnection connection) { + void firstRun(@ConnectionOracle JdbcConnection connection) { connection.execute("INSERT INTO users VALUES(1)"); } @Order(2) @Test - void secondRun(@ContainerOracleConnection JdbcConnection connection) { + void secondRun(@ConnectionOracle JdbcConnection connection) { var usersFound = connection.queryOne("SELECT * FROM users", r -> r.getInt(1)); assertTrue(usersFound.isEmpty()); } diff --git a/postgres/README.md b/postgres/README.md index 64ad288..73583f7 100644 --- a/postgres/README.md +++ b/postgres/README.md @@ -18,7 +18,7 @@ Features: **Gradle** ```groovy -testImplementation "io.goodforgod:testcontainers-extensions-postgres:0.9.6" +testImplementation "io.goodforgod:testcontainers-extensions-postgres:0.10.0" ``` **Maven** @@ -26,7 +26,7 @@ testImplementation "io.goodforgod:testcontainers-extensions-postgres:0.9.6" io.goodforgod testcontainers-extensions-postgres - 0.9.6 + 0.10.0 test ``` @@ -52,9 +52,8 @@ testRuntimeOnly "org.postgresql:postgresql:42.6.0" ## Content - [Usage](#usage) -- [Container](#container) - - [Connection](#container-connection) - - [Migration](#container-migration) +- [Connection](#connection) + - [Migration](#connection-migration) - [Annotation](#annotation) - [Manual Container](#manual-container) - [Connection](#annotation-connection) @@ -66,15 +65,18 @@ testRuntimeOnly "org.postgresql:postgresql:42.6.0" Test with container start in `PER_RUN` mode and migration per method will look like: ```java -@TestcontainersPostgres(mode = ContainerMode.PER_RUN, +@TestcontainersPostgreSQL(mode = ContainerMode.PER_RUN, migration = @Migration( engine = Migration.Engines.FLYWAY, apply = Migration.Mode.PER_METHOD, drop = Migration.Mode.PER_METHOD)) class ExampleTests { + @ConnectionPostgreSQL + private JdbcConnection connection; + @Test - void test(@ContainerPostgresConnection JdbcConnection connection) { + void test() { connection.execute("INSERT INTO users VALUES(1);"); var usersFound = connection.queryMany("SELECT * FROM users;", r -> r.getInt(1)); assertEquals(1, usersFound.size()); @@ -82,56 +84,50 @@ class ExampleTests { } ``` -## Container +## Connection -Library provides special `PostgreSQLContainerExtra` with ability for migration and connection. -It can be used with [Testcontainers JUnit Extension](https://java.testcontainers.org/test_framework_integration/junit_5/). +`JdbcConnection` is an abstraction with asserting data in database container and easily manipulate container connection settings. +You can inject connection via `@ConnectionPostgreSQL` as field or method argument or manually create it from container or manual settings. ```java class ExampleTests { + private static final PostgreSQLContainer container = new PostgreSQLContainer<>(); + @Test void test() { - try (var container = new PostgreSQLContainerExtra<>(DockerImageName.parse("postgres:15.6-alpine"))) { - container.start(); - } + container.start(); + JdbcConnection connection = JdbcConnection.forContainer(container); + connection.execute("INSERT INTO users VALUES(1);"); } } ``` -### Container Connection +### Connection Migration -`JdbcConnection` provides connection parameters, useful asserts, checks, etc. for easier testing. +`Migrations` allow easily migrate database between test executions and drop after tests. +You can migrate container via `@TestcontainersPostgreSQL#migration` annotation parameter or manually using `JdbcConnection`. ```java +@TestcontainersPostgreSQL class ExampleTests { - @Test - void test() { - try (var container = new PostgreSQLContainerExtra<>(DockerImageName.parse("postgres:15.6-alpine"))) { - container.start(); - container.connection().assertQueriesNone("SELECT * FROM users;"); + @Test + void test(@ConnectionPostgreSQL JdbcConnection connection) { + connection.migrationEngine(Migration.Engines.FLYWAY).apply("db/migration"); + connection.execute("INSERT INTO users VALUES(1);"); + connection.migrationEngine(Migration.Engines.FLYWAY).drop("db/migration"); } - } } ``` -### Container Migration - -`Migrations` allow easily migrate database between test executions and drop after tests. - -Annotation parameters: -- `engine` - to use for migration. -- `apply` - parameter configures migration mode. -- `drop` - configures when to reset/drop/clear database. - Available migration engines: - [Flyway](https://documentation.red-gate.com/fd/cockroachdb-184127591.html) - [Liquibase](https://www.liquibase.com/databases/cockroachdb-2) ## Annotation -`@TestcontainersPostgres` - allow **automatically start container** with specified image in different modes without the need to configure it. +`@TestcontainersPostgreSQL` - allow **automatically start container** with specified image in different modes without the need to configure it. Available containers modes: @@ -141,11 +137,11 @@ Available containers modes: Simple example on how to start container per class, **no need to configure** container: ```java -@TestcontainersPostgres(mode = ContainerMode.PER_CLASS) +@TestcontainersPostgreSQL(mode = ContainerMode.PER_CLASS) class ExampleTests { @Test - void test(@ContainerPostgresConnection JdbcConnection connection) { + void test(@ConnectionPostgreSQL JdbcConnection connection) { assertNotNull(connection); } } @@ -157,7 +153,7 @@ It is possible to customize image with annotation `image` parameter. Image also can be provided from environment variable: ```java -@TestcontainersPostgres(image = "${MY_IMAGE_ENV|postgres:15.6-alpine}") +@TestcontainersPostgreSQL(image = "${MY_IMAGE_ENV|postgres:15.6-alpine}") class ExampleTests { @Test @@ -175,22 +171,21 @@ Image syntax: ### Manual Container -When you need to **manually configure container** with specific options, you can provide such container as instance that will be used by `@TestcontainersPostgres`, -this can be done using `@ContainerPostgres` annotation for container. +When you need to **manually configure container** with specific options, you can provide such container as instance that will be used by `@TestcontainersPostgreSQL`, +this can be done using `@ContainerPostgreSQL` annotation for container. -Example: ```java -@TestcontainersPostgres(mode = ContainerMode.PER_CLASS) +@TestcontainersPostgreSQL(mode = ContainerMode.PER_CLASS) class ExampleTests { - @ContainerPostgres + @ContainerPostgreSQL private static final PostgreSQLContainer container = new PostgreSQLContainer<>() .withDatabaseName("user") .withUsername("user") .withPassword("user"); @Test - void test(@ContainerPostgresConnection JdbcConnection connection) { + void test(@ConnectionPostgreSQL JdbcConnection connection) { assertEquals("user", connection.params().database()); assertEquals("user", connection.params().username()); assertEquals("user", connection.params().password()); @@ -202,7 +197,7 @@ class ExampleTests { In case you want to enable [Network.SHARED](https://java.testcontainers.org/features/networking/) for containers you can do this using `network` & `shared` parameter in annotation: ```java -@TestcontainersPostgres(network = @Network(shared = true)) +@TestcontainersPostgreSQL(network = @Network(shared = true)) class ExampleTests { @Test @@ -219,7 +214,7 @@ Alias can be extracted from environment variable also or default value can be pr In case specified environment variable is missing `default alias` will be created: ```java -@TestcontainersPostgres(network = @Network(alias = "${MY_ALIAS_ENV|my_default_alias}")) +@TestcontainersPostgreSQL(network = @Network(alias = "${MY_ALIAS_ENV|my_default_alias}")) class ExampleTests { @Test @@ -237,19 +232,18 @@ Image syntax: ### Annotation Connection -`JdbcConnection` - can be injected to field or method parameter and used to communicate with running container via `@ContainerPostgresConnection` annotation. +`JdbcConnection` - can be injected to field or method parameter and used to communicate with running container via `@ConnectionPostgreSQL` annotation. `JdbcConnection` provides connection parameters, useful asserts, checks, etc. for easier testing. -Example: ```java -@TestcontainersPostgres(mode = ContainerMode.PER_CLASS, image = "postgres:15.6-alpine") +@TestcontainersPostgreSQL(mode = ContainerMode.PER_CLASS, image = "postgres:15.6-alpine") class ExampleTests { - @ContainerPostgresConnection - private JdbcConnection connectionInField; + @ConnectionPostgreSQL + private JdbcConnection connection; @Test - void test(@ContainerPostgresConnection JdbcConnection connection) { + void test() { connection.execute("CREATE TABLE users (id INT NOT NULL PRIMARY KEY);"); connection.execute("INSERT INTO users VALUES(1);"); connection.assertInserted("INSERT INTO users VALUES(2);"); @@ -286,6 +280,7 @@ Annotation parameters: - `engine` - to use for migration. - `apply` - parameter configures migration mode. - `drop` - configures when to reset/drop/clear database. +- `locations` - configures locations where migrations are placed. Available migration engines: - [Flyway](https://documentation.red-gate.com/fd/postgresql-184127604.html) @@ -301,7 +296,7 @@ CREATE TABLE IF NOT EXISTS users Test with container and migration per method will look like: ```java -@TestcontainersPostgres(mode = ContainerMode.PER_CLASS, +@TestcontainersPostgreSQL(mode = ContainerMode.PER_CLASS, migration = @Migration( engine = Migration.Engines.FLYWAY, apply = Migration.Mode.PER_METHOD, @@ -309,7 +304,7 @@ Test with container and migration per method will look like: class ExampleTests { @Test - void test(@ContainerPostgresConnection JdbcConnection connection) { + void test(@ConnectionPostgreSQL JdbcConnection connection) { connection.execute("INSERT INTO users VALUES(1);"); var usersFound = connection.queryMany("SELECT * FROM users;", r -> r.getInt(1)); assertEquals(1, usersFound.size()); diff --git a/postgres/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/ConnectionPostgreSQL.java b/postgres/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/ConnectionPostgreSQL.java new file mode 100644 index 0000000..4af426c --- /dev/null +++ b/postgres/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/ConnectionPostgreSQL.java @@ -0,0 +1,12 @@ +package io.goodforgod.testcontainers.extensions.jdbc; + +import java.lang.annotation.*; + +/** + * Indicates that annotated field or parameter should be injected with {@link JdbcConnection} value + * of current active container + */ +@Documented +@Target({ ElementType.FIELD, ElementType.PARAMETER }) +@Retention(RetentionPolicy.RUNTIME) +public @interface ConnectionPostgreSQL {} diff --git a/postgres/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/ContainerPostgres.java b/postgres/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/ContainerPostgreSQL.java similarity index 52% rename from postgres/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/ContainerPostgres.java rename to postgres/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/ContainerPostgreSQL.java index 3f07533..13aeda4 100644 --- a/postgres/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/ContainerPostgres.java +++ b/postgres/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/ContainerPostgreSQL.java @@ -1,12 +1,13 @@ package io.goodforgod.testcontainers.extensions.jdbc; import java.lang.annotation.*; +import org.testcontainers.containers.PostgreSQLContainer; /** - * Indicates that annotated field containers {@link PostgreSQLContainerExtra} instance - * that should be used by {@link TestcontainersPostgres} rather than creating default container + * Indicates that annotated field containers {@link PostgreSQLContainer} instance + * that should be used by {@link TestcontainersPostgreSQL} rather than creating default container */ @Documented @Target({ ElementType.FIELD }) @Retention(RetentionPolicy.RUNTIME) -public @interface ContainerPostgres {} +public @interface ContainerPostgreSQL {} diff --git a/postgres/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/PostgreSQLContainerExtra.java b/postgres/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/PostgreSQLContainerExtra.java deleted file mode 100644 index 07d71c5..0000000 --- a/postgres/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/PostgreSQLContainerExtra.java +++ /dev/null @@ -1,137 +0,0 @@ -package io.goodforgod.testcontainers.extensions.jdbc; - -import java.time.Duration; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import org.jetbrains.annotations.ApiStatus.Internal; -import org.jetbrains.annotations.NotNull; -import org.slf4j.LoggerFactory; -import org.testcontainers.containers.PostgreSQLContainer; -import org.testcontainers.containers.output.Slf4jLogConsumer; -import org.testcontainers.utility.DockerImageName; - -public class PostgreSQLContainerExtra> extends PostgreSQLContainer { - - private static final String PROTOCOL = "postgresql"; - - private static final String EXTERNAL_TEST_POSTGRES_JDBC_URL = "EXTERNAL_TEST_POSTGRES_JDBC_URL"; - private static final String EXTERNAL_TEST_POSTGRES_USERNAME = "EXTERNAL_TEST_POSTGRES_USERNAME"; - private static final String EXTERNAL_TEST_POSTGRES_PASSWORD = "EXTERNAL_TEST_POSTGRES_PASSWORD"; - private static final String EXTERNAL_TEST_POSTGRES_HOST = "EXTERNAL_TEST_POSTGRES_HOST"; - private static final String EXTERNAL_TEST_POSTGRES_PORT = "EXTERNAL_TEST_POSTGRES_PORT"; - private static final String EXTERNAL_TEST_POSTGRES_DATABASE = "EXTERNAL_TEST_POSTGRES_DATABASE"; - - private volatile JdbcConnectionImpl connection; - private volatile FlywayJdbcMigrationEngine flywayJdbcMigrationEngine; - private volatile LiquibaseJdbcMigrationEngine liquibaseJdbcMigrationEngine; - - public PostgreSQLContainerExtra(String dockerImageName) { - this(DockerImageName.parse(dockerImageName)); - } - - public PostgreSQLContainerExtra(DockerImageName dockerImageName) { - super(dockerImageName); - - final String alias = "postgres-" + System.currentTimeMillis(); - this.withDatabaseName("postgres"); - this.withUsername("postgres"); - this.withPassword("postgres"); - this.withLogConsumer(new Slf4jLogConsumer(LoggerFactory.getLogger(PostgreSQLContainerExtra.class)) - .withMdc("image", dockerImageName.asCanonicalNameString()) - .withMdc("alias", alias)); - this.withStartupTimeout(Duration.ofMinutes(5)); - - setNetworkAliases(new ArrayList<>(List.of(alias))); - } - - @Internal - JdbcMigrationEngine getMigrationEngine(@NotNull Migration.Engines engine) { - if (engine == Migration.Engines.FLYWAY) { - if (flywayJdbcMigrationEngine == null) { - this.flywayJdbcMigrationEngine = new FlywayJdbcMigrationEngine(connection()); - } - return this.flywayJdbcMigrationEngine; - } else if (engine == Migration.Engines.LIQUIBASE) { - if (liquibaseJdbcMigrationEngine == null) { - this.liquibaseJdbcMigrationEngine = new LiquibaseJdbcMigrationEngine(connection()); - } - return this.liquibaseJdbcMigrationEngine; - } else { - throw new UnsupportedOperationException("Unsupported engine: " + engine); - } - } - - @NotNull - public JdbcConnection connection() { - if (connection == null) { - final Optional connectionExternal = getConnectionExternal(); - if (connectionExternal.isEmpty() && !isRunning()) { - throw new IllegalStateException("PostgresConnection can't be create for container that is not running"); - } - - final JdbcConnection jdbcConnection = connectionExternal.orElseGet(() -> { - final String alias = getNetworkAliases().get(getNetworkAliases().size() - 1); - return JdbcConnectionImpl.forJDBC(getJdbcUrl(), - getHost(), - getMappedPort(PostgreSQLContainer.POSTGRESQL_PORT), - alias, - PostgreSQLContainer.POSTGRESQL_PORT, - getDatabaseName(), - getUsername(), - getPassword()); - }); - - this.connection = (JdbcConnectionImpl) jdbcConnection; - } - - return connection; - } - - @Override - public void start() { - final Optional connectionExternal = getConnectionExternal(); - if (connectionExternal.isEmpty()) { - super.start(); - } - } - - @Override - public void stop() { - if (flywayJdbcMigrationEngine != null) { - flywayJdbcMigrationEngine.close(); - flywayJdbcMigrationEngine = null; - } - if (liquibaseJdbcMigrationEngine != null) { - liquibaseJdbcMigrationEngine.close(); - liquibaseJdbcMigrationEngine = null; - } - if (connection != null) { - connection.close(); - connection = null; - } - super.stop(); - } - - @NotNull - private static Optional getConnectionExternal() { - var url = System.getenv(EXTERNAL_TEST_POSTGRES_JDBC_URL); - var host = System.getenv(EXTERNAL_TEST_POSTGRES_HOST); - var port = System.getenv(EXTERNAL_TEST_POSTGRES_PORT); - var user = System.getenv(EXTERNAL_TEST_POSTGRES_USERNAME); - var password = System.getenv(EXTERNAL_TEST_POSTGRES_PASSWORD); - var db = Optional.ofNullable(System.getenv(EXTERNAL_TEST_POSTGRES_DATABASE)).orElse("postgres"); - - if (url != null) { - if (host != null && port != null) { - return Optional.of(JdbcConnectionImpl.forJDBC(url, host, Integer.parseInt(port), null, null, db, user, password)); - } else { - return Optional.of(JdbcConnectionImpl.forExternal(url, user, password)); - } - } else if (host != null && port != null) { - return Optional.of(JdbcConnectionImpl.forProtocol(PROTOCOL, host, Integer.parseInt(port), db, user, password)); - } else { - return Optional.empty(); - } - } -} diff --git a/postgres/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/PostgreSQLContext.java b/postgres/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/PostgreSQLContext.java new file mode 100644 index 0000000..764d4f4 --- /dev/null +++ b/postgres/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/PostgreSQLContext.java @@ -0,0 +1,98 @@ +package io.goodforgod.testcontainers.extensions.jdbc; + +import io.goodforgod.testcontainers.extensions.ContainerContext; +import java.util.Optional; +import org.jetbrains.annotations.ApiStatus.Internal; +import org.jetbrains.annotations.NotNull; +import org.testcontainers.containers.PostgreSQLContainer; + +@Internal +final class PostgreSQLContext implements ContainerContext { + + private static final String PROTOCOL = "postgresql"; + + private static final String EXTERNAL_TEST_POSTGRES_JDBC_URL = "EXTERNAL_TEST_POSTGRES_JDBC_URL"; + private static final String EXTERNAL_TEST_POSTGRES_USERNAME = "EXTERNAL_TEST_POSTGRES_USERNAME"; + private static final String EXTERNAL_TEST_POSTGRES_PASSWORD = "EXTERNAL_TEST_POSTGRES_PASSWORD"; + private static final String EXTERNAL_TEST_POSTGRES_HOST = "EXTERNAL_TEST_POSTGRES_HOST"; + private static final String EXTERNAL_TEST_POSTGRES_PORT = "EXTERNAL_TEST_POSTGRES_PORT"; + private static final String EXTERNAL_TEST_POSTGRES_DATABASE = "EXTERNAL_TEST_POSTGRES_DATABASE"; + + private volatile JdbcConnectionImpl connection; + + private final PostgreSQLContainer container; + + PostgreSQLContext(PostgreSQLContainer container) { + this.container = container; + } + + @NotNull + public JdbcConnection connection() { + if (connection == null) { + final Optional connectionExternal = getConnectionExternal(); + if (connectionExternal.isEmpty() && !container.isRunning()) { + throw new IllegalStateException("MysqlConnection can't be create for container that is not running"); + } + + final JdbcConnection containerConnection = connectionExternal.orElseGet(() -> { + final String alias = container.getNetworkAliases().get(container.getNetworkAliases().size() - 1); + return JdbcConnectionImpl.forJDBC(container.getJdbcUrl(), + container.getHost(), + container.getMappedPort(PostgreSQLContainer.POSTGRESQL_PORT), + alias, + PostgreSQLContainer.POSTGRESQL_PORT, + container.getDatabaseName(), + container.getUsername(), + container.getPassword()); + }); + + this.connection = (JdbcConnectionImpl) containerConnection; + } + + return connection; + } + + @Override + public void start() { + final Optional connectionExternal = getConnectionExternal(); + if (connectionExternal.isEmpty()) { + container.start(); + } + } + + @Override + public void stop() { + if (connection != null) { + connection.stop(); + connection = null; + } + container.stop(); + } + + @NotNull + private static Optional getConnectionExternal() { + var url = System.getenv(EXTERNAL_TEST_POSTGRES_JDBC_URL); + var host = System.getenv(EXTERNAL_TEST_POSTGRES_HOST); + var port = System.getenv(EXTERNAL_TEST_POSTGRES_PORT); + var user = System.getenv(EXTERNAL_TEST_POSTGRES_USERNAME); + var password = System.getenv(EXTERNAL_TEST_POSTGRES_PASSWORD); + var db = Optional.ofNullable(System.getenv(EXTERNAL_TEST_POSTGRES_DATABASE)).orElse("postgres"); + + if (url != null) { + if (host != null && port != null) { + return Optional.of(JdbcConnectionImpl.forJDBC(url, host, Integer.parseInt(port), null, null, db, user, password)); + } else { + return Optional.of(JdbcConnectionImpl.forExternal(url, user, password)); + } + } else if (host != null && port != null) { + return Optional.of(JdbcConnectionImpl.forProtocol(PROTOCOL, host, Integer.parseInt(port), db, user, password)); + } else { + return Optional.empty(); + } + } + + @Override + public String toString() { + return container.getDockerImageName(); + } +} diff --git a/postgres/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/PostgresMetadata.java b/postgres/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/PostgresMetadata.java deleted file mode 100644 index 0368979..0000000 --- a/postgres/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/PostgresMetadata.java +++ /dev/null @@ -1,18 +0,0 @@ -package io.goodforgod.testcontainers.extensions.jdbc; - -import io.goodforgod.testcontainers.extensions.ContainerMode; -import org.jetbrains.annotations.ApiStatus.Internal; -import org.jetbrains.annotations.NotNull; - -@Internal -final class PostgresMetadata extends JdbcMetadata { - - public PostgresMetadata(boolean network, String alias, String image, ContainerMode runMode, Migration migration) { - super(network, alias, image, runMode, migration); - } - - @Override - public @NotNull String networkAliasDefault() { - return "postgres-" + System.currentTimeMillis(); - } -} diff --git a/postgres/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/TestcontainersPostgres.java b/postgres/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/TestcontainersPostgreSQL.java similarity index 82% rename from postgres/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/TestcontainersPostgres.java rename to postgres/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/TestcontainersPostgreSQL.java index efb0c4d..cbc331f 100644 --- a/postgres/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/TestcontainersPostgres.java +++ b/postgres/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/TestcontainersPostgreSQL.java @@ -5,28 +5,27 @@ import java.lang.annotation.*; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.extension.ExtendWith; +import org.testcontainers.containers.PostgreSQLContainer; /** - * Extension that is running {@link PostgreSQLContainerExtra} for tests in different modes with + * Extension that is running {@link PostgreSQLContainer} for tests in different modes with * database * schema migration support between test executions */ @Order(Order.DEFAULT - 100) // Run before other extensions -@ExtendWith(TestcontainersPostgresExtension.class) +@ExtendWith(TestcontainersPostgreSQLExtension.class) @Documented @Target({ ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) -public @interface TestcontainersPostgres { +public @interface TestcontainersPostgreSQL { /** - * @see TestcontainersPostgresExtension#getContainerDefault(PostgresMetadata) * @return Postgres image *

* 1) Image can have static value: "postgres:15.6-alpine" * 2) Image can be provided via environment variable using syntax: "${MY_IMAGE_ENV}" * 3) Image environment variable can have default value if empty using syntax: * "${MY_IMAGE_ENV|postgres:15.6-alpine}" - *

*/ String image() default "postgres:15.6-alpine"; diff --git a/postgres/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/TestcontainersPostgreSQLExtension.java b/postgres/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/TestcontainersPostgreSQLExtension.java new file mode 100644 index 0000000..75a9660 --- /dev/null +++ b/postgres/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/TestcontainersPostgreSQLExtension.java @@ -0,0 +1,76 @@ +package io.goodforgod.testcontainers.extensions.jdbc; + +import io.goodforgod.testcontainers.extensions.ContainerContext; +import java.lang.annotation.Annotation; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.Network; +import org.testcontainers.containers.PostgreSQLContainer; +import org.testcontainers.containers.output.Slf4jLogConsumer; +import org.testcontainers.utility.DockerImageName; + +final class TestcontainersPostgreSQLExtension extends + AbstractTestcontainersJdbcExtension, JdbcMetadata> { + + private static final ExtensionContext.Namespace NAMESPACE = ExtensionContext.Namespace + .create(TestcontainersPostgreSQLExtension.class); + + @SuppressWarnings("unchecked") + @Override + protected Class> getContainerType() { + return (Class>) ((Class) PostgreSQLContainer.class); + } + + @Override + protected Class getContainerAnnotation() { + return ContainerPostgreSQL.class; + } + + @Override + protected Class getConnectionAnnotation() { + return ConnectionPostgreSQL.class; + } + + @Override + protected ExtensionContext.Namespace getNamespace() { + return NAMESPACE; + } + + @Override + protected PostgreSQLContainer createContainerDefault(JdbcMetadata metadata) { + var image = DockerImageName.parse(metadata.image()) + .asCompatibleSubstituteFor(DockerImageName.parse("gvenzl/oracle-xe")); + + final PostgreSQLContainer container = new PostgreSQLContainer<>(image); + final String alias = Optional.ofNullable(metadata.networkAlias()).orElseGet(() -> "oracle-" + System.currentTimeMillis()); + container.withDatabaseName("postgres"); + container.withUsername("postgres"); + container.withPassword("postgres"); + container.withLogConsumer(new Slf4jLogConsumer(LoggerFactory.getLogger(PostgreSQLContainer.class)) + .withMdc("image", image.asCanonicalNameString()) + .withMdc("alias", alias)); + container.withStartupTimeout(Duration.ofMinutes(5)); + container.setNetworkAliases(new ArrayList<>(List.of(alias))); + if (metadata.networkShared()) { + container.withNetwork(Network.SHARED); + } + + return container; + } + + @Override + protected ContainerContext createContainerContext(PostgreSQLContainer container) { + return new PostgreSQLContext(container); + } + + @NotNull + protected Optional findMetadata(@NotNull ExtensionContext context) { + return findAnnotation(TestcontainersPostgreSQL.class, context) + .map(a -> new JdbcMetadata(a.network().shared(), a.network().alias(), a.image(), a.mode(), a.migration())); + } +} diff --git a/postgres/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/TestcontainersPostgresExtension.java b/postgres/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/TestcontainersPostgresExtension.java deleted file mode 100644 index 6344150..0000000 --- a/postgres/src/main/java/io/goodforgod/testcontainers/extensions/jdbc/TestcontainersPostgresExtension.java +++ /dev/null @@ -1,71 +0,0 @@ -package io.goodforgod.testcontainers.extensions.jdbc; - -import java.lang.annotation.Annotation; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; -import org.jetbrains.annotations.NotNull; -import org.junit.jupiter.api.extension.ExtensionContext; -import org.testcontainers.containers.Network; -import org.testcontainers.containers.PostgreSQLContainer; -import org.testcontainers.utility.DockerImageName; - -final class TestcontainersPostgresExtension extends - AbstractTestcontainersJdbcExtension, PostgresMetadata> { - - private static final ExtensionContext.Namespace NAMESPACE = ExtensionContext.Namespace - .create(TestcontainersPostgresExtension.class); - - @SuppressWarnings("unchecked") - @Override - protected Class> getContainerType() { - return (Class>) ((Class) PostgreSQLContainerExtra.class); - } - - @Override - protected Class getContainerAnnotation() { - return ContainerPostgres.class; - } - - @Override - protected Class getConnectionAnnotation() { - return ContainerPostgresConnection.class; - } - - @Override - protected PostgreSQLContainerExtra getContainerDefault(PostgresMetadata metadata) { - var dockerImage = DockerImageName.parse(metadata.image()) - .asCompatibleSubstituteFor(DockerImageName.parse(PostgreSQLContainer.IMAGE)); - - var container = new PostgreSQLContainerExtra<>(dockerImage); - container.setNetworkAliases(new ArrayList<>(List.of(metadata.networkAliasOrDefault()))); - if (metadata.networkShared()) { - container.withNetwork(Network.SHARED); - } - - return container; - } - - @Override - protected JdbcMigrationEngine getMigrationEngine(Migration.Engines engine, ExtensionContext context) { - var containerCurrent = getContainerCurrent(context); - return containerCurrent.getMigrationEngine(engine); - } - - @Override - protected ExtensionContext.Namespace getNamespace() { - return NAMESPACE; - } - - @NotNull - protected Optional findMetadata(@NotNull ExtensionContext context) { - return findAnnotation(TestcontainersPostgres.class, context) - .map(a -> new PostgresMetadata(a.network().shared(), a.network().alias(), a.image(), a.mode(), a.migration())); - } - - @NotNull - protected JdbcConnection getConnectionForContainer(PostgresMetadata metadata, - @NotNull PostgreSQLContainerExtra container) { - return container.connection(); - } -} diff --git a/postgres/src/test/java/io/goodforgod/testcontainers/extensions/postgres/PostgresFlywayPerMethodMigrationTests.java b/postgres/src/test/java/io/goodforgod/testcontainers/extensions/postgres/PostgresFlywayPerMethodMigrationTests.java index d9e280c..9829b1e 100644 --- a/postgres/src/test/java/io/goodforgod/testcontainers/extensions/postgres/PostgresFlywayPerMethodMigrationTests.java +++ b/postgres/src/test/java/io/goodforgod/testcontainers/extensions/postgres/PostgresFlywayPerMethodMigrationTests.java @@ -3,16 +3,16 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import io.goodforgod.testcontainers.extensions.ContainerMode; -import io.goodforgod.testcontainers.extensions.jdbc.ContainerPostgresConnection; +import io.goodforgod.testcontainers.extensions.jdbc.ConnectionPostgreSQL; import io.goodforgod.testcontainers.extensions.jdbc.JdbcConnection; import io.goodforgod.testcontainers.extensions.jdbc.Migration; -import io.goodforgod.testcontainers.extensions.jdbc.TestcontainersPostgres; +import io.goodforgod.testcontainers.extensions.jdbc.TestcontainersPostgreSQL; import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestMethodOrder; -@TestcontainersPostgres(mode = ContainerMode.PER_CLASS, +@TestcontainersPostgreSQL(mode = ContainerMode.PER_CLASS, image = "postgres:15.6-alpine", migration = @Migration( engine = Migration.Engines.FLYWAY, @@ -23,13 +23,13 @@ class PostgresFlywayPerMethodMigrationTests { @Order(1) @Test - void firstRun(@ContainerPostgresConnection JdbcConnection connection) { + void firstRun(@ConnectionPostgreSQL JdbcConnection connection) { connection.execute("INSERT INTO users VALUES(1);"); } @Order(2) @Test - void secondRun(@ContainerPostgresConnection JdbcConnection connection) { + void secondRun(@ConnectionPostgreSQL JdbcConnection connection) { var usersFound = connection.queryOne("SELECT * FROM users;", r -> r.getInt(1)); assertTrue(usersFound.isEmpty()); } diff --git a/postgres/src/test/java/io/goodforgod/testcontainers/extensions/postgres/PostgresLiquibaseMigrationPerMethodTests.java b/postgres/src/test/java/io/goodforgod/testcontainers/extensions/postgres/PostgresLiquibaseMigrationPerMethodTests.java index f69b93d..1d875b9 100644 --- a/postgres/src/test/java/io/goodforgod/testcontainers/extensions/postgres/PostgresLiquibaseMigrationPerMethodTests.java +++ b/postgres/src/test/java/io/goodforgod/testcontainers/extensions/postgres/PostgresLiquibaseMigrationPerMethodTests.java @@ -3,16 +3,16 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import io.goodforgod.testcontainers.extensions.ContainerMode; -import io.goodforgod.testcontainers.extensions.jdbc.ContainerPostgresConnection; +import io.goodforgod.testcontainers.extensions.jdbc.ConnectionPostgreSQL; import io.goodforgod.testcontainers.extensions.jdbc.JdbcConnection; import io.goodforgod.testcontainers.extensions.jdbc.Migration; -import io.goodforgod.testcontainers.extensions.jdbc.TestcontainersPostgres; +import io.goodforgod.testcontainers.extensions.jdbc.TestcontainersPostgreSQL; import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestMethodOrder; -@TestcontainersPostgres(mode = ContainerMode.PER_CLASS, +@TestcontainersPostgreSQL(mode = ContainerMode.PER_CLASS, image = "postgres:15.6-alpine", migration = @Migration( engine = Migration.Engines.LIQUIBASE, @@ -23,13 +23,13 @@ class PostgresLiquibaseMigrationPerMethodTests { @Order(1) @Test - void firstRun(@ContainerPostgresConnection JdbcConnection connection) { + void firstRun(@ConnectionPostgreSQL JdbcConnection connection) { connection.execute("INSERT INTO users VALUES(1);"); } @Order(2) @Test - void secondRun(@ContainerPostgresConnection JdbcConnection connection) { + void secondRun(@ConnectionPostgreSQL JdbcConnection connection) { var usersFound = connection.queryOne("SELECT * FROM users;", r -> r.getInt(1)); assertTrue(usersFound.isEmpty()); } diff --git a/redis/README.md b/redis/README.md index ecaefc2..762958f 100644 --- a/redis/README.md +++ b/redis/README.md @@ -17,7 +17,7 @@ Features: **Gradle** ```groovy -testImplementation "io.goodforgod:testcontainers-extensions-redis:0.9.6" +testImplementation "io.goodforgod:testcontainers-extensions-redis:0.10.0" ``` **Maven** @@ -25,7 +25,7 @@ testImplementation "io.goodforgod:testcontainers-extensions-redis:0.9.6" io.goodforgod testcontainers-extensions-redis - 0.9.6 + 0.10.0 test ``` @@ -51,8 +51,7 @@ testImplementation "redis.clients:jedis:4.4.3" ## Content - [Usage](#usage) -- [Container](#container) - - [Connection](#container-connection) +- [Connection](#connection) - [Annotation](#annotation) - [Manual Container](#manual-container) - [Connection](#annotation-connection) @@ -66,8 +65,11 @@ Test with container start in `PER_RUN` mode will look like: @TestcontainersRedis(mode = ContainerMode.PER_RUN) class ExampleTests { + @ConnectionRedis + private RedisConnection connection; + @Test - void test(@ContainerRedisConnection RedisConnection connection) { + void test() { connection.commands().set("11", "1"); connection.commands().set("12", "2"); assertEquals(2, connection.countPrefix(RedisKey.of("1"))); @@ -75,39 +77,23 @@ class ExampleTests { } ``` -## Container +## Connection -Library provides special `RedisContainerExtra` with ability for migration and connection. -It can be used with [Testcontainers JUnit Extension](https://java.testcontainers.org/test_framework_integration/junit_5/). +`RedisConnection` is an abstraction with asserting data in database container and easily manipulate container connection settings. +You can inject connection via `@ConnectionRedis` as field or method argument or manually create it from container or manual settings. ```java class ExampleTests { - @Test - void test() { - try (var container = new RedisContainerExtra<>(DockerImageName.parse("redis:7.2-alpine"))) { - container.start(); - } - } -} -``` - -### Container Connection - -`RedisConnection` provides connection parameters, useful asserts, checks, etc. for easier testing. - -```java -class ExampleTests { + private static final RedisContainer container = new RedisContainer(); @Test void test() { - try (var container = new RedisContainerExtra<>(DockerImageName.parse("redis:7.2-alpine"))) { - container.start(); - var connection = container.connection(); - connection.commands().set("11", "1"); - connection.commands().set("12", "2"); - assertEquals(2, connection.countPrefix(RedisKey.of("1"))); - } + container.start(); + RedisConnection connection = RedisConnection.forContainer(container); + connection.commands().set("11", "1"); + connection.commands().set("12", "2"); + assertEquals(2, connection.countPrefix(RedisKey.of("1"))); } } ``` @@ -128,7 +114,7 @@ Simple example on how to start container per class, **no need to configure** con class ExampleTests { @Test - void test(@ContainerRedisConnection RedisConnection connection) { + void test(@ConnectionRedis RedisConnection connection) { assertNotNull(connection); } } @@ -167,13 +153,10 @@ Example: class ExampleTests { @ContainerRedis - private static final RedisContainer container = new RedisContainer() - .withNetworkAliases("myredis") - .withLogConsumer(new Slf4jLogConsumer(LoggerFactory.getLogger(RedisContainer.class))) - .withNetwork(Network.SHARED); + private static final RedisContainer container = new RedisContainer().withNetworkAliases("myredis"); @Test - void test(@ContainerRedisConnection RedisConnection connection) { + void test(@ConnectionRedis RedisConnection connection) { assertEquals("myredis", connection.paramsInNetwork().get().host()); } } @@ -218,7 +201,7 @@ Image syntax: ### Annotation Connection -`RedisConnection` - can be injected to field or method parameter and used to communicate with running container via `@ContainerRedisConnection` annotation. +`RedisConnection` - can be injected to field or method parameter and used to communicate with running container via `@ConnectionRedis` annotation. `RedisConnection` provides connection parameters, useful asserts, checks, etc. for easier testing. Example: @@ -226,11 +209,11 @@ Example: @TestcontainersRedis(mode = ContainerMode.PER_CLASS, image = "redis:7.2-alpine") class ExampleTests { - @ContainerRedisConnection - private RedisConnection connectionInField; + @ConnectionRedis + private RedisConnection connection; @Test - void test(@ContainerRedisConnection RedisConnection connection) { + void test() { connection.commands().set("11", "1"); connection.commands().set("12", "2"); assertEquals(2, connection.countPrefix(RedisKey.of("1"))); diff --git a/redis/src/main/java/io/goodforgod/testcontainers/extensions/redis/ContainerRedisConnection.java b/redis/src/main/java/io/goodforgod/testcontainers/extensions/redis/ConnectionRedis.java similarity index 78% rename from redis/src/main/java/io/goodforgod/testcontainers/extensions/redis/ContainerRedisConnection.java rename to redis/src/main/java/io/goodforgod/testcontainers/extensions/redis/ConnectionRedis.java index 447fe57..e176931 100644 --- a/redis/src/main/java/io/goodforgod/testcontainers/extensions/redis/ContainerRedisConnection.java +++ b/redis/src/main/java/io/goodforgod/testcontainers/extensions/redis/ConnectionRedis.java @@ -3,11 +3,11 @@ import java.lang.annotation.*; /** - * Indicates that annotated field or parameter should be injected with {@link RedisContainerExtra} + * Indicates that annotated field or parameter should be injected with {@link RedisContainer} * value * of current active container */ @Documented @Target({ ElementType.FIELD, ElementType.PARAMETER }) @Retention(RetentionPolicy.RUNTIME) -public @interface ContainerRedisConnection {} +public @interface ConnectionRedis {} diff --git a/redis/src/main/java/io/goodforgod/testcontainers/extensions/redis/ContainerRedis.java b/redis/src/main/java/io/goodforgod/testcontainers/extensions/redis/ContainerRedis.java index a43c59d..beec21c 100644 --- a/redis/src/main/java/io/goodforgod/testcontainers/extensions/redis/ContainerRedis.java +++ b/redis/src/main/java/io/goodforgod/testcontainers/extensions/redis/ContainerRedis.java @@ -3,7 +3,7 @@ import java.lang.annotation.*; /** - * Indicates that annotated field containers {@link RedisContainerExtra} instance + * Indicates that annotated field containers {@link RedisContainer} instance * that should be used by {@link TestcontainersRedis} rather than creating default container */ @Documented diff --git a/redis/src/main/java/io/goodforgod/testcontainers/extensions/redis/RedisCommandsImpl.java b/redis/src/main/java/io/goodforgod/testcontainers/extensions/redis/JedisCommandsImpl.java similarity index 68% rename from redis/src/main/java/io/goodforgod/testcontainers/extensions/redis/RedisCommandsImpl.java rename to redis/src/main/java/io/goodforgod/testcontainers/extensions/redis/JedisCommandsImpl.java index bec138e..8528a43 100644 --- a/redis/src/main/java/io/goodforgod/testcontainers/extensions/redis/RedisCommandsImpl.java +++ b/redis/src/main/java/io/goodforgod/testcontainers/extensions/redis/JedisCommandsImpl.java @@ -6,9 +6,9 @@ import redis.clients.jedis.JedisClientConfig; @Internal -final class RedisCommandsImpl extends Jedis implements RedisCommands { +final class JedisCommandsImpl extends Jedis implements JedisConnection { - RedisCommandsImpl(final HostAndPort hostPort, final JedisClientConfig config) { + JedisCommandsImpl(final HostAndPort hostPort, final JedisClientConfig config) { super(hostPort, config); } } diff --git a/redis/src/main/java/io/goodforgod/testcontainers/extensions/redis/RedisCommands.java b/redis/src/main/java/io/goodforgod/testcontainers/extensions/redis/JedisConnection.java similarity index 90% rename from redis/src/main/java/io/goodforgod/testcontainers/extensions/redis/RedisCommands.java rename to redis/src/main/java/io/goodforgod/testcontainers/extensions/redis/JedisConnection.java index b66582b..f5e9489 100644 --- a/redis/src/main/java/io/goodforgod/testcontainers/extensions/redis/RedisCommands.java +++ b/redis/src/main/java/io/goodforgod/testcontainers/extensions/redis/JedisConnection.java @@ -5,7 +5,7 @@ /** * @see redis.clients.jedis.Jedis */ -public interface RedisCommands extends +public interface JedisConnection extends ServerCommands, DatabaseCommands, JedisCommands, diff --git a/redis/src/main/java/io/goodforgod/testcontainers/extensions/redis/RedisConnection.java b/redis/src/main/java/io/goodforgod/testcontainers/extensions/redis/RedisConnection.java index ac1df91..df504ee 100644 --- a/redis/src/main/java/io/goodforgod/testcontainers/extensions/redis/RedisConnection.java +++ b/redis/src/main/java/io/goodforgod/testcontainers/extensions/redis/RedisConnection.java @@ -5,11 +5,13 @@ import java.util.List; import java.util.Optional; import org.jetbrains.annotations.NotNull; +import redis.clients.jedis.HostAndPort; +import redis.clients.jedis.util.JedisURIHelper; /** - * Describes active Redis connection of currently running {@link RedisContainerExtra} + * Describes active Redis connection of currently running {@link RedisContainer} */ -public interface RedisConnection { +public interface RedisConnection extends AutoCloseable { /** * Redis connection parameters @@ -48,7 +50,7 @@ interface Params { * @return new Redis connection */ @NotNull - RedisCommands commands(); + JedisConnection getConnection(); void deleteAll(); @@ -138,4 +140,38 @@ interface Params { * @param expected exact number of values expected */ List assertCountsEquals(long expected, @NotNull Collection keys); + + @Override + void close(); + + static RedisConnection forContainer(RedisContainer container) { + if (!container.isRunning()) { + throw new IllegalStateException(container.getClass().getSimpleName() + " container is not running"); + } + + var params = new RedisConnectionImpl.ParamsImpl(container.getHost(), container.getPort(), container.getUser(), + container.getPassword(), container.getDatabase()); + final String alias = container.getNetworkAliases().get(container.getNetworkAliases().size() - 1); + var network = new RedisConnectionImpl.ParamsImpl(alias, RedisContainer.PORT, container.getUser(), container.getPassword(), + container.getDatabase()); + return new RedisConnectionClosableImpl(params, network); + } + + static RedisConnection forURI(URI uri) { + HostAndPort hostAndPort = JedisURIHelper.getHostAndPort(uri); + String user = JedisURIHelper.getUser(uri); + String password = JedisURIHelper.getPassword(uri); + int database = JedisURIHelper.getDBIndex(uri); + var params = new RedisConnectionImpl.ParamsImpl(hostAndPort.getHost(), hostAndPort.getPort(), user, password, database); + return new RedisConnectionClosableImpl(params, null); + } + + static RedisConnection forParams(String host, + int port, + int database, + String username, + String password) { + var params = new RedisConnectionImpl.ParamsImpl(host, port, username, password, database); + return new RedisConnectionClosableImpl(params, null); + } } diff --git a/redis/src/main/java/io/goodforgod/testcontainers/extensions/redis/RedisConnectionClosableImpl.java b/redis/src/main/java/io/goodforgod/testcontainers/extensions/redis/RedisConnectionClosableImpl.java new file mode 100644 index 0000000..9ad277b --- /dev/null +++ b/redis/src/main/java/io/goodforgod/testcontainers/extensions/redis/RedisConnectionClosableImpl.java @@ -0,0 +1,16 @@ +package io.goodforgod.testcontainers.extensions.redis; + +import org.jetbrains.annotations.ApiStatus.Internal; + +@Internal +final class RedisConnectionClosableImpl extends RedisConnectionImpl { + + RedisConnectionClosableImpl(Params params, Params network) { + super(params, network); + } + + @Override + public void close() { + super.stop(); + } +} diff --git a/redis/src/main/java/io/goodforgod/testcontainers/extensions/redis/RedisConnectionImpl.java b/redis/src/main/java/io/goodforgod/testcontainers/extensions/redis/RedisConnectionImpl.java index 59bc4b7..19cd1f2 100644 --- a/redis/src/main/java/io/goodforgod/testcontainers/extensions/redis/RedisConnectionImpl.java +++ b/redis/src/main/java/io/goodforgod/testcontainers/extensions/redis/RedisConnectionImpl.java @@ -16,9 +16,9 @@ import redis.clients.jedis.args.FlushMode; @Internal -final class RedisConnectionImpl implements RedisConnection { +class RedisConnectionImpl implements RedisConnection { - private static final class ParamsImpl implements Params { + static final class ParamsImpl implements Params { private final String host; private final int port; @@ -78,7 +78,7 @@ public String toString() { private final Params network; private volatile boolean isClosed = false; - private volatile RedisCommandsImpl jedis; + private volatile JedisCommandsImpl jedis; RedisConnectionImpl(Params params, Params network) { this.params = params; @@ -108,7 +108,7 @@ static RedisConnection forExternal(String host, int database, String username, String password) { - var params = new ParamsImpl(host, port, username, password, database); + var params = new RedisConnectionImpl.ParamsImpl(host, port, username, password, database); return new RedisConnectionImpl(params, null); } @@ -123,7 +123,7 @@ static RedisConnection forExternal(String host, } @NotNull - private RedisCommands connection() { + private JedisConnection connection() { if (isClosed) { throw new IllegalStateException("RedisConnection was closed"); } @@ -144,7 +144,7 @@ private RedisCommands connection() { config.password(params().password()); } - jedis = new RedisCommandsImpl(new HostAndPort(params().host(), params().port()), config.build()); + jedis = new JedisCommandsImpl(new HostAndPort(params().host(), params().port()), config.build()); } catch (Exception e) { throw new RedisConnectionException(e); } @@ -154,7 +154,7 @@ private RedisCommands connection() { } @NotNull - public RedisCommands commands() { + public JedisConnection getConnection() { return connection(); } @@ -316,7 +316,12 @@ public String toString() { return params().toString(); } - void close() { + @Override + public void close() { + // do nothing + } + + void stop() { if (jedis != null) { jedis.close(); jedis = null; diff --git a/redis/src/main/java/io/goodforgod/testcontainers/extensions/redis/RedisContainerExtra.java b/redis/src/main/java/io/goodforgod/testcontainers/extensions/redis/RedisContext.java similarity index 56% rename from redis/src/main/java/io/goodforgod/testcontainers/extensions/redis/RedisContainerExtra.java rename to redis/src/main/java/io/goodforgod/testcontainers/extensions/redis/RedisContext.java index 10785d7..95b5102 100644 --- a/redis/src/main/java/io/goodforgod/testcontainers/extensions/redis/RedisContainerExtra.java +++ b/redis/src/main/java/io/goodforgod/testcontainers/extensions/redis/RedisContext.java @@ -1,15 +1,12 @@ package io.goodforgod.testcontainers.extensions.redis; -import java.time.Duration; -import java.util.ArrayList; -import java.util.List; +import io.goodforgod.testcontainers.extensions.ContainerContext; import java.util.Optional; +import org.jetbrains.annotations.ApiStatus.Internal; import org.jetbrains.annotations.NotNull; -import org.slf4j.LoggerFactory; -import org.testcontainers.containers.output.Slf4jLogConsumer; -import org.testcontainers.utility.DockerImageName; -public class RedisContainerExtra> extends RedisContainer { +@Internal +final class RedisContext implements ContainerContext { private static final String EXTERNAL_TEST_REDIS_USERNAME = "EXTERNAL_TEST_REDIS_USERNAME"; private static final String EXTERNAL_TEST_REDIS_PASSWORD = "EXTERNAL_TEST_REDIS_PASSWORD"; @@ -19,42 +16,32 @@ public class RedisContainerExtra> extends private volatile RedisConnectionImpl connection; - public RedisContainerExtra(String dockerImageName) { - super(dockerImageName); - } - - public RedisContainerExtra(DockerImageName dockerImageName) { - super(dockerImageName); + private final RedisContainer container; - final String alias = "redis-" + System.currentTimeMillis(); - this.withLogConsumer(new Slf4jLogConsumer(LoggerFactory.getLogger(RedisContainerExtra.class)) - .withMdc("image", dockerImageName.asCanonicalNameString()) - .withMdc("alias", alias)) - .withStartupTimeout(Duration.ofMinutes(5)); - - this.setNetworkAliases(new ArrayList<>(List.of(alias))); + RedisContext(RedisContainer container) { + this.container = container; } @NotNull public RedisConnection connection() { if (connection == null) { final Optional connectionExternal = getConnectionExternal(); - if (connectionExternal.isEmpty() && !isRunning()) { + if (connectionExternal.isEmpty() && !container.isRunning()) { throw new IllegalStateException("RedisConnection can't be create for container that is not running"); } - final RedisConnection redisConnection = connectionExternal.orElseGet(() -> { - final String alias = getNetworkAliases().get(getNetworkAliases().size() - 1); - return RedisConnectionImpl.forContainer(getHost(), - getMappedPort(RedisContainer.PORT), + final RedisConnection containerConnection = connectionExternal.orElseGet(() -> { + final String alias = container.getNetworkAliases().get(container.getNetworkAliases().size() - 1); + return RedisConnectionImpl.forContainer(container.getHost(), + container.getMappedPort(RedisContainer.PORT), alias, RedisContainer.PORT, - getDatabase(), - getUser(), - getPassword()); + container.getDatabase(), + container.getUser(), + container.getPassword()); }); - this.connection = (RedisConnectionImpl) redisConnection; + this.connection = (RedisConnectionImpl) containerConnection; } return connection; @@ -64,15 +51,17 @@ public RedisConnection connection() { public void start() { final Optional connectionExternal = getConnectionExternal(); if (connectionExternal.isEmpty()) { - super.start(); + container.start(); } } @Override public void stop() { - connection.close(); - connection = null; - super.stop(); + if (connection != null) { + connection.stop(); + connection = null; + } + container.stop(); } @NotNull @@ -89,4 +78,9 @@ private static Optional getConnectionExternal() { return Optional.empty(); } } + + @Override + public String toString() { + return container.getDockerImageName(); + } } diff --git a/redis/src/main/java/io/goodforgod/testcontainers/extensions/redis/RedisMetadata.java b/redis/src/main/java/io/goodforgod/testcontainers/extensions/redis/RedisMetadata.java index 28d3475..39c6b0d 100644 --- a/redis/src/main/java/io/goodforgod/testcontainers/extensions/redis/RedisMetadata.java +++ b/redis/src/main/java/io/goodforgod/testcontainers/extensions/redis/RedisMetadata.java @@ -3,7 +3,6 @@ import io.goodforgod.testcontainers.extensions.AbstractContainerMetadata; import io.goodforgod.testcontainers.extensions.ContainerMode; import org.jetbrains.annotations.ApiStatus.Internal; -import org.jetbrains.annotations.NotNull; @Internal final class RedisMetadata extends AbstractContainerMetadata { @@ -11,9 +10,4 @@ final class RedisMetadata extends AbstractContainerMetadata { RedisMetadata(boolean network, String alias, String image, ContainerMode runMode) { super(network, alias, image, runMode); } - - @Override - public @NotNull String networkAliasDefault() { - return "redis-" + System.currentTimeMillis(); - } } diff --git a/redis/src/main/java/io/goodforgod/testcontainers/extensions/redis/TestcontainersRedis.java b/redis/src/main/java/io/goodforgod/testcontainers/extensions/redis/TestcontainersRedis.java index 08c256d..117286d 100644 --- a/redis/src/main/java/io/goodforgod/testcontainers/extensions/redis/TestcontainersRedis.java +++ b/redis/src/main/java/io/goodforgod/testcontainers/extensions/redis/TestcontainersRedis.java @@ -7,7 +7,7 @@ import org.junit.jupiter.api.extension.ExtendWith; /** - * Extension that is running {@link RedisContainerExtra} for tests in different modes with database + * Extension that is running {@link RedisContainer} for tests in different modes with database * schema migration support between test executions */ @Order(Order.DEFAULT - 100) // Run before other extensions @@ -18,14 +18,12 @@ public @interface TestcontainersRedis { /** - * @see TestcontainersRedisExtension#getContainerDefault(RedisMetadata) * @return Redis image *

* 1) Image can have static value: "redis:7.2-alpine" * 2) Image can be provided via environment variable using syntax: "${MY_IMAGE_ENV}" * 3) Image environment variable can have default value if empty using syntax: * "${MY_IMAGE_ENV|redis:7.2-alpine}" - *

*/ String image() default "redis:7.2-alpine"; diff --git a/redis/src/main/java/io/goodforgod/testcontainers/extensions/redis/TestcontainersRedisExtension.java b/redis/src/main/java/io/goodforgod/testcontainers/extensions/redis/TestcontainersRedisExtension.java index 7cd6770..c978fe9 100644 --- a/redis/src/main/java/io/goodforgod/testcontainers/extensions/redis/TestcontainersRedisExtension.java +++ b/redis/src/main/java/io/goodforgod/testcontainers/extensions/redis/TestcontainersRedisExtension.java @@ -1,26 +1,30 @@ package io.goodforgod.testcontainers.extensions.redis; import io.goodforgod.testcontainers.extensions.AbstractTestcontainersExtension; +import io.goodforgod.testcontainers.extensions.ContainerContext; import java.lang.annotation.Annotation; +import java.time.Duration; import java.util.ArrayList; import java.util.List; import java.util.Optional; import org.jetbrains.annotations.ApiStatus.Internal; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.extension.ExtensionContext; +import org.slf4j.LoggerFactory; import org.testcontainers.containers.Network; +import org.testcontainers.containers.output.Slf4jLogConsumer; import org.testcontainers.utility.DockerImageName; @Internal class TestcontainersRedisExtension extends - AbstractTestcontainersExtension, RedisMetadata> { + AbstractTestcontainersExtension, RedisMetadata> { private static final ExtensionContext.Namespace NAMESPACE = ExtensionContext.Namespace .create(TestcontainersRedisExtension.class); @SuppressWarnings("unchecked") - protected Class> getContainerType() { - return (Class>) ((Class) RedisContainerExtra.class); + protected Class> getContainerType() { + return (Class>) ((Class) RedisContainer.class); } protected Class getContainerAnnotation() { @@ -28,7 +32,7 @@ protected Class getContainerAnnotation() { } protected Class getConnectionAnnotation() { - return ContainerRedisConnection.class; + return ConnectionRedis.class; } @Override @@ -37,12 +41,22 @@ protected Class getConnectionType() { } @Override - protected RedisContainerExtra getContainerDefault(RedisMetadata metadata) { - var dockerImage = DockerImageName.parse(metadata.image()) + protected ExtensionContext.Namespace getNamespace() { + return NAMESPACE; + } + + @Override + protected RedisContainer createContainerDefault(RedisMetadata metadata) { + var image = DockerImageName.parse(metadata.image()) .asCompatibleSubstituteFor(DockerImageName.parse("redis")); - var container = new RedisContainerExtra<>(dockerImage); - container.setNetworkAliases(new ArrayList<>(List.of(metadata.networkAliasOrDefault()))); + final RedisContainer container = new RedisContainer<>(image); + final String alias = Optional.ofNullable(metadata.networkAlias()).orElseGet(() -> "redis-" + System.currentTimeMillis()); + container.withLogConsumer(new Slf4jLogConsumer(LoggerFactory.getLogger(RedisContainer.class)) + .withMdc("image", image.asCanonicalNameString()) + .withMdc("alias", alias)) + .withStartupTimeout(Duration.ofMinutes(5)); + container.setNetworkAliases(new ArrayList<>(List.of(alias))); if (metadata.networkShared()) { container.withNetwork(Network.SHARED); } @@ -51,8 +65,8 @@ protected RedisContainerExtra getContainerDefault(RedisMetadata metadata) { } @Override - protected ExtensionContext.Namespace getNamespace() { - return NAMESPACE; + protected ContainerContext createContainerContext(RedisContainer container) { + return new RedisContext(container); } @NotNull @@ -60,9 +74,4 @@ protected Optional findMetadata(@NotNull ExtensionContext context return findAnnotation(TestcontainersRedis.class, context) .map(a -> new RedisMetadata(a.network().shared(), a.network().alias(), a.image(), a.mode())); } - - @NotNull - protected RedisConnection getConnectionForContainer(RedisMetadata metadata, RedisContainerExtra container) { - return container.connection(); - } } diff --git a/redis/src/test/java/io/goodforgod/testcontainers/extensions/redis/RedisConnectionAssertsTests.java b/redis/src/test/java/io/goodforgod/testcontainers/extensions/redis/RedisConnectionAssertsTests.java index 8016887..79a882d 100644 --- a/redis/src/test/java/io/goodforgod/testcontainers/extensions/redis/RedisConnectionAssertsTests.java +++ b/redis/src/test/java/io/goodforgod/testcontainers/extensions/redis/RedisConnectionAssertsTests.java @@ -11,7 +11,7 @@ @TestcontainersRedis(mode = ContainerMode.PER_CLASS, image = "redis:7.0-alpine") class RedisConnectionAssertsTests { - @ContainerRedisConnection + @ConnectionRedis private RedisConnection connection; @BeforeEach @@ -21,15 +21,15 @@ void clean() { @Test void countPrefix() { - connection.commands().set("11", "1"); - connection.commands().set("12", "2"); + connection.getConnection().set("11", "1"); + connection.getConnection().set("12", "2"); assertEquals(2, connection.countPrefix(RedisKey.of("1"))); } @Test void assertCountsPrefixNoneWhenMore() { - connection.commands().set("11", "1"); - connection.commands().set("12", "2"); + connection.getConnection().set("11", "1"); + connection.getConnection().set("12", "2"); assertThrows(AssertionFailedError.class, () -> connection.assertCountsPrefixNone(RedisKey.of("1"))); } @@ -45,14 +45,14 @@ void assertCountsPrefixAtLeastWhenZero() { @Test void assertCountsPrefixAtLeastWhenMore() { - connection.commands().set("11", "1"); - connection.commands().set("12", "2"); + connection.getConnection().set("11", "1"); + connection.getConnection().set("12", "2"); assertDoesNotThrow(() -> connection.assertCountsPrefixAtLeast(1, RedisKey.of("1"))); } @Test void assertCountsPrefixAtLeastWhenEquals() { - connection.commands().set("11", "1"); + connection.getConnection().set("11", "1"); assertDoesNotThrow(() -> connection.assertCountsPrefixAtLeast(1, RedisKey.of("1"))); } @@ -63,15 +63,15 @@ void assertCountsPrefixExactWhenZero() { @Test void count() { - connection.commands().set("11", "1"); - connection.commands().set("12", "2"); + connection.getConnection().set("11", "1"); + connection.getConnection().set("12", "2"); assertEquals(1, connection.count(RedisKey.of("11"))); } @Test void assertCountsNoneWhenMore() { - connection.commands().set("11", "1"); - connection.commands().set("12", "2"); + connection.getConnection().set("11", "1"); + connection.getConnection().set("12", "2"); var k1 = RedisKey.of("11"); var k2 = RedisKey.of("12"); assertNotEquals(k1, k2); @@ -92,15 +92,15 @@ void assertCountsAtLeastWhenZero() { @Test void assertCountsAtLeastWhenOther() { - connection.commands().set("11", "{\"a\":1}"); - connection.commands().set("12", "{\"a\":2}"); + connection.getConnection().set("11", "{\"a\":1}"); + connection.getConnection().set("12", "{\"a\":2}"); assertDoesNotThrow(() -> connection.assertCountsAtLeast(1, RedisKey.of("11", "22"))); } @Test void assertCountsAtLeastWhenMore() { - connection.commands().set("11", "{\"a\":1}"); - connection.commands().set("12", "{\"a\":2}"); + connection.getConnection().set("11", "{\"a\":1}"); + connection.getConnection().set("12", "{\"a\":2}"); var values = assertDoesNotThrow(() -> connection.assertCountsAtLeast(1, RedisKey.of("11", "12"))); assertEquals(2, values.size()); assertNotEquals(values.get(0), values.get(1)); @@ -112,7 +112,7 @@ void assertCountsAtLeastWhenMore() { @Test void assertCountsAtLeastWhenEquals() { - connection.commands().set("11", "1"); + connection.getConnection().set("11", "1"); assertDoesNotThrow(() -> connection.assertCountsAtLeast(1, RedisKey.of("11"))); }