This document covers using MySQL containers with R2DBC for reactive, non-blocking database access with Project Reactor or other reactive frameworks.
R2DBC Classes:
MySQLR2DBCDatabaseContainer - Wrapper that adapts MySQLContainer for R2DBCMySQLR2DBCDatabaseContainerProvider - Provider for URL-based R2DBC container creationKey Methods:
MySQLR2DBCDatabaseContainer(MySQLContainer<?> container) - ConstructorMySQLR2DBCDatabaseContainer.getOptions(MySQLContainer<?> container): ConnectionFactoryOptions - Static method to get ConnectionFactoryOptionsconfigure(ConnectionFactoryOptions options): ConnectionFactoryOptions - Configure options from containerstart(): void / stop(): void - Lifecycle methods (delegated to wrapped container)Required Dependencies:
org.testcontainers:mysql:1.21.4 (this package)org.testcontainers:r2dbc:1.21.4 (R2DBC support)io.asyncer:r2dbc-mysql:1.0.0 (R2DBC MySQL driver)Important Notes:
The MySQL module provides full R2DBC (Reactive Relational Database Connectivity) support, allowing you to use MySQL containers with reactive applications. R2DBC enables asynchronous, non-blocking database operations with backpressure support through the Reactive Streams specification.
To use R2DBC support, add these dependencies to your project:
Gradle:
testImplementation 'org.testcontainers:mysql:1.21.4'
testImplementation 'org.testcontainers:r2dbc:1.21.4'
testImplementation 'io.asyncer:r2dbc-mysql:1.0.0'Maven:
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>mysql</artifactId>
<version>1.21.4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>r2dbc</artifactId>
<version>1.21.4</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.asyncer</groupId>
<artifactId>r2dbc-mysql</artifactId>
<version>1.0.0</version>
<scope>test</scope>
</dependency>Wrapper class that adapts a MySQLContainer for R2DBC usage.
/**
* Creates an R2DBC database container wrapper around an existing MySQLContainer.
* Delegates lifecycle management (start, stop) to the wrapped container.
* The wrapped MySQLContainer must be started before using R2DBC operations.
*
* @param container the MySQLContainer to wrap (must not be null)
*/
class MySQLR2DBCDatabaseContainer implements R2DBCDatabaseContainer {
MySQLR2DBCDatabaseContainer(MySQLContainer<?> container);
/**
* Creates R2DBC ConnectionFactoryOptions from a MySQLContainer.
* This is a convenience method that creates a wrapper and configures options.
* The container must be started before calling this method.
*
* @param container the MySQLContainer to get options from (must not be null, must be started)
* @return ConnectionFactoryOptions configured for the container
* @throws IllegalStateException if container is not started
*/
static ConnectionFactoryOptions getOptions(MySQLContainer<?> container);
/**
* Configures R2DBC ConnectionFactoryOptions with container connection details.
* Sets host, port, database name, username, and password from the container.
* The container must be started before calling this method.
*
* @param options the base ConnectionFactoryOptions to configure
* @return configured ConnectionFactoryOptions
* @throws IllegalStateException if wrapped container is not started
*/
ConnectionFactoryOptions configure(ConnectionFactoryOptions options);
/**
* Starts the wrapped MySQL container.
* Delegated to the wrapped MySQLContainer.start().
*
* @throws ContainerLaunchException if container fails to start
* @throws IllegalStateException if container cannot be accessed within timeout
*/
void start();
/**
* Stops the wrapped MySQL container.
* Delegated to the wrapped MySQLContainer.stop().
*/
void stop();
}Usage Example:
import org.testcontainers.containers.MySQLContainer;
import org.testcontainers.containers.MySQLR2DBCDatabaseContainer;
import org.testcontainers.utility.DockerImageName;
import io.r2dbc.spi.ConnectionFactory;
import io.r2dbc.spi.ConnectionFactories;
import io.r2dbc.spi.ConnectionFactoryOptions;
// Create MySQL container
MySQLContainer<?> mysqlContainer = new MySQLContainer<>(DockerImageName.parse("mysql:8.0"))
.withDatabaseName("testdb");
// Wrap for R2DBC usage
MySQLR2DBCDatabaseContainer r2dbcContainer = new MySQLR2DBCDatabaseContainer(mysqlContainer);
r2dbcContainer.start();
// Get R2DBC connection options
ConnectionFactoryOptions options = MySQLR2DBCDatabaseContainer.getOptions(mysqlContainer);
// Create R2DBC ConnectionFactory
ConnectionFactory connectionFactory = ConnectionFactories.get(options);
// Use with reactive code...
r2dbcContainer.stop();Factory class for creating R2DBC MySQL containers from ConnectionFactoryOptions.
class MySQLR2DBCDatabaseContainerProvider implements R2DBCDatabaseContainerProvider {
/**
* The R2DBC driver identifier for MySQL.
* Value: "mysql" (from MySqlConnectionFactoryProvider.MYSQL_DRIVER)
*/
static final String DRIVER = "mysql";
/**
* Checks if this provider supports the given connection options.
* Returns true if the driver option is "mysql".
*
* @param options the ConnectionFactoryOptions to check
* @return true if driver is "mysql", false otherwise
*/
boolean supports(ConnectionFactoryOptions options);
/**
* Creates an R2DBC database container from connection options.
* Extracts image tag, database name, and reuse settings from options.
*
* @param options the ConnectionFactoryOptions specifying container configuration
* @return an R2DBCDatabaseContainer instance
* @throws IllegalArgumentException if options are invalid
*/
R2DBCDatabaseContainer createContainer(ConnectionFactoryOptions options);
/**
* Returns connection factory metadata with default credentials if not specified.
* Adds default username "test" and password "test" if not present in options.
*
* @param options the ConnectionFactoryOptions to get metadata for
* @return ConnectionFactoryMetadata with defaults applied, or null
*/
ConnectionFactoryMetadata getMetadata(ConnectionFactoryOptions options);
}Usage Example:
import org.testcontainers.containers.MySQLR2DBCDatabaseContainerProvider;
import io.r2dbc.spi.ConnectionFactoryOptions;
import io.r2dbc.spi.Option;
// Create options for container creation
ConnectionFactoryOptions options = ConnectionFactoryOptions.builder()
.option(ConnectionFactoryOptions.DRIVER, "tc")
.option(Option.valueOf("tc-type"), "mysql")
.option(ConnectionFactoryOptions.DATABASE, "testdb")
.build();
// Provider is automatically discovered and used by ConnectionFactoriesimport org.testcontainers.containers.MySQLContainer;
import org.testcontainers.containers.MySQLR2DBCDatabaseContainer;
import org.testcontainers.utility.DockerImageName;
import io.r2dbc.spi.ConnectionFactory;
import io.r2dbc.spi.ConnectionFactories;
import io.r2dbc.spi.ConnectionFactoryOptions;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
public class R2DBCExample {
public void testReactiveDatabase() {
// Create and start container
MySQLContainer<?> mysql = new MySQLContainer<>(DockerImageName.parse("mysql:8.0"))
.withDatabaseName("reactivedb")
.withInitScript("schema.sql");
MySQLR2DBCDatabaseContainer r2dbc = new MySQLR2DBCDatabaseContainer(mysql);
r2dbc.start();
try {
// Get connection factory
ConnectionFactoryOptions options = MySQLR2DBCDatabaseContainer.getOptions(mysql);
ConnectionFactory connectionFactory = ConnectionFactories.get(options);
// Execute reactive queries
Flux<Integer> results = Flux.usingWhen(
connectionFactory.create(),
connection -> Flux.from(connection
.createStatement("SELECT id FROM users WHERE active = ?")
.bind(0, true)
.execute())
.flatMap(result -> result.map((row, metadata) ->
row.get("id", Integer.class)
)),
connection -> connection.close()
);
// Process results reactively
results.subscribe(id -> System.out.println("User ID: " + id));
} finally {
r2dbc.stop();
}
}
}import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.DynamicPropertyRegistry;
import org.springframework.test.context.DynamicPropertySource;
import org.testcontainers.containers.MySQLContainer;
import org.testcontainers.containers.MySQLR2DBCDatabaseContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.utility.DockerImageName;
import io.r2dbc.spi.ConnectionFactoryOptions;
@SpringBootTest
@Testcontainers
public class SpringR2DBCTest {
@Container
static MySQLContainer<?> mysql = new MySQLContainer<>(DockerImageName.parse("mysql:8.0"))
.withDatabaseName("springtest");
@DynamicPropertySource
static void registerProperties(DynamicPropertyRegistry registry) {
ConnectionFactoryOptions options = MySQLR2DBCDatabaseContainer.getOptions(mysql);
registry.add("spring.r2dbc.url", () ->
String.format("r2dbc:mysql://%s:%d/%s",
mysql.getHost(),
mysql.getMappedPort(MySQLContainer.MYSQL_PORT),
mysql.getDatabaseName()
)
);
registry.add("spring.r2dbc.username", mysql::getUsername);
registry.add("spring.r2dbc.password", mysql::getPassword);
}
// Spring R2DBC repositories and tests...
}import org.testcontainers.containers.MySQLContainer;
import org.testcontainers.utility.DockerImageName;
import io.r2dbc.spi.ConnectionFactory;
import io.r2dbc.spi.ConnectionFactories;
import io.r2dbc.spi.ConnectionFactoryOptions;
import reactor.core.publisher.Mono;
public void manualR2DBCConfiguration() {
MySQLContainer<?> mysql = new MySQLContainer<>(DockerImageName.parse("mysql:8.0"))
.withDatabaseName("manualdb");
mysql.start();
try {
// Manually build R2DBC connection options
ConnectionFactoryOptions options = ConnectionFactoryOptions.builder()
.option(ConnectionFactoryOptions.DRIVER, "mysql")
.option(ConnectionFactoryOptions.HOST, mysql.getHost())
.option(ConnectionFactoryOptions.PORT, mysql.getMappedPort(MySQLContainer.MYSQL_PORT))
.option(ConnectionFactoryOptions.DATABASE, mysql.getDatabaseName())
.option(ConnectionFactoryOptions.USER, mysql.getUsername())
.option(ConnectionFactoryOptions.PASSWORD, mysql.getPassword())
.build();
ConnectionFactory connectionFactory = ConnectionFactories.get(options);
// Use connection factory
Mono.from(connectionFactory.create())
.flatMapMany(connection ->
Mono.from(connection.createStatement("SELECT 1").execute())
.flatMapMany(result -> result.map((row, metadata) -> row.get(0, Integer.class)))
.doFinally(signalType -> connection.close())
)
.subscribe(value -> System.out.println("Result: " + value));
} finally {
mysql.stop();
}
}import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.MySQLContainer;
import org.testcontainers.containers.MySQLR2DBCDatabaseContainer;
import org.testcontainers.utility.DockerImageName;
import io.r2dbc.spi.ConnectionFactory;
import io.r2dbc.spi.ConnectionFactories;
import io.r2dbc.spi.ConnectionFactoryOptions;
import reactor.test.StepVerifier;
import reactor.core.publisher.Flux;
public class R2DBCJUnit5Test {
private static MySQLContainer<?> mysql;
private static MySQLR2DBCDatabaseContainer r2dbc;
private static ConnectionFactory connectionFactory;
@BeforeAll
static void setUp() {
mysql = new MySQLContainer<>(DockerImageName.parse("mysql:8.0"))
.withDatabaseName("junit5test");
r2dbc = new MySQLR2DBCDatabaseContainer(mysql);
r2dbc.start();
ConnectionFactoryOptions options = MySQLR2DBCDatabaseContainer.getOptions(mysql);
connectionFactory = ConnectionFactories.get(options);
}
@AfterAll
static void tearDown() {
r2dbc.stop();
}
@Test
void testReactiveQuery() {
Flux.usingWhen(
connectionFactory.create(),
connection -> Flux.from(connection
.createStatement("SELECT 1 as value")
.execute())
.flatMap(result -> result.map((row, metadata) ->
row.get("value", Integer.class)
)),
connection -> connection.close()
)
.as(StepVerifier::create)
.expectNext(1)
.verifyComplete();
}
}import io.r2dbc.spi.Connection;
import io.r2dbc.spi.ConnectionFactory;
import reactor.core.publisher.Mono;
public void transactionalOperation(ConnectionFactory connectionFactory) {
Mono.usingWhen(
connectionFactory.create(),
connection -> Mono.from(connection.beginTransaction())
.then(Mono.from(connection.createStatement(
"INSERT INTO accounts (name, balance) VALUES (?, ?)")
.bind(0, "Alice")
.bind(1, 1000)
.execute()))
.then(Mono.from(connection.createStatement(
"INSERT INTO accounts (name, balance) VALUES (?, ?)")
.bind(0, "Bob")
.bind(1, 500)
.execute()))
.then(Mono.from(connection.commitTransaction()))
.onErrorResume(error ->
Mono.from(connection.rollbackTransaction())
.then(Mono.error(error))
),
Connection::close
)
.subscribe();
}import org.testcontainers.containers.MySQLContainer;
import org.testcontainers.containers.MySQLR2DBCDatabaseContainer;
import org.testcontainers.utility.DockerImageName;
// Enable container reuse for faster test execution
MySQLContainer<?> mysql = new MySQLContainer<>(DockerImageName.parse("mysql:8.0"))
.withDatabaseName("reusabledb")
.withReuse(true); // Enable reuse
MySQLR2DBCDatabaseContainer r2dbc = new MySQLR2DBCDatabaseContainer(mysql);
r2dbc.start();
// Container will be reused across test runsR2DBC URLs for MySQL follow this format:
r2dbc:mysql://[host]:[port]/[database]?[options]Example:
r2dbc:mysql://localhost:32768/testdbThe MySQLR2DBCDatabaseContainer handles URL construction automatically based on container settings.
usingWhen to ensure connections are closedR2DBC Driver Not Found: Ensure io.asyncer:r2dbc-mysql is on your classpath
Connection Timeout: Increase container startup timeout:
mysql.withStartupTimeoutSeconds(180);Port Mapping Issues: Ensure you're using the mapped port from the container:
mysql.getMappedPort(MySQLContainer.MYSQL_PORT)Transaction Issues: Verify you're properly committing/rolling back transactions in reactive flows
Backpressure Issues: Use Project Reactor's backpressure operators (onBackpressureBuffer, onBackpressureDrop) when needed
IllegalStateException: Ensure container is started before calling getOptions() or configure()