#29 - Use Testcontainers to run Postgres integration tests.

Postgres integration tests now run either with a locally installed and started database or if no such database can be found an instance gets started in a Docker container via Testcontainers.

We prefer a database based on TestContainers. Only if this can't be obtained do we try to access a local database for Postgres.

This minimizes the risk of accessing a database during tests that is not intended for that purpose.

If the system property `spring.data.r2dbc.test.preferLocalDatabase` is set to "true" the local database is preferred.

Original pull request: #51.
This commit is contained in:
Jens Schauder
2019-01-15 12:49:52 +01:00
committed by Mark Paluch
parent 3a1085e244
commit 6f8bca6135
3 changed files with 152 additions and 19 deletions

View File

@@ -24,6 +24,8 @@ import java.util.concurrent.TimeUnit;
import org.junit.AssumptionViolatedException;
import org.junit.rules.ExternalResource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* {@link ExternalResource} wrapper to encapsulate {@link ProvidedDatabase} and
@@ -33,6 +35,8 @@ import org.junit.rules.ExternalResource;
*/
public abstract class ExternalDatabase extends ExternalResource {
private static Logger LOG = LoggerFactory.getLogger(ExternalDatabase.class);
/**
* @return the post of the database service.
*/
@@ -53,18 +57,37 @@ public abstract class ExternalDatabase extends ExternalResource {
*/
public abstract String getUsername();
/**
* Throws an {@link AssumptionViolatedException} if the database cannot be reached.
*/
@Override
protected void before() {
try (Socket socket = new Socket()) {
socket.connect(new InetSocketAddress(getHostname(), getPort()), Math.toIntExact(TimeUnit.SECONDS.toMillis(5)));
} catch (IOException e) {
if (!checkValidity()) {
throw new AssumptionViolatedException(
String.format("Cannot connect to %s:%d. Skipping tests.", getHostname(), getPort()));
}
}
/**
* performs a test if the database can actually be reached.
*
* @return true, if the database could be reached.
*/
boolean checkValidity() {
try (Socket socket = new Socket()) {
socket.connect(new InetSocketAddress(getHostname(), getPort()), Math.toIntExact(TimeUnit.SECONDS.toMillis(5)));
return true;
} catch (IOException e) {
LOG.debug("external database not available.", e);
}
return false;
}
/**
* @return password for the database user.
*/
@@ -122,4 +145,42 @@ public abstract class ExternalDatabase extends ExternalResource {
return password;
}
}
/**
* An {@link ExternalDatabase} that couldn't get constructed.
*
* @author Jens Schauder
*/
static class NoSuchDatabase extends ExternalDatabase {
@Override
boolean checkValidity() {
return false;
}
@Override
public int getPort() {
throw new UnsupportedOperationException();
}
@Override
public String getHostname() {
throw new UnsupportedOperationException();
}
@Override
public String getDatabase() {
throw new UnsupportedOperationException();
}
@Override
public String getUsername() {
throw new UnsupportedOperationException();
}
@Override
public String getPassword() {
throw new UnsupportedOperationException();
}
}
}

View File

@@ -4,18 +4,24 @@ import io.r2dbc.postgresql.PostgresqlConnectionConfiguration;
import io.r2dbc.postgresql.PostgresqlConnectionFactory;
import io.r2dbc.spi.ConnectionFactory;
import java.util.function.Supplier;
import javax.sql.DataSource;
import org.postgresql.ds.PGSimpleDataSource;
import org.springframework.data.r2dbc.testing.ExternalDatabase.ProvidedDatabase;
import org.testcontainers.containers.PostgreSQLContainer;
/**
* Utility class for testing against Postgres.
*
* @author Mark Paluch
* @author Jens Schauder
*/
public class PostgresTestSupport {
private static ExternalDatabase testContainerDatabase;
public static String CREATE_TABLE_LEGOSET = "CREATE TABLE legoset (\n" //
+ " id integer CONSTRAINT id PRIMARY KEY,\n" //
+ " name varchar(255) NOT NULL,\n" //
@@ -31,30 +37,91 @@ public class PostgresTestSupport {
public static String INSERT_INTO_LEGOSET = "INSERT INTO legoset (id, name, manual) VALUES($1, $2, $3)";
/**
* Returns a locally provided database at {@code postgres:@localhost:5432/postgres}.
* Returns a database either hosted locally at {@code postgres:@localhost:5432/postgres} or running inside Docker.
*
* @return
* @return information about the database. Guaranteed to be not {@literal null}.
*/
public static ExternalDatabase database() {
return local();
if (Boolean.getBoolean("spring.data.r2dbc.test.preferLocalDatabase")) {
return getFirstWorkingDatabase( //
PostgresTestSupport::local, //
PostgresTestSupport::testContainer //
);
} else {
return getFirstWorkingDatabase( //
PostgresTestSupport::testContainer, //
PostgresTestSupport::local //
);
}
}
private static ExternalDatabase getFirstWorkingDatabase(Supplier<ExternalDatabase> first,
Supplier<ExternalDatabase> second) {
ExternalDatabase database = first.get();
if (database.checkValidity()) {
return database;
} else {
return second.get();
}
}
/**
* Returns a locally provided database at {@code postgres:@localhost:5432/postgres}.
*
* @return
*/
private static ExternalDatabase local() {
return ProvidedDatabase.builder().hostname("localhost").port(5432).database("postgres").username("postgres")
return ProvidedDatabase.builder() //
.hostname("localhost") //
.port(5432) //
.database("postgres") //
.username("postgres") //
.password("").build();
}
/**
* Returns a database provided via Testcontainers.
*/
private static ExternalDatabase testContainer() {
if (testContainerDatabase == null) {
try {
PostgreSQLContainer postgreSQLContainer = new PostgreSQLContainer();
postgreSQLContainer.start();
testContainerDatabase = ProvidedDatabase.builder() //
.hostname("localhost") //
.port(postgreSQLContainer.getFirstMappedPort()) //
.database(postgreSQLContainer.getDatabaseName()) //
.username(postgreSQLContainer.getUsername()) //
.password(postgreSQLContainer.getPassword()).build();
} catch (IllegalStateException ise) {
// docker is not available.
testContainerDatabase = new ExternalDatabase.NoSuchDatabase();
}
}
return testContainerDatabase;
}
/**
* Creates a new {@link ConnectionFactory} configured from the {@link ExternalDatabase}..
*/
public static ConnectionFactory createConnectionFactory(ExternalDatabase database) {
return new PostgresqlConnectionFactory(PostgresqlConnectionConfiguration.builder().host(database.getHostname())
.database(database.getDatabase()).username(database.getUsername()).password(database.getPassword()).build());
return new PostgresqlConnectionFactory(PostgresqlConnectionConfiguration.builder() //
.host(database.getHostname()) //
.database(database.getDatabase()) //
.port(database.getPort()) //
.username(database.getUsername()) //
.password(database.getPassword()) //
.build());
}
/**
@@ -72,4 +139,5 @@ public class PostgresTestSupport {
return dataSource;
}
}

View File

@@ -33,31 +33,35 @@ public class SqlServerTestSupport {
/**
* Returns a locally provided database at {@code sqlserver:@localhost:1433/master}.
*
* @return
*/
public static ExternalDatabase database() {
return local();
}
/**
* Returns a locally provided database at {@code postgres:@localhost:5432/postgres}.
*
* @return
* Returns a locally provided database at {@code sqlserver:@localhost:1433/master}.
*/
private static ExternalDatabase local() {
return ProvidedDatabase.builder().hostname("localhost").port(1433).database("master").username("sa")
.password("A_Str0ng_Required_Password").build();
return ProvidedDatabase.builder() //
.hostname("localhost") //
.port(1433) //
.database("master") //
.username("sa") //
.password("A_Str0ng_Required_Password") //
.build();
}
/**
* Creates a new {@link ConnectionFactory} configured from the {@link ExternalDatabase}..
*/
public static ConnectionFactory createConnectionFactory(ExternalDatabase database) {
return new MssqlConnectionFactory(MssqlConnectionConfiguration.builder().host(database.getHostname()) //
.database(database.getDatabase()) //
.username(database.getUsername()) //
.password(database.getPassword()) //
.port(database.getPort()) //
.build());
}