Testcontainers expert
The official Ortus AI Skills repo
npx -y skills add Ortus-Solutions/skills --skill testcontainers-expertAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Use when writing integration tests that require real infrastructure dependencies such as databases, message brokers, caches, cloud service emulators, or any Dockerized service. Invoke for container lifecycle management, reusable containers, network configuration, wait strategies, custom images, and Testcontainers modules for PostgreSQL, MySQL, Kafka, Redis, LocalStack, and more.
The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.
SKILL.md
20.4 KB, ~4.6k tokens by cl100k_base, as published. Nobody here has run it
Testcontainers Expert
Testcontainers specialist for reliable, reproducible Java integration tests against real infrastructure.
Role Definition
Designs integration test suites that use real infrastructure in Docker containers to eliminate the gap between mocked behavior and production reality. Applies Testcontainers patterns for lifecycle management, performance optimization, and CI compatibility without sacrificing test isolation.
When to Use This Skill
- Testing repository or DAO layers against a real database
- Validating message-driven flows with a real Kafka or RabbitMQ broker
- Testing against cloud service APIs with LocalStack (AWS) or Azurite (Azure)
- Running full-stack Spring Boot slice tests with live dependencies
- Replacing brittle embedded databases (H2) with production-equivalent engines
- Configuring Testcontainers in CI/CD pipelines
Dependency Setup
Maven (BOM approach — recommended)
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers-bom</artifactId>
<version>1.20.1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- Core -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<scope>test</scope>
</dependency>
<!-- JUnit 5 integration -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<!-- Module examples — add only what you need -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>kafka</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>localstack</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
Gradle
val testcontainersVersion = "1.20.1"
dependencies {
testImplementation(platform("org.testcontainers:testcontainers-bom:$testcontainersVersion"))
testImplementation("org.testcontainers:testcontainers")
testImplementation("org.testcontainers:junit-jupiter")
testImplementation("org.testcontainers:postgresql")
testImplementation("org.testcontainers:kafka")
testImplementation("org.testcontainers:localstack")
}
JUnit 5 Integration
@Testcontainers + @Container
@Testcontainers
class OrderRepositoryIT {
// static = shared across all tests in the class (faster)
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine")
.withDatabaseName("orders_test")
.withUsername("test")
.withPassword("test");
// instance = new container per test (more isolated, slower)
@Container
RedisContainer redis = new RedisContainer("redis:7-alpine");
@Test
void shouldPersistOrder() {
var dataSource = buildDataSource(postgres);
var repo = new OrderRepository(dataSource);
// ...
}
}
Rule: Use static @Container unless you need a fresh container state for every test.
Container Lifecycle
| Lifecycle | Annotation combo | Container scope |
|---|---|---|
| Per test class (shared) | static @Container | started once, stopped after class |
| Per test method | instance @Container | started and stopped per test |
| Global (singleton) | Singleton pattern (see below) | started once, reused across classes |
| Manual | container.start() / container.stop() | fully controlled |
Manual lifecycle (programmatic)
class ProductServiceIT {
static PostgreSQLContainer<?> postgres;
@BeforeAll
static void startInfrastructure() {
postgres = new PostgreSQLContainer<>("postgres:16-alpine")
.withReuse(true);
postgres.start();
}
@AfterAll
static void stopInfrastructure() {
if (postgres != null) postgres.stop();
}
}
Database Containers
PostgreSQL
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine")
.withDatabaseName("testdb")
.withUsername("user")
.withPassword("pass")
.withInitScript("db/schema.sql"); // run SQL on startup
// Access connection info
String jdbcUrl = postgres.getJdbcUrl(); // jdbc:postgresql://localhost:PORT/testdb
String username = postgres.getUsername();
String password = postgres.getPassword();
MySQL
static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0")
.withDatabaseName("testdb")
.withUsername("user")
.withPassword("pass");
MongoDB
static MongoDBContainer mongo = new MongoDBContainer("mongo:7.0");
// Access URI
String mongoUri = mongo.getReplicaSetUrl(); // mongodb://localhost:PORT/test
Microsoft SQL Server
static MSSQLServerContainer<?> mssql = new MSSQLServerContainer<>("mcr.microsoft.com/mssql/server:2022-latest")
.acceptLicense()
.withPassword("Strong!Password1");
Database initialization strategies
Option 1: withInitScript — run a classpath SQL file
.withInitScript("db/init.sql")
Option 2: Flyway / Liquibase — apply migrations automatically
@BeforeAll
static void runMigrations() {
Flyway.configure()
.dataSource(postgres.getJdbcUrl(), postgres.getUsername(), postgres.getPassword())
.load()
.migrate();
}
Option 3: @Sql (Spring) — run SQL per test method
@Test
@Sql("/test-data/orders.sql")
void shouldListOrders() { ... }
Spring Boot Integration
Approach 1: @DynamicPropertySource
@SpringBootTest
@Testcontainers
class OrderServiceIT {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine");
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.datasource.username", postgres::getUsername);
registry.add("spring.datasource.password", postgres::getPassword);
}
@Autowired
private OrderService orderService;
@Test
void shouldPersistAndRetrieveOrder() {
var placed = orderService.place(Order.of("item-1", 3));
var found = orderService.findById(placed.id());
assertThat(found).isPresent().get().extracting(Order::status)
.isEqualTo(OrderStatus.ACCEPTED);
}
}
Approach 2: Spring Boot 3.1+ ServiceConnection
@SpringBootTest
@Testcontainers
class OrderServiceIT {
@Container
@ServiceConnection
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine");
// No @DynamicPropertySource needed — Spring auto-configures the datasource
}
Approach 3: Reusable @TestConfiguration base class
@TestConfiguration(proxyBeanMethods = false)
public class TestInfrastructureConfiguration {
@Bean
@ServiceConnection
PostgreSQLContainer<?> postgresContainer() {
return new PostgreSQLContainer<>("postgres:16-alpine");
}
@Bean
@ServiceConnection
KafkaContainer kafkaContainer() {
return new KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:7.6.0"));
}
}
// Use in tests
@SpringBootTest
@Import(TestInfrastructureConfiguration.class)
class ApplicationIT { ... }
Singleton Container Pattern (Cross-class Reuse)
Avoid starting a new container per test class. Start once, share across all tests in a JVM run.
public abstract class PostgresIntegrationBase {
static final PostgreSQLContainer<?> POSTGRES;
static {
POSTGRES = new PostgreSQLContainer<>("postgres:16-alpine")
.withDatabaseName("testdb")
.withReuse(true);
POSTGRES.start();
}
@DynamicPropertySource
static void overrideProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", POSTGRES::getJdbcUrl);
registry.add("spring.datasource.username", POSTGRES::getUsername);
registry.add("spring.datasource.password", POSTGRES::getPassword);
}
}
// All tests extend the base
@SpringBootTest
class OrderRepositoryIT extends PostgresIntegrationBase { ... }
@SpringBootTest
class ProductRepositoryIT extends PostgresIntegrationBase { ... }
Enable container reuse in ~/.testcontainers.properties:
testcontainers.reuse.enable=true
Message Brokers
Kafka
static KafkaContainer kafka = new KafkaContainer(
DockerImageName.parse("confluentinc/cp-kafka:7.6.0")
);
// Access bootstrap servers
String bootstrapServers = kafka.getBootstrapServers();
// Spring Boot 3.1+ ServiceConnection
@Container
@ServiceConnection
static KafkaContainer kafka = new KafkaContainer(
DockerImageName.parse("confluentinc/cp-kafka:7.6.0")
);
RabbitMQ
static RabbitMQContainer rabbitmq = new RabbitMQContainer("rabbitmq:3.13-management")
.withVhost("test-vhost")
.withUser("admin", "admin");
// Access AMQP URL
String amqpUrl = rabbitmq.getAmqpUrl();
Redis
// Requires testcontainers-redis or use GenericContainer
static GenericContainer<?> redis = new GenericContainer<>("redis:7-alpine")
.withExposedPorts(6379)
.waitingFor(Wait.forListeningPort());
int redisPort = redis.getMappedPort(6379);
String redisHost = redis.getHost();
Cloud Service Emulators
LocalStack (AWS)
static LocalStackContainer localstack = new LocalStackContainer(
DockerImageName.parse("localstack/localstack:3.0")
).withServices(S3, SQS, DYNAMODB);
// Build AWS clients pointing to LocalStack
S3Client s3 = S3Client.builder()
.endpointOverride(localstack.getEndpoint())
.credentialsProvider(StaticCredentialsProvider.create(
AwsBasicCredentials.create(localstack.getAccessKey(), localstack.getSecretKey())
))
.region(Region.of(localstack.getRegion()))
.build();
Azurite (Azure Storage)
static GenericContainer<?> azurite = new GenericContainer<>("mcr.microsoft.com/azure-storage/azurite:3.30.0")
.withExposedPorts(10000, 10001, 10002)
.withCommand("azurite --blobHost 0.0.0.0 --queueHost 0.0.0.0 --tableHost 0.0.0.0")
.waitingFor(Wait.forListeningPort());
Wait Strategies
Wait strategies prevent test failures caused by containers not being ready.
// Wait for a port to be listening (default for most containers)
.waitingFor(Wait.forListeningPort())
// Wait for an HTTP endpoint to return 200
.waitingFor(Wait.forHttp("/health").forStatusCode(200))
// Wait for HTTPS
.waitingFor(Wait.forHttps("/health").allowInsecure())
// Wait for a log message
.waitingFor(Wait.forLogMessage(".*database system is ready.*", 1))
// Wait for all of the above
.waitingFor(new WaitAllStrategy()
.withStrategy(Wait.forListeningPort())
.withStrategy(Wait.forLogMessage(".*ready.*", 1))
.withStartupTimeout(Duration.ofSeconds(60))
)
// Custom wait with startup timeout
.waitingFor(Wait.forListeningPort()
.withStartupTimeout(Duration.ofMinutes(2)))
GenericContainer — Custom Images
Use when no dedicated Testcontainers module exists.
static GenericContainer<?> wiremock = new GenericContainer<>("wiremock/wiremock:3.5.4")
.withExposedPorts(8080)
.withClasspathResourceMapping("wiremock", "/home/wiremock", BindMode.READ_ONLY)
.waitingFor(Wait.forHttp("/__admin/health").forStatusCode(200));
int wiremockPort = wiremock.getMappedPort(8080);
String wiremockUrl = "http://" + wiremock.getHost() + ":" + wiremockPort;
Executing commands inside container
var result = container.execInContainer("pg_dump", "-U", "postgres", "testdb");
assertThat(result.getExitCode()).isZero();
String dump = result.getStdout();
Copying files to/from container
container.copyFileToContainer(MountableFile.forClasspathResource("data.json"), "/app/data.json");
container.copyFileFromContainer("/app/output.csv", "/tmp/local-output.csv");
Docker Compose
static DockerComposeContainer<?> environment = new DockerComposeContainer<>(
new File("src/test/resources/docker-compose.yml")
)
.withExposedService("postgres", 5432, Wait.forListeningPort())
.withExposedService("redis", 6379, Wait.forListeningPort())
.withLocalCompose(true); // use local `docker compose` binary
// Access service info
String postgresHost = environment.getServiceHost("postgres", 5432);
int postgresPort = environment.getServicePort("postgres", 5432);
Container Networking
// Put multiple containers on the same network
static Network network = Network.newNetwork();
static GenericContainer<?> appContainer = new GenericContainer<>("myapp:latest")
.withNetwork(network)
.withNetworkAliases("app")
.dependsOn(postgresContainer);
static PostgreSQLContainer<?> postgresContainer = new PostgreSQLContainer<>("postgres:16-alpine")
.withNetwork(network)
.withNetworkAliases("db");
// App container can reach DB via jdbc:postgresql://db:5432/testdb
Configuration and Environment Variables
new GenericContainer<>("myapp:latest")
.withEnv("APP_ENV", "test")
.withEnv("DB_URL", "jdbc:postgresql://db:5432/testdb")
.withEnv(Map.of(
"FEATURE_FLAG_X", "true",
"LOG_LEVEL", "DEBUG"
))
.withLabel("test-group", "integration");
Elasticsearch / OpenSearch
static ElasticsearchContainer elasticsearch = new ElasticsearchContainer(
DockerImageName.parse("docker.elastic.co/elasticsearch/elasticsearch:8.13.0")
)
.withEnv("xpack.security.enabled", "false")
.withEnv("discovery.type", "single-node");
String httpHostAddress = elasticsearch.getHttpHostAddress();
// e.g. localhost:9200
Performance Optimization
| Technique | Impact | When to Use |
|---|---|---|
Static @Container | High | Always, unless fresh state needed per test |
| Singleton pattern | Very High | When multiple test classes share infrastructure |
withReuse(true) | Very High | Local dev; combine with .testcontainers.properties |
withImagePullPolicy(CACHED) | Medium | CI with warm Docker cache |
| Parallel test execution | High | Independent test classes |
Shared Network | Low | When containers must communicate |
Reuse configuration (~/.testcontainers.properties)
testcontainers.reuse.enable=true
docker.client.strategy=org.testcontainers.dockerclient.UnixSocketClientProviderStrategy
CI/CD Considerations
GitHub Actions
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
- name: Run integration tests
run: ./mvnw verify -Pintegration-tests
# Docker is pre-installed on ubuntu-latest runners
# Testcontainers works out of the box
Ryuk (resource cleanup daemon)
Testcontainers starts a Ryuk container to clean up resources after tests. In restricted environments:
// Disable Ryuk (containers survive JVM exit — use with caution)
System.setProperty("testcontainers.ryuk.disabled", "true");
// OR via environment variable: TESTCONTAINERS_RYUK_DISABLED=true
Docker socket alternatives
# Use Podman socket
DOCKER_HOST=unix:///run/user/1000/podman/podman.sock
TESTCONTAINERS_RYUK_DISABLED=true
Full Spring Boot Integration Test Example
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Testcontainers
@Transactional
class OrderApiIT {
@Container
@ServiceConnection
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16-alpine")
.withInitScript("db/schema.sql");
@Container
@ServiceConnection
static KafkaContainer kafka = new KafkaContainer(
DockerImageName.parse("confluentinc/cp-kafka:7.6.0")
);
@Autowired
private TestRestTemplate restTemplate;
@Autowired
private OrderRepository orderRepository;
@Test
void shouldCreateOrderAndPublishEvent() throws Exception {
var request = new PlaceOrderRequest("item-1", 5);
var response = restTemplate.postForEntity("/api/orders", request, OrderResponse.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
assertThat(response.getBody()).isNotNull();
assertThat(response.getBody().status()).isEqualTo("ACCEPTED");
// Verify database state
var saved = orderRepository.findById(response.getBody().id());
assertThat(saved).isPresent();
// Verify Kafka event (consume with test consumer)
var consumer = buildTestConsumer(kafka.getBootstrapServers());
consumer.subscribe(List.of("orders.placed"));
var records = consumer.poll(Duration.ofSeconds(5));
assertThat(records.count()).isEqualTo(1);
assertThat(records.iterator().next().value()).contains("item-1");
}
}
Reference Guide
| Infrastructure | Module | Image recommendation |
|---|---|---|
| PostgreSQL | testcontainers:postgresql | postgres:16-alpine |
| MySQL | testcontainers:mysql | mysql:8.0 |
| MongoDB | testcontainers:mongodb | mongo:7.0 |
| Kafka | testcontainers:kafka | confluentinc/cp-kafka:7.6.0 |
| RabbitMQ | testcontainers:rabbitmq | rabbitmq:3.13-management |
| Redis | GenericContainer | redis:7-alpine |
| Elasticsearch | testcontainers:elasticsearch | docker.elastic.co/elasticsearch/elasticsearch:8.13.0 |
| LocalStack | testcontainers:localstack | localstack/localstack:3.0 |
| WireMock | GenericContainer | wiremock/wiremock:3.5.4 |
Constraints
MUST DO
- Use
static @Containerby default to share containers across test methods - Apply the Singleton pattern for containers shared across test classes
- Use dedicated Testcontainers modules over
GenericContainerwhen available - Always configure an appropriate
waitingForstrategy - Use Alpine-based images in CI to reduce pull time
MUST NOT DO
- Do not use H2 in-memory database as a substitute for PostgreSQL or MySQL in integration tests
- Do not start containers in
@BeforeEach— use@BeforeAllwith a static container - Do not hardcode host or port — always use
container.getHost()andcontainer.getMappedPort() - Do not disable Ryuk in production CI without explicit resource cleanup strategy
- Do not use
withReuse(true)in CI pipelines where container state may pollute runs
Output Templates
## Integration Test Infrastructure Plan
- Test class: [ClassName]
- Required containers: [list with images and modules]
- Lifecycle: [static/singleton/per-test with justification]
- Spring integration: [@DynamicPropertySource / @ServiceConnection]
- Wait strategy: [per container]
- Data setup: [initScript / Flyway / @Sql]
- CI considerations: [runner, Ryuk, pull policy]
Knowledge Reference
testcontainers 1.20, junit-jupiter integration, @testcontainers, @container, postgresqlcontainer, mysqlcontainer, mongodbcontainer, kafkacontainer, rabbitmqcontainer, localstackcontainer, genericcontainer, dockercomposecontainer, wait strategies, waitforlisteningport, waitforlogmessage, waitforhttp, singleton pattern, withreuse, servicecondition, @dynamicpropertysource, @serviceconnection, spring boot 3.1 integration, network, withnetworkaliases, dependson, execincontainer, copytofromcontainer, ryuk, ci github actions, podman socket, testcontainers.properties, flyway liquibase migration, alpine images, performance optimization
Related Skills
junit-expertmockito-expertjava-expertcode-reviewer
What ships with it
Read from the repository
Just SKILL.md. No reference files, no scripts.
Gives 0 of the 12 instructions most databases sql skills give in ~4.6k tokens
Counted across 589 of the 662 authors here whose files we hold, read 2026-08-07
- Use parameterized queriesin 37 of 589, across 34 files
- Use timestamptz for timestampsin 30 of 589, across 14 files
- Index foreign keysin 29 of 589, across 18 files
- Create indexes concurrentlyin 29 of 589, across 24 files
- Use numeric type for moneyin 25 of 589, across 8 files
- Use cursor pagination instead of offsetin 24 of 589, across 17 files
- Select only required columnsin 24 of 589, across 20 files
- Add indexes manually on foreign key columnsin 22 of 589, across 12 files
- Normalize to third normal formin 19 of 589, across 10 files
- Configure connection poolingin 19 of 589, across 17 files
- Put equality columns before range columns in indexesin 18 of 589, across 10 files
- Read individual rule files for detailed explanationsin 18 of 589, across 4 files
Said here and by no other author read
- use the testcontainers bill of materials
- use static container fields unless isolated state is required
- enable test reuse to speed up local development
- use the singleton container pattern to share containers
- apply wait strategies before running tests
- use generic containers when no dedicated module exists
Grouped from the skills themselves: near-identical wordings counted once, and counted by distinct author, so one author publishing three of these counts once. Length counted with cl100k_base; the agent that loads this file may tokenize it differently.