Testcontainers skill for Codex
Test against real Postgres, not a lookalike. Codex swaps H2, SQLite and mocked repositories for the same database, broker and cache engines production runs, started once per test run with real readiness checks and production migrations. It writes idiomatic setup for Spring Boot, Vitest or Jest global setup, pytest session fixtures, Go TestMain and xUnit fixtures, and fixes Docker plumbing for CI runners, Colima and Podman.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- testcontainers Installs to ~/.agents/skills/testcontainers/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install testcontainers The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project testcontainers Installs to .agents/skills/testcontainers/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/testcontainers.zipunzip -o testcontainers.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) testcontainers PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $testcontainers, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Runs real dependencies in tests with Testcontainers for Java, Node/TypeScript, Python, Go and .NET - PostgreSQL, MySQL, Redis, Kafka, LocalStack and generic images - with pinned image tags, correct wait strategies, one container per suite rather than per test, Spring Boot @ServiceConnection, reuse for local loops, and Docker setup for CI, Colima, Podman and sandboxes. Use when tests mock a database or broker they should exercise for real, when H2 or SQLite stands in for Postgres, when adding container-backed integration tests, or when Testcontainers fails to start in CI or locally.
Use it when
- Tests use an in-memory stand-in that behaves differently from production
- Adding integration tests for a repository, queue consumer or cache layer
- Container start-up makes the test suite slow
- Testcontainers fails to connect to Docker locally or in CI
Use something else when
- You are deciding test scope and database isolation strategy in general: use integration-testing
- You need a local dev environment rather than test dependencies: use docker-compose-dev
How to check the result
The suite passes twice in a row without manual cleanup, docker ps shows no leftover containers, and a deliberately broken SQL statement fails a test the old in-memory fake let through.
The mistake it prevents
Starting a new container for every test method by declaring it as an instance field, which turns a two-minute suite into a twenty-minute one.
Done well means: integration tests talk to the same database, broker or cache engine and major version as production, containers start once per test run (not per test), readiness is awaited by a real wait strategy, and the suite runs the same on a laptop and in CI.
Inspect first
- Read
AGENTS.md, the build file (pom.xml,build.gradle(.kts),package.json,pyproject.toml,go.mod,*.csproj) and any existingdocker-compose.ymlfor the service versions production uses. - Check Docker is reachable:
docker info. In a Codex sandbox the Docker socket and image pulls usually need escalated permissions and network; ask for approval rather than working around it. If Docker is not available at all, say so and stop. - Find the current test doubles: H2, SQLite in-memory,
mongodb-memory-server, mocked repositories. List which behaviours they cannot reproduce (JSONB,ON CONFLICT, transaction isolation, collation, Kafka partitions). - Find how the app reads connection settings so the container can be injected through configuration, not code changes.
Decide scope
| Question | Default |
|---|---|
| One container per test, class or run? | Per run (or per worker) with data isolation inside; per-test containers cost seconds each |
| Data isolation | Transaction rollback, truncate between tests, or a database per worker (see integration-testing) |
| Schema | Run the real migrations once after start; never ddl-auto=create |
| Image tag | Same major/minor as production, pinned (postgres:16.4-alpine); never latest |
| Many services | A few targeted containers, or DockerComposeContainer/ComposeContainer when the topology matters |
Java and Spring Boot
@SpringBootTest
@Testcontainers
class OrderRepositoryIT {
@Container
@ServiceConnection // Spring Boot 3.1+: wires spring.datasource.* automatically
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16.4-alpine");
@Autowired OrderRepository orders;
@Test
void upsertKeepsOneRowPerReference() { /* ... */ }
}
staticcontainers start once per class; for one per JVM, declare them in a shared base class or anApplicationContextInitializer, started in a static block.- Without Spring Boot, register properties with
@DynamicPropertySource(registry.add("spring.datasource.url", postgres::getJdbcUrl)) or passgetJdbcUrl(),getUsername(),getPassword()to your pool. - Testcontainers for Java 2.x renamed artifacts to
testcontainers-<module>(for exampleorg.testcontainers:testcontainers-postgresql), moved classes toorg.testcontainers.<module>packages and dropped JUnit 4. Match imports to the version in the build file; OpenRewrite has a migration recipe.
Node / TypeScript
import { PostgreSqlContainer, type StartedPostgreSqlContainer } from "@testcontainers/postgresql";
let pg: StartedPostgreSqlContainer;
export async function setup() { // Vitest globalSetup or Jest globalSetup
pg = await new PostgreSqlContainer("postgres:16.4-alpine").start();
process.env.DATABASE_URL = pg.getConnectionUri();
await runMigrations(process.env.DATABASE_URL);
}
export async function teardown() {
await pg?.stop();
}
Generic images: new GenericContainer("redis:7.4-alpine").withExposedPorts(6379) .withWaitStrategy(Wait.forLogMessage("Ready to accept connections")).start(), then
container.getHost() and container.getMappedPort(6379).
Python (pytest)
import pytest
from testcontainers.postgres import PostgresContainer
@pytest.fixture(scope="session")
def database_url():
with PostgresContainer("postgres:16.4-alpine", driver="psycopg") as pg:
url = pg.get_connection_url()
run_migrations(url)
yield url
The driver argument controls the SQLAlchemy URL scheme; check it matches the installed
driver (psycopg 3 vs psycopg2). Use scope="session", and with pytest-xdist create one
database per worker inside the shared container.
Go
func TestMain(m *testing.M) {
ctx := context.Background()
ctr, err := postgres.Run(ctx, "postgres:16.4-alpine",
postgres.WithDatabase("app"), postgres.WithUsername("app"), postgres.WithPassword("secret"),
postgres.BasicWaitStrategies(),
)
if err != nil {
log.Fatal(err)
}
dsn, _ = ctr.ConnectionString(ctx, "sslmode=disable")
code := m.Run()
_ = testcontainers.TerminateContainer(ctr)
os.Exit(code)
}
Inside a single test, testcontainers.CleanupContainer(t, ctr) registers termination.
.NET (xUnit)
public sealed class DbFixture : IAsyncLifetime
{
public PostgreSqlContainer Pg { get; } = new PostgreSqlBuilder().WithImage("postgres:16.4-alpine").Build();
public Task InitializeAsync() => Pg.StartAsync();
public Task DisposeAsync() => Pg.DisposeAsync().AsTask();
}
Share it with ICollectionFixture<DbFixture>; read Pg.GetConnectionString().
Wait strategies
Port open is not ready. Postgres opens its port during init and restarts once. Use the module defaults when a module exists; for generic images choose:
| Service type | Wait for |
|---|---|
| Database with a module | Module default (log message plus port) |
| HTTP service | Wait.forHttp("/health").forStatusCode(200) / forHttp equivalents |
Image with a Docker HEALTHCHECK | Health check wait |
| Logs a ready line | Log message, with the expected occurrence count |
Local speed
- Reuse containers between runs on a laptop: set
testcontainers.reuse.enable=truein~/.testcontainers.propertiesand call.withReuse(true)(Java) or.withReuse()(Node). Never enable reuse in CI, and reset data at the start of the run because state survives. - Pull images once ahead of time (
docker pull postgres:16.4-alpine) so the first test run is not dominated by a download.
CI and alternative runtimes
- GitHub Actions
ubuntu-latesthas Docker; macOS and Windows hosted runners do not run Linux containers. Run container tests on Linux runners. - Colima: export
DOCKER_HOST=unix://$HOME/.colima/default/docker.sockandTESTCONTAINERS_DOCKER_SOCKET_OVERRIDE=/var/run/docker.sock. - Podman: point
DOCKER_HOSTat the Podman socket; if the Ryuk reaper cannot start,TESTCONTAINERS_RYUK_DISABLED=trueworks but then the suite must stop its own containers. - Docker-in-Docker CI (GitLab): set
DOCKER_HOST=tcp://docker:2375and the host overrideTESTCONTAINERS_HOST_OVERRIDE=docker.
Verification
- Run the suite twice in a row; the second run must pass without manual cleanup.
docker psafter the run shows no leftover test containers (except deliberate reuse).- Break a migration or a SQL statement on purpose; the container-backed test must fail where the old in-memory fake passed.
- Record start-up time; if container start dominates, move to per-run containers.
Gotchas
- Hard-coding port 5432: Testcontainers maps to a random host port. Always read the mapped port or connection string.
latesttags make CI break on upstream releases and differ from production.- Creating the container in a non-static field (JUnit) starts one per test method.
- Apple Silicon: some images lack arm64 builds and run under emulation, slowly. Prefer multi-arch official images.
- Testcontainers proves engine behaviour, not configuration: production settings
(
max_connections, extensions, collation) must be set on the container too.
What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Testcontainers"
short_description: "Test against real Postgres, not a lookalike"
default_prompt: "$testcontainers Replace H2 in our Spring Boot tests with a real Postgres via Testcontainers" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.