seed-skills/testcontainers-reuse-node/SKILL.md
Teaches the agent to speed up Node integration tests with Testcontainers reuse — withReuse(true), TESTCONTAINERS_REUSE_ENABLE, the .testcontainers.properties opt-in, stable hashing for Postgres/MySQL/Kafka, and Ryuk/CI caveats.
npx skillsauth add PramodDutta/qaskills Testcontainers Reuse (Node)Install this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
This skill makes the agent use Testcontainers' reuse feature to cut local integration-test startup from seconds to milliseconds, while avoiding the footguns: reuse is an explicit developer opt-in (it is off in CI by design), it requires a stable container configuration to hash, and a reused container is not cleaned up by Ryuk, so test data must be reset by the test, not the container lifecycle.
Use this skill when local integration tests are slow because every run boots a fresh Postgres/MySQL/Kafka, or when the user asks about withReuse, TESTCONTAINERS_REUSE_ENABLE, or "keep the container alive between runs."
withReuse(true) is set AND testcontainers.reuse.enable=true is in the user's ~/.testcontainers.properties (or TESTCONTAINERS_REUSE_ENABLE=true). It should stay off in CI, where clean state matters more than speed..stop() are mutually exclusive in intent. Do not call .stop() in teardown for a reused container, or you defeat reuse on the next run.Reuse needs a machine-level opt-in. The agent should instruct the user to create this file (it is intentionally not committed):
# ~/.testcontainers.properties
testcontainers.reuse.enable=true
Equivalent for the current shell (useful in scripts, NOT in CI):
export TESTCONTAINERS_REUSE_ENABLE=true
Without this, withReuse(true) is silently ignored and a fresh container starts every time.
withReuse(true) plus a fixed name/labels. The data reset (TRUNCATE) is what makes reuse safe across runs.
import { PostgreSqlContainer, type StartedPostgreSqlContainer } from '@testcontainers/postgresql';
import { Client } from 'pg';
let container: StartedPostgreSqlContainer;
let client: Client;
beforeAll(async () => {
container = await new PostgreSqlContainer('postgres:16-alpine')
.withDatabase('appdb')
.withUsername('test')
.withPassword('test')
// A stable label set keeps the config hash constant -> the same
// container is reused on the next run.
.withLabels({ project: 'my-app', purpose: 'integration' })
.withReuse() // <-- opt into reuse
.start();
client = new Client({ connectionString: container.getConnectionUri() });
await client.connect();
// Schema is created idempotently because the container may already exist.
await client.query(`
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
email TEXT UNIQUE NOT NULL
);
`);
});
beforeEach(async () => {
// Reset DATA, not the container. This is the key to safe reuse.
await client.query('TRUNCATE TABLE users RESTART IDENTITY CASCADE');
});
afterAll(async () => {
await client.end();
// IMPORTANT: do NOT call container.stop() — that kills the reusable container.
});
test('inserts and reads a user', async () => {
await client.query(`INSERT INTO users (email) VALUES ('[email protected]')`);
const { rows } = await client.query('SELECT email FROM users');
expect(rows).toEqual([{ email: '[email protected]' }]);
});
Reuse shines when several test files would each spin up their own DB. A small singleton returns the same started container; the hash makes them converge on one Docker container.
// test/support/postgres.ts
import { PostgreSqlContainer, type StartedPostgreSqlContainer } from '@testcontainers/postgresql';
let started: Promise<StartedPostgreSqlContainer> | undefined;
export function getPostgres(): Promise<StartedPostgreSqlContainer> {
if (!started) {
started = new PostgreSqlContainer('postgres:16-alpine')
.withDatabase('appdb')
.withUsername('test')
.withPassword('test')
.withReuse()
.start();
}
return started;
}
// any.integration.test.ts
import { getPostgres } from './support/postgres';
test('uses the shared reusable container', async () => {
const pg = await getPostgres();
expect(pg.getConnectionUri()).toContain('appdb');
});
Same shape, different module. Idempotent schema + per-test reset.
import { MySqlContainer, type StartedMySqlContainer } from '@testcontainers/mysql';
import mysql from 'mysql2/promise';
let container: StartedMySqlContainer;
beforeAll(async () => {
container = await new MySqlContainer('mysql:8.4')
.withDatabase('appdb')
.withUsername('test')
.withUserPassword('test')
.withReuse()
.start();
const conn = await mysql.createConnection(container.getConnectionUri());
await conn.query(`CREATE TABLE IF NOT EXISTS orders (id INT PRIMARY KEY AUTO_INCREMENT, sku VARCHAR(64))`);
await conn.end();
});
beforeEach(async () => {
const conn = await mysql.createConnection(container.getConnectionUri());
await conn.query('TRUNCATE TABLE orders');
await conn.end();
});
Kafka boot is expensive, so reuse pays off most here. Reset by deleting topics rather than restarting the broker.
import { KafkaContainer, type StartedKafkaContainer } from '@testcontainers/kafka';
import { Kafka } from 'kafkajs';
let container: StartedKafkaContainer;
let kafka: Kafka;
beforeAll(async () => {
container = await new KafkaContainer('confluentinc/cp-kafka:7.6.1')
.withReuse()
.start();
kafka = new Kafka({ brokers: [`${container.getHost()}:${container.getMappedPort(9093)}`] });
});
beforeEach(async () => {
// Reset state by recreating the topic, not the broker.
const admin = kafka.admin();
await admin.connect();
const topics = await admin.listTopics();
if (topics.includes('events')) {
await admin.deleteTopics({ topics: ['events'] });
}
await admin.createTopics({ topics: [{ topic: 'events', numPartitions: 1 }] });
await admin.disconnect();
});
test('produces and consumes an event', async () => {
const producer = kafka.producer();
await producer.connect();
await producer.send({ topic: 'events', messages: [{ value: 'hello' }] });
await producer.disconnect();
// ...consume and assert...
});
In CI, isolation beats speed and reused containers can poison subsequent jobs. Gate the behavior on environment.
import { PostgreSqlContainer } from '@testcontainers/postgresql';
const isCI = process.env.CI === 'true';
const builder = new PostgreSqlContainer('postgres:16-alpine')
.withDatabase('appdb')
.withUsername('test')
.withPassword('test');
// Only reuse locally. In CI, start fresh and let Ryuk reap it.
const container = await (isCI ? builder : builder.withReuse()).start();
~/.testcontainers.properties with testcontainers.reuse.enable=true) in the project README — without it, withReuse() is a no-op.beforeEach (TRUNCATE / recreate topics), never by stopping the container. A reused container persists data by design..stop() in afterAll for reusable containers; closing only the client connection is enough.CI env gate) so jobs get clean, Ryuk-reaped containers and never inherit stale state.CREATE TABLE IF NOT EXISTS) because the container may already exist from a previous run.withReuse() but expecting CI to reuse. CI usually lacks the opt-in (and should) — reuse silently disables, masking the intent. Gate it on environment instead.container.stop() in teardown for a reusable container — it kills the very container the next run wanted to reuse.postgres:latest) — the hash and the pulled image drift, breaking reproducible reuse.docker rm).withReuse(true) / TESTCONTAINERS_REUSE_ENABLE?".testcontainers.properties go?"development
Build WebdriverIO E2E suites — wdio.conf.ts setup, $ and $$ selectors, auto-wait and waitUntil, Mocha framework structure, page objects, parallel capabilities, and services for visual testing and Appium mobile.
testing
Test Vue 3 components with Vue Test Utils and Vitest — mount vs shallowMount, finding and triggering DOM, asserting props and emitted events, awaiting async updates, and mocking Pinia stores and Vue Router.
testing
Write fast unit and integration tests with Vitest — vitest.config.ts setup, vi.fn and vi.mock module mocking, fake timers, snapshots, V8 coverage with thresholds, workspaces for monorepos, and in-source testing.
development
Practice strict red-green-refactor test-driven development — write one failing test first, make it pass with the minimum code, then refactor under green, with worked cycles in Jest and pytest, AAA structure, and behavior-based test naming.