Testing PostgreSQL — decision tree
Companion to knowledge/conventions/testing.md. That file is the long-form
reference; this is the action card.
Step 1: Pick the flavor
Answer these in order. First "yes" wins.
Does the behavior need multiple sessions / concurrent transactions /
heavyweight-lock contention?
→ isolation spec in source/src/test/isolation/specs/<name>.spec.
Caveat: isolationtester only sees heavyweight locks via pg_locks. LWLock /
buffer-pin contention → use source/src/test/modules/injection_points/
(or a custom C test module) instead.
Minimal spec grammar:
setup { CREATE TABLE t (a int); }
teardown { DROP TABLE t; }
session "s1" { step "s1a" { BEGIN; } step "s1b" { COMMIT; } }
session "s2" { step "s2a" { SELECT * FROM t; } }
permutation "s1a" "s2a" "s1b"
When any step blocks, the permutation line is mandatory — auto-generated
permutations will hang CI.
Does it need initdb, a restart, multiple clusters, replication, a backup,
a signal, or testing a CLI tool (pg_dump, pg_basebackup, pg_rewind)?
→ TAP test in the appropriate t/ dir:
- Recovery / replication →
source/src/test/recovery/t/NNN_name.pl
- Logical replication →
source/src/test/subscription/t/NNN_name.pl
- Auth →
source/src/test/authentication/t/NNN_name.pl
- SSL →
source/src/test/ssl/t/NNN_name.pl
- A specific CLI tool →
source/src/bin/<tool>/t/NNN_name.pl
- Contrib extension →
source/contrib/<ext>/t/NNN_name.pl
- Brand new area without an obvious home → ask before creating a directory.
Are you testing a C-level hook, internal API, or something only reachable
from C?
→ Add (or extend) a test module under source/src/test/modules/<name>/,
then drive it from a .sql or TAP test in the same module dir.
Else (pure SQL: planner, builtin function, DDL, error message, catalog
behavior): regress test. source/src/test/regress/sql/<name>.sql +
expected/<name>.out, wired into parallel_schedule.
Step 2: Wire it in (don't skip — easiest mistake)
| Flavor |
File you must edit |
| regress |
source/src/test/regress/parallel_schedule (add to a test: line, max 20 per group) |
| isolation |
source/src/test/isolation/isolation_schedule (test: <name>) |
| TAP |
meson.build in the same dir as t/ (e.g. source/src/test/recovery/meson.build) AND the Makefile if one exists |
| modules |
source/src/test/modules/meson.build if adding a new module dir |
Step 3: Generate expected output
For regress and isolation: write the .sql / .spec, run the test, copy the
produced output to expected/:
# regress
meson test -C build regress/regress # will FAIL the first time
cp source/src/test/regress/results/<name>.out source/src/test/regress/expected/<name>.out
# now re-run and check it passes
meson test -C build regress/regress
Always read the generated .out before committing. Anything OID-y,
timestamp-y, or row-order-y is a future buildfarm failure. Cast it, ORDER BY it,
or SET it away.
Step 4: Run only your test (fast loop)
TAP replication-sync idiom (in src/test/recovery/t/):
$primary->wait_for_replay_catchup($standby);
# or, explicitly:
my $lsn = $primary->lsn('insert');
$standby->poll_query_until('postgres',
"SELECT pg_last_wal_replay_lsn() >= '$lsn'::pg_lsn");
End every TAP file with done_testing();. TAP tests verify replicated state
(LSN advance, table contents), not WAL-record identity — use pg_waldump from
a shell step if you really need the latter.
# Single TAP test
meson test -C build --suite recovery 001_stream_rep -v
# Single isolation spec
cd build/src/test/isolation && \
./pg_isolation_regress --temp-instance=/tmp/iso \
--top-builddir=../../../.. <spec_name>
# Single regress test
cd build/src/test/regress && \
./pg_regress --temp-instance=/tmp/pgr \
--top-builddir=../../../.. <test_name>
# Whole suite by name
meson test -C build --suite regress
meson test -C build --suite isolation
meson test -C build --suite recovery
make equivalents (from source/src/test/<area>/):
make check — fresh temp instance, what CI does. Use before submitting.
make installcheck — against your running cluster. Faster, less safe.
make check PROVE_TESTS=t/NNN_name.pl — just one TAP test.
Step 5: When it fails
- regress: open
build/src/test/regress/regression.diffs first. If the diff
is what you want, copy results/<name>.out over expected/<name>.out.
- isolation:
build/src/test/isolation/output_iso/regression.diffs.
Hanging = invalid permutation (a step blocks with nothing to unblock it).
- TAP:
build/testrun/<suite>/<test>/log/regress_log_* is the Perl-side log.
Read it before the postmaster logs. Set PG_TEST_NOCLEAN=1 to keep
tmp_check/ even on success.
Cheat sheet — decision in one line
"Multiple sessions? → isolation. Initdb-or-restart? → TAP. C hook? → module.
Otherwise → regress."
Common rejections in review
- "Use a regress test, not TAP" — for anything expressible in SQL.
- "Output isn't stable" — OIDs, timestamps, plan costs, unsorted results.
- "Add it to the schedule" — file exists but isn't wired into
parallel_schedule
/ isolation_schedule / meson.build.
- "Don't introduce
_1.out variants" — prefer to stabilize the test
(markers in isolation; SET / casts in regress).
- "Renumbered existing TAP tests" — breaks backports. Always append
NNN.
- "Uses non-core Perl module" — only
IPC::Run is allowed; everything else
must be in Perl 5.14 core.
See knowledge/conventions/testing.md for full citations and the long form.
Cross-references
knowledge/conventions/testing.md — long-form reference with citations.
.claude/skills/build-and-run/SKILL.md — --suite setup requirement, single-test invocations under meson.
.claude/skills/debugging/SKILL.md — waitpoints + PGOPTIONS="-W N" for catching things that happen too fast.
.claude/skills/coding-style/SKILL.md — regress_* naming, EXPLAIN (COSTS OFF) portability rule.
.claude/skills/review-checklist/SKILL.md — the test-coverage phase consults this skill.
.claude/commands/pg-test.md — slash-command wrapper that always prepends --suite setup.
1---2name: testing3description: Pick the right PostgreSQL test flavor for a core / contrib patch — covers pg_regress .sql / .out pairs, isolationtester specs and permutations for concurrency / deadlock races, TAP (PostgreSQL::Test::Cluster) for multi-node and pg_basebackup / replication / recovery scenarios, and src/test/modules for in-tree C test modules. Spans where test files live, how to wire them into the right schedule / meson.build, and how to run a single test fast (REGRESS_OPTS, --temp-instance, PROVE_FLAGS). Use whenever adding tests to a PG patch, reviewing a patch's test coverage, picking between regress / isolation / TAP for a new feature, debugging a regression diff, or asked "where should the test for X live". Skip for pytest / unittest / Jest / Vitest / Mocha / RSpec / JUnit / Go testing / Rust cargo-test app testing, pgbench / sysbench / HammerDB performance benchmarking, end-to-end / Selenium / Playwright web testing, and load / stress testing of production PG.4---56# Testing PostgreSQL — decision tree78Companion to `knowledge/conventions/testing.md`. That file is the long-form9reference; this is the action card.1011## Step 1: Pick the flavor1213Answer these in order. **First "yes" wins.**14151. **Does the behavior need multiple sessions / concurrent transactions /16 heavyweight-lock contention?**17 → **isolation spec** in `source/src/test/isolation/specs/<name>.spec`.18 Caveat: `isolationtester` only sees heavyweight locks via `pg_locks`. LWLock /19 buffer-pin contention → use `source/src/test/modules/injection_points/`20 (or a custom C test module) instead.2122 Minimal spec grammar:2324 ```25 setup { CREATE TABLE t (a int); }26 teardown { DROP TABLE t; }27 session "s1" { step "s1a" { BEGIN; } step "s1b" { COMMIT; } }28 session "s2" { step "s2a" { SELECT * FROM t; } }29 permutation "s1a" "s2a" "s1b"30 ```3132 When any step blocks, the `permutation` line is **mandatory** — auto-generated33 permutations will hang CI.34352. **Does it need initdb, a restart, multiple clusters, replication, a backup,36 a signal, or testing a CLI tool (`pg_dump`, `pg_basebackup`, `pg_rewind`)?**37 → **TAP test** in the appropriate `t/` dir:38 - Recovery / replication → `source/src/test/recovery/t/NNN_name.pl`39 - Logical replication → `source/src/test/subscription/t/NNN_name.pl`40 - Auth → `source/src/test/authentication/t/NNN_name.pl`41 - SSL → `source/src/test/ssl/t/NNN_name.pl`42 - A specific CLI tool → `source/src/bin/<tool>/t/NNN_name.pl`43 - Contrib extension → `source/contrib/<ext>/t/NNN_name.pl`44 - Brand new area without an obvious home → ask before creating a directory.45463. **Are you testing a C-level hook, internal API, or something only reachable47 from C?**48 → Add (or extend) a test module under `source/src/test/modules/<name>/`,49 then drive it from a `.sql` or TAP test in the same module dir.50514. **Else** (pure SQL: planner, builtin function, DDL, error message, catalog52 behavior): **regress test**. `source/src/test/regress/sql/<name>.sql` +53 `expected/<name>.out`, wired into `parallel_schedule`.5455## Step 2: Wire it in (don't skip — easiest mistake)5657| Flavor | File you must edit |58|---|---|59| regress | `source/src/test/regress/parallel_schedule` (add to a `test:` line, max 20 per group) |60| isolation | `source/src/test/isolation/isolation_schedule` (`test: <name>`) |61| TAP | `meson.build` in the same dir as `t/` (e.g. `source/src/test/recovery/meson.build`) AND the `Makefile` if one exists |62| modules | `source/src/test/modules/meson.build` if adding a new module dir |6364## Step 3: Generate expected output6566For regress and isolation: write the `.sql` / `.spec`, run the test, copy the67produced output to `expected/`:6869```bash70# regress71meson test -C build regress/regress # will FAIL the first time72cp source/src/test/regress/results/<name>.out source/src/test/regress/expected/<name>.out73# now re-run and check it passes74meson test -C build regress/regress75```7677**Always read the generated `.out` before committing.** Anything OID-y,78timestamp-y, or row-order-y is a future buildfarm failure. Cast it, ORDER BY it,79or `SET` it away.8081## Step 4: Run only your test (fast loop)8283TAP replication-sync idiom (in `src/test/recovery/t/`):8485```perl86$primary->wait_for_replay_catchup($standby);87# or, explicitly:88my $lsn = $primary->lsn('insert');89$standby->poll_query_until('postgres',90 "SELECT pg_last_wal_replay_lsn() >= '$lsn'::pg_lsn");91```9293End every TAP file with `done_testing();`. TAP tests verify replicated state94(LSN advance, table contents), not WAL-record identity — use `pg_waldump` from95a shell step if you really need the latter.9697```bash98# Single TAP test99meson test -C build --suite recovery 001_stream_rep -v100101# Single isolation spec102cd build/src/test/isolation && \103 ./pg_isolation_regress --temp-instance=/tmp/iso \104 --top-builddir=../../../.. <spec_name>105106# Single regress test107cd build/src/test/regress && \108 ./pg_regress --temp-instance=/tmp/pgr \109 --top-builddir=../../../.. <test_name>110111# Whole suite by name112meson test -C build --suite regress113meson test -C build --suite isolation114meson test -C build --suite recovery115```116117`make` equivalents (from `source/src/test/<area>/`):118- `make check` — fresh temp instance, what CI does. Use before submitting.119- `make installcheck` — against your running cluster. Faster, less safe.120- `make check PROVE_TESTS=t/NNN_name.pl` — just one TAP test.121122## Step 5: When it fails123124- **regress**: open `build/src/test/regress/regression.diffs` first. If the diff125 is what you want, copy `results/<name>.out` over `expected/<name>.out`.126- **isolation**: `build/src/test/isolation/output_iso/regression.diffs`.127 Hanging = invalid permutation (a step blocks with nothing to unblock it).128- **TAP**: `build/testrun/<suite>/<test>/log/regress_log_*` is the Perl-side log.129 Read it before the postmaster logs. Set `PG_TEST_NOCLEAN=1` to keep130 `tmp_check/` even on success.131132## Cheat sheet — decision in one line133134> *"Multiple sessions? → isolation. Initdb-or-restart? → TAP. C hook? → module.135> Otherwise → regress."*136137## Common rejections in review138139- "Use a regress test, not TAP" — for anything expressible in SQL.140- "Output isn't stable" — OIDs, timestamps, plan costs, unsorted results.141- "Add it to the schedule" — file exists but isn't wired into `parallel_schedule`142 / `isolation_schedule` / `meson.build`.143- "Don't introduce `_1.out` variants" — prefer to stabilize the test144 (markers in isolation; `SET` / casts in regress).145- "Renumbered existing TAP tests" — breaks backports. Always append `NNN`.146- "Uses non-core Perl module" — only `IPC::Run` is allowed; everything else147 must be in Perl 5.14 core.148149See `knowledge/conventions/testing.md` for full citations and the long form.150151## Cross-references152153- `knowledge/conventions/testing.md` — long-form reference with citations.154- `.claude/skills/build-and-run/SKILL.md` — `--suite setup` requirement, single-test invocations under meson.155- `.claude/skills/debugging/SKILL.md` — waitpoints + `PGOPTIONS="-W N"` for catching things that happen too fast.156- `.claude/skills/coding-style/SKILL.md` — `regress_*` naming, `EXPLAIN (COSTS OFF)` portability rule.157- `.claude/skills/review-checklist/SKILL.md` — the test-coverage phase consults this skill.158- `.claude/commands/pg-test.md` — slash-command wrapper that always prepends `--suite setup`.