Unit tests (the default command)

The everyday command, and the default. unitautogen --schema public runs it with no command word, and unitautogen unit also works. For each function and procedure it generates a full set of pgTAP tests, runs them and measures how much of the routine they exercised. Then it does the same for your triggers. Community Edition, free.

unitautogen --schema public --out unit_tests --html report.html --json report.json

Every routine gets these tests.

  • Executes with valid inputs
  • Low and high boundary values
  • One test per branch, the true side and the false side
  • Returns a stable result-set shape
  • Returns rows matching its recorded output
  • Touches only the expected tables
  • A hand-built expectation scaffold for you to fill in

Triggers are tested by behaviour. The test fires the INSERT, UPDATE or DELETE and asserts what the trigger did.

What it writes

  • One test_<routine>.sql per function in <out>/unit/<schema>/functions/
  • Trigger tests, test_trg_<schema>__<table>__<trigger>.sql, in <out>/unit/<schema>/triggers/
  • The CI report bundle in <out>/unit/
  • The HTML report and the JSON summary wherever --html and --json point, plus run_summary.json for the run
  • --out defaults to unitautogen_tests/. Set TG_EMIT_FLAT=1 for the old flat layout.

Options

  • --schema a,b the schemas to cover
  • --like pat a name filter
  • --out DIR where the test files go
  • --html PATH the HTML report
  • --json PATH the machine-readable summary
  • --report or --no-report the CI bundle, on by default
  • --no-coverage generate and run only, faster
  • --include-extension also test extension-owned routines

On Supabase, unitautogen --supabase --schema public writes to supabase/tests/seccover/unit/, where supabase test db runs it. See Supabase mode.

The JSON summary

A list with one object per routine. A representative entry follows.

{
  "proc": "public.salesbycategory",
  "tests": 7,
  "status": "ok",
  "branch_coverage": 1.0,
  "statement_coverage": 1.0,
  "tests_passed": 6,
  "tests_failed": 0,
  "tests_skipped": 1,
  "pruned": [],
  "uncovered_classes": {}
}

7 tests were generated. 6 pass and 1 is skipped, the hand-built expectation scaffold that waits for you to fill it in. Nothing failed, nothing was pruned and nothing is left uncovered. tests_failed is 0 for every routine, because a test that cannot pass cleanly is never kept.

The HTML report

The --html file is the Database Coverage Report. It opens to four summary cards and a table with one row per routine. Click a routine name for its annotated source.

Unit test coverage report with summary cards and a per-routine table
A sample schema with 6 functions and 2 triggers. 72 function tests in the top table, and 5 behaviour assertions for the triggers below it.
Report elementWhat it shows
Line coverageThe share of executable lines the generated tests ran, for example 16 of 16 lines.
Branch coverageThe mean, across all routines, of the true and false decision outcomes the tests drove.
TestsGenerated tests, split into pass, fail, error and skip. Fail is always 0. Skip counts the scaffolds left for you to fill in.
AutonomyHow many tests are generated as they are and how many you have edited.
Per-routine rowTestable and Gen, the pass, fail, error and skip counts, lines covered, line %, branch % and the uncovered tags.
Uncovered tagsD suspected dead code, H error recovery, E tool limit, BUG a function bug, DDL a statement that emits DDL, Q a query condition not exercised on the test data. See Reading the report.
Trigger bindingsOne row per trigger with its timing, level and events, the tests and their results, the trigger's own coverage, the tables it writes, behaviour tags such as update:wrote and its status. A trigger with no provable behaviour is skipped, never asserted vacuously.
Click a routineThe write footprint per test, the query arms measured on the test data and the annotated source, each line coloured by whether it ran, with hit counts.

A generated trigger test drives the change and asserts what the trigger did. For the audit trigger above, the test fires an update and checks the exact audit row it wrote.

-- generated: test_trg_account__audit_balance
UPDATE public.account SET balance = 1;                       -- Act: fire the trigger
RETURN NEXT is(
    (SELECT ... "action" FROM public.audit_log),
    '[{"action": "balance_change"}]'::jsonb,                 -- Assert: the row the trigger wrote
    'audit_balance :: update write-effect on public.audit_log');

The coverage drill-down

Click a routine or trigger name in the report to open its coverage detail. This is where you see how a routine is covered and what to do about a gap.

Function coverage drill-down with the per-test footprint and annotated source with hit counts
account_health. The footprint shows each test's writes and the value it returned, above the annotated source with a hit count on every line.

Read the annotated source from the top. A green line ran, and its hit count says how many tests drove it. A red line never ran, and its tag says whether it is a suspected bug, error-recovery code or a tool limit. The footprint shows which input produced which result, so you can confirm each branch returns what it should. If you change the code, the tests that pinned those values fail. See the worked example.

Trigger coverage drill-down with the per-firing write footprint, call tree and stitched source
audit_balance. The write footprint per firing, the call tree of the trigger function and its helpers, and the stitched source with hit counts.

A trigger drill-down shows what each firing changed, and what it correctly left alone. The stitched source covers the trigger function and every helper it calls, so an uncovered line is a firing you have not provoked yet. Click a test name in either drill-down to open its pgTAP definition.

Use it to

  • Catch regressions, since a changed behaviour fails the pinned test
  • Find untested branches, the red lines and skips
  • Spot suspected bugs, since a D line is a contradiction in your logic
  • Commit the test_*.sql files so CI re-runs them on every change