Integration Testing skill for Codex
Test the seams where production actually breaks. Codex writes integration tests around real boundaries such as SQL, transactions, framework wiring, queue consumers and outbound clients, using the real engine for what you own and HTTP stubs for what you do not. It picks a database isolation strategy that fits the code (rollback, truncation, per-worker template databases), covers duplicate delivery and partial failure, and proves each test fails when its boundary breaks.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- integration-testing Installs to ~/.agents/skills/integration-testing/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install integration-testing 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 integration-testing Installs to .agents/skills/integration-testing/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/integration-testing.zipunzip -o integration-testing.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))) integration-testing 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 $integration-testing, 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
Designs integration tests that exercise real seams - HTTP handlers through the database, repositories against the real engine, queue consumers, external API clients - choosing what is real and what is faked, isolating database state per test or per worker (transaction rollback, truncation, template databases), running migrations once, and keeping suites parallel and fast. Use when unit tests pass but production breaks at the boundaries, when deciding what an integration test should cover, when tests share or leak database state, or when an integration suite is slow or order-dependent.
Use it when
- Unit tests pass but bugs appear at database, HTTP or queue boundaries
- Integration tests share state, depend on order or cannot run in parallel
- Deciding the scope of a new integration test
- Framework transaction wrappers hide commit-time behaviour
Use something else when
- You need container setup details for a specific language: use testcontainers
- You are testing a deployed HTTP API from the outside: use api-testing
- You are verifying another team's API contract: use contract-testing
How to check the result
The suite passes twice in random order at CI parallelism, and dropping the index, check or field each test protects makes that test fail.
The mistake it prevents
Relying on per-test transaction rollback while the code under test commits on its own connection, so tests see nothing or skip the commit-time constraints they were meant to check.
An integration test is worth its cost when it crosses a boundary where bugs actually live (SQL, serialisation, transactions, framework wiring, configuration), uses the real thing on our side of that boundary, fakes only what we do not own, and leaves no state behind for the next test.
Inspect first
- Read
AGENTS.mdfor the test commands and how integration tests are separated (folder, tag, file suffix such as*IT.java,*.int.test.ts,-m integration). - List the system’s boundaries: database, cache, queue, object storage, third-party HTTP APIs, filesystem, clock, email/SMS.
- Read the existing test setup: how the DB is created, how state is reset, whether tests run in parallel, how long the suite takes.
- Check recent production incidents or bug tickets: integration tests should cover the classes of failure that actually happened.
Decide what is real
| Dependency | In integration tests | Why |
|---|---|---|
| Your database | Real engine and version (container or CI service) | SQL dialect, constraints, isolation and JSON behaviour differ in fakes |
| Your cache, queue, search engine | Real, containerised | Serialisation, TTLs, ordering, acknowledgements |
| Your framework (routing, DI, middleware, auth) | Real, in-process | Wiring and config bugs are the target |
| Another team’s service | Stub at HTTP (MSW, WireMock, respx) plus contract tests | You cannot control its data or uptime |
| Third-party SaaS (Stripe, Twilio, S3) | Stub at HTTP or official local emulator (LocalStack, stripe-mock) | Cost, rate limits, side effects |
| Clock, randomness | Injected and controlled | Determinism |
| Email, SMS, push | In-memory adapter or local catcher (Mailpit) | Never send real messages from tests |
Use testcontainers for container plumbing and api-mocking-msw for HTTP stubs.
Pick the slice
| Slice | Covers | Example |
|---|---|---|
| Repository / DAO | Queries, mappings, constraints, migrations | OrderRepository.upsert keeps one row per reference under conflict |
| Service + DB | Transactions, rollback on error, locking | Failed payment leaves stock unchanged |
| HTTP in-process | Routing, validation, auth middleware, serialisation, error mapping | POST /orders without a token returns 401 problem+json |
| Consumer | Deserialisation, idempotency, retry/dead-letter behaviour | Same message twice produces one order |
| Outbound client | Request shape, timeouts, error mapping | Provider 429 becomes a retryable error with backoff |
Test the HTTP layer in process (supertest, FastAPI TestClient/httpx ASGITransport,
Spring MockMvc or WebTestClient, Go httptest.NewServer) rather than over a deployed
URL; that is api-testing or E2E territory.
Database isolation strategies
| Strategy | Speed | Works when | Breaks when |
|---|---|---|---|
| Wrap each test in a transaction, roll back | Fastest | Code under test uses the same connection | Code opens its own transactions/connections, uses COMMIT-time behaviour (deferred constraints, triggers on commit, after-commit hooks, LISTEN/NOTIFY) |
| Truncate touched tables after each test | Fast | Small schemas | Many tables; parallel tests sharing a DB |
| Database per worker from a template | Fast and parallel | Postgres (CREATE DATABASE w1 TEMPLATE app_template) | Engines without cheap cloning |
| Schema per worker | Parallel | Apps that honour search_path / schema config | Code hard-codes schema names |
| Unique data per test, never clean | Parallel, simple | Append-only or tenant-scoped domains | Tests that count all rows |
Default for Postgres: run migrations once into app_template, then create one database per
worker from it, and roll back or truncate within the worker.
-- once per run, after migrations (marking it a template lets non-owners clone it)
ALTER DATABASE app_template IS_TEMPLATE true;
-- per worker (no active connections to the template allowed)
CREATE DATABASE app_w3 TEMPLATE app_template;
Framework defaults to know: Django TestCase wraps each test in a transaction (use
TransactionTestCase when the code commits or uses on_commit; or
captureOnCommitCallbacks); Rails transactional tests roll back by default; Spring
@Transactional on a test rolls back but hides lazy-loading and commit-time errors.
Workflow
- Write down the boundary and the failure the test protects against
- Choose the slice and what is real vs stubbed using the tables above
- Set up data with factories inside the test (see
test-data-factories), not shared fixtures - Act through the public entry point of the slice (HTTP call, service method, message)
- Assert on externally visible results: response, persisted rows, emitted messages, outbound requests; not on internal method calls
- Include the unhappy paths that matter: constraint violation, timeout, duplicate delivery, partial failure, permission denied
- Run the new tests in random order and in parallel before committing
def test_duplicate_payment_webhook_is_processed_once(client, db, stripe_signature):
order = OrderFactory(status="pending", payment_intent="pi_123")
payload = webhook_payload("payment_intent.succeeded", "pi_123")
for _ in range(2):
r = client.post("/webhooks/stripe", content=payload,
headers={"Stripe-Signature": stripe_signature(payload)})
assert r.status_code == 200
db.refresh(order)
assert order.status == "paid"
assert db.query(LedgerEntry).filter_by(order_id=order.id).count() == 1
Keeping the suite fast
- Start containers and run migrations once per run, not per file.
- Parallelise by worker with isolated databases; target a suite under 5-10 minutes in CI.
- Keep integration tests focused: one boundary each. Business-rule permutations belong in unit tests below.
- Tag integration tests so developers can run unit tests alone in seconds.
Verification
- Run the suite twice, in random order, with the CI worker count. All green both times.
- Break the thing each test claims to protect (drop a unique index, remove the idempotency check, change a JSON field name) and confirm a test fails.
- Confirm no test depends on data created by another (
rgfor hard-coded ids; random order catches the rest).
Gotchas
- Transaction-rollback isolation silently skips commit-time behaviour; test the commit-dependent paths with truncation instead.
- Asserting on
count(*)of a whole table fails as soon as tests run in parallel; scope queries to the data the test created. - Seeding a big shared dataset “for realism” creates coupling; each test creates its own.
- Stubbing your own database layer in an “integration” test turns it into a slow unit test.
- Tests that pass only with a specific
TZor locale on CI: set them explicitly in the test environment, matching production.
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: "Integration Testing"
short_description: "Test the seams where production actually breaks"
default_prompt: "$integration-testing Write integration tests for our order service that hit a real Postgres" 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.