Skip to content

Commit bc29649

Browse files
committed
docs(testcontainers): document fixture choice + shard timings workflow
1 parent d639dc5 commit bc29649

2 files changed

Lines changed: 75 additions & 2 deletions

File tree

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,74 @@
1-
# Test container
1+
# Test containers
22

3-
This is package exposes some useful vitest utilities for writing tests with Postgres, Prisma, and Redis.
3+
Vitest utilities for writing tests against real Postgres, Prisma, Redis and ClickHouse - we don't mock
4+
(see the root `CLAUDE.md`), we boot containers. Also exposes a duration-weighted shard sequencer for
5+
splitting slow suites across CI shards.
6+
7+
## Choosing a fixture
8+
9+
Most tests share one set of containers per vitest worker (booted once, reset between tests) - this is
10+
much faster than a container per test. Reach for an isolated variant only when a test needs it.
11+
12+
| Fixture | Postgres | Redis | ClickHouse | Use for |
13+
| --- | --- | --- | --- | --- |
14+
| `redisTest` | - | shared | - | redis-only tests |
15+
| `postgresTest` | shared (clone) | - | - | db-only tests |
16+
| `containerTest` | shared (clone) | shared | shared | the default - needs all three |
17+
| `isolatedRedisTest` | - | per-test | - | background redis work (see below) |
18+
| `containerTestWithIsolatedRedis` | shared (clone) | per-test | shared | background redis work + db/clickhouse |
19+
| `replicationContainerTest` | per-test | per-test | shared | Postgres→ClickHouse logical replication |
20+
21+
"shared (clone)" = one Postgres per worker with a template database; each test gets a fast `CREATE
22+
DATABASE ... TEMPLATE` clone, so schema isn't re-pushed per test.
23+
24+
### The background-work gotcha
25+
26+
If a test spawns work that **outlives the test body** - a `RunEngine`, a `redis-worker` Worker, a
27+
`BatchQueue` - and that work isn't fully drained before the test ends, you **must** use an isolated
28+
redis fixture (`isolatedRedisTest` / `containerTestWithIsolatedRedis`).
29+
30+
On the shared fixture, the leaked background loop keeps polling the one worker-scoped redis after the
31+
test's clients close, bleeding into the next test. The symptom is an intermittent `"Connection is
32+
closed"` error or a test that hangs until its timeout. `FLUSHALL` between tests does **not** fix this -
33+
it clears data, not live connections/loops, so per-test key prefixes won't help either. A plain
34+
db/redis test with no lingering background work is fine on the shared fixtures.
35+
36+
## Sharding (`./sequencer`)
37+
38+
CI splits the slow suites with `vitest --shard=i/N`. `DurationShardingSequencer` replaces vitest's
39+
default file-count split with a duration-weighted one: it reads `test-timings.json` at the repo root
40+
(`{ "<repo-relative path>": <ms> }`) and greedily bin-packs files so each shard does roughly equal
41+
*work*, not an equal *number of files*. The packing is deterministic, so every shard computes the same
42+
bins and runs each file exactly once.
43+
44+
Configs opt in via:
45+
46+
```ts
47+
import { DurationShardingSequencer } from "@internal/testcontainers/sequencer";
48+
// in defineConfig:
49+
test: { sequence: { sequencer: DurationShardingSequencer } }
50+
```
51+
52+
### Adding tests - nothing to do
53+
54+
New test files are discovered by vitest's glob and sharded automatically. A file with no entry in
55+
`test-timings.json` is given the **median** duration as a fallback, so it's still placed on exactly one
56+
shard - correctness never depends on the timings being present or current.
57+
58+
What the timings affect is **balance**. A new heavy test estimated at the median can be under-weighted
59+
and land on an already-full shard, making that shard slower. There's headroom between the current
60+
makespan and the CI budget to absorb this, so it tolerates drift - but if a shard creeps toward the
61+
budget, refresh the timings.
62+
63+
### Refreshing `test-timings.json`
64+
65+
Measure each shard with the JSON reporter and write per-file `endTime - startTime` (ms), keyed by
66+
repo-relative path, back into `test-timings.json`. Set `GITHUB_ACTIONS=true` so suites that
67+
`skipIf(CI)` are excluded, matching what actually runs on CI:
68+
69+
```bash
70+
GITHUB_ACTIONS=true pnpm exec vitest run --reporter=json --outputFile=/tmp/run.json
71+
```
72+
73+
Stale entries for deleted/renamed files are harmless (they're simply ignored). This is a periodic
74+
chore, not a per-PR one.

internal-packages/testcontainers/src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,8 @@ type RedisTestContext = {
296296
redisOptions: RedisOptions;
297297
};
298298

299+
// Worker-scoped redis (boots once, FLUSHALL between tests). Use isolatedRedisTest for tests that run
300+
// background redis work (redis-worker Workers, BatchQueue) past the test body - see its note + README.
299301
export const redisTest = test.extend<RedisTestContext>({
300302
redisContainer: [bootWorkerRedis, { scope: "worker" }],
301303
resetRedis: [flushRedis, { auto: true }],

0 commit comments

Comments
 (0)