agentsclimarketplace

Testcontainers expert

Skill Ortus-Solutions/skills/testcontainers-expert

The official Ortus AI Skills repo

Install
npx -y skills add Ortus-Solutions/skills --skill testcontainers-expert

Assembled 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

LifecycleAnnotation comboContainer scope
Per test class (shared)static @Containerstarted once, stopped after class
Per test methodinstance @Containerstarted and stopped per test
Global (singleton)Singleton pattern (see below)started once, reused across classes
Manualcontainer.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

TechniqueImpactWhen to Use
Static @ContainerHighAlways, unless fresh state needed per test
Singleton patternVery HighWhen multiple test classes share infrastructure
withReuse(true)Very HighLocal dev; combine with .testcontainers.properties
withImagePullPolicy(CACHED)MediumCI with warm Docker cache
Parallel test executionHighIndependent test classes
Shared NetworkLowWhen 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

InfrastructureModuleImage recommendation
PostgreSQLtestcontainers:postgresqlpostgres:16-alpine
MySQLtestcontainers:mysqlmysql:8.0
MongoDBtestcontainers:mongodbmongo:7.0
Kafkatestcontainers:kafkaconfluentinc/cp-kafka:7.6.0
RabbitMQtestcontainers:rabbitmqrabbitmq:3.13-management
RedisGenericContainerredis:7-alpine
Elasticsearchtestcontainers:elasticsearchdocker.elastic.co/elasticsearch/elasticsearch:8.13.0
LocalStacktestcontainers:localstacklocalstack/localstack:3.0
WireMockGenericContainerwiremock/wiremock:3.5.4

Constraints

MUST DO

  • Use static @Container by default to share containers across test methods
  • Apply the Singleton pattern for containers shared across test classes
  • Use dedicated Testcontainers modules over GenericContainer when available
  • Always configure an appropriate waitingFor strategy
  • 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 @BeforeAll with a static container
  • Do not hardcode host or port — always use container.getHost() and container.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-expert
  • mockito-expert
  • java-expert
  • code-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.

Keep looking

Skills are one crate of 327,069. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.