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.

  • Testing & QA
  • $testcontainers
  • 186 lines
  • 2 files
  • ~1,850 tokens
  • MIT
  • Updated

Install

curl -fsSL codexskills.dev/install.sh \  | sh -s -- testcontainers

Installs to ~/.agents/skills/testcontainers/ for every project on this machine.

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

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.

~/.agents/skills/testcontainers/SKILL.md

--- name: testcontainers description: Runs real dependencies in tests with Testcontainers for Java, Node/TypeScript, Python, Go and .NET - PostgreSQL, MySQL, Redis, Kafka, LocalStack and… license: MIT ---

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

  1. Read AGENTS.md, the build file (pom.xml, build.gradle(.kts), package.json, pyproject.toml, go.mod, *.csproj) and any existing docker-compose.yml for the service versions production uses.
  2. 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.
  3. 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).
  4. Find how the app reads connection settings so the container can be injected through configuration, not code changes.

Decide scope

QuestionDefault
One container per test, class or run?Per run (or per worker) with data isolation inside; per-test containers cost seconds each
Data isolationTransaction rollback, truncate between tests, or a database per worker (see integration-testing)
SchemaRun the real migrations once after start; never ddl-auto=create
Image tagSame major/minor as production, pinned (postgres:16.4-alpine); never latest
Many servicesA 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() { /* ... */ }
}
  • static containers start once per class; for one per JVM, declare them in a shared base class or an ApplicationContextInitializer, started in a static block.
  • Without Spring Boot, register properties with @DynamicPropertySource (registry.add("spring.datasource.url", postgres::getJdbcUrl)) or pass getJdbcUrl(), getUsername(), getPassword() to your pool.
  • Testcontainers for Java 2.x renamed artifacts to testcontainers-<module> (for example org.testcontainers:testcontainers-postgresql), moved classes to org.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 typeWait for
Database with a moduleModule default (log message plus port)
HTTP serviceWait.forHttp("/health").forStatusCode(200) / forHttp equivalents
Image with a Docker HEALTHCHECKHealth check wait
Logs a ready lineLog message, with the expected occurrence count

Local speed

  • Reuse containers between runs on a laptop: set testcontainers.reuse.enable=true in ~/.testcontainers.properties and 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-latest has 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.sock and TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE=/var/run/docker.sock.
  • Podman: point DOCKER_HOST at the Podman socket; if the Ryuk reaper cannot start, TESTCONTAINERS_RYUK_DISABLED=true works but then the suite must stop its own containers.
  • Docker-in-Docker CI (GitLab): set DOCKER_HOST=tcp://docker:2375 and the host override TESTCONTAINERS_HOST_OVERRIDE=docker.

Verification

  • Run the suite twice in a row; the second run must pass without manual cleanup.
  • docker ps after 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.
  • latest tags 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.yamlgenerated from this page

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.

esc