pgsql-test-snapshot
Snapshot Testing with pgsql-test
Use snapshot utilities from pgsql-test/utils to create deterministic, reproducible test assertions. These helpers replace dynamic values (IDs, UUIDs, dates, hashes) with stable placeholders.
When to Apply
Use this skill when:
- Writing snapshot tests for database queries
- Need deterministic output from queries with UUIDs or timestamps
- Testing API responses that include database-generated values
- Comparing query results across test runs
Core Utilities
Import from pgsql-test/utils or drizzle-orm-test/utils:
import {
snapshot,
prune,
pruneIds,
pruneDates,
pruneUUIDs,
pruneHashes,
pruneTokens,
composePruners,
createSnapshot
} from 'pgsql-test/utils';
Basic Usage
snapshot()
The main utility that applies all default pruners recursively:
import { snapshot } from 'pgsql-test/utils';
const result = await db.query('SELECT * FROM users');
expect(snapshot(result.rows)).toMatchSnapshot();
Output transforms dynamic values to stable placeholders:
// Before snapshot()
{
id: '550e8400-e29b-41d4-a716-446655440000',
name: 'Alice',
created_at: '2024-01-15T10:30:00.000Z',
password_hash: '$2b$10$...'
}
// After snapshot()
{
id: '[ID]',
name: 'Alice',
created_at: '[DATE]',
password_hash: '[hash]'
}
With Drizzle ORM
import { drizzle } from 'drizzle-orm/node-postgres';
import { snapshot } from 'drizzle-orm-test/utils';
import { users } from './schema';
const drizzleDb = drizzle(db.client);
const result = await drizzleDb.select().from(users);
expect(snapshot(result)).toMatchSnapshot();
Individual Pruners
pruneIds()
Replaces id and *_id fields with [ID]:
import { pruneIds } from 'pgsql-test/utils';
pruneIds({ id: 123, user_id: 'abc-123', name: 'Alice' });
// { id: '[ID]', user_id: '[ID]', name: 'Alice' }
pruneDates()
Replaces Date objects and ISO date strings in *_at or *At fields:
import { pruneDates } from 'pgsql-test/utils';
pruneDates({
created_at: '2024-01-15T10:30:00.000Z',
updatedAt: new Date(),
name: 'Alice'
});
// { created_at: '[DATE]', updatedAt: '[DATE]', name: 'Alice' }
pruneUUIDs()
Replaces UUID values in uuid and queue_name fields:
import { pruneUUIDs } from 'pgsql-test/utils';
pruneUUIDs({ uuid: '550e8400-e29b-41d4-a716-446655440000' });
// { uuid: '[UUID]' }
pruneHashes()
Replaces *_hash fields starting with $:
import { pruneHashes } from 'pgsql-test/utils';
pruneHashes({ password_hash: '$2b$10$xyz...' });
// { password_hash: '[hash]' }
pruneTokens()
Replaces token and *_token fields:
import { pruneTokens } from 'pgsql-test/utils';
pruneTokens({ access_token: 'eyJhbGciOiJIUzI1NiIs...' });
// { access_token: '[token]' }
pruneIdArrays()
Replaces *_ids array fields with count placeholder:
import { pruneIdArrays } from 'pgsql-test/utils';
pruneIdArrays({ member_ids: ['id1', 'id2', 'id3'] });
// { member_ids: '[UUIDs-3]' }
prunePeoplestamps()
Replaces *_by fields (audit columns):
import { prunePeoplestamps } from 'pgsql-test/utils';
prunePeoplestamps({ created_by: 'user-123', updated_by: 'user-456' });
// { created_by: '[peoplestamp]', updated_by: '[peoplestamp]' }
pruneSchemas()
Replaces schema names starting with zz-:
import { pruneSchemas } from 'pgsql-test/utils';
pruneSchemas({ schema: 'zz-abc123' });
// { schema: '[schemahash]' }
ID Hash Tracking
Track ID relationships across snapshots using IdHash:
import { snapshot, pruneIds, IdHash } from 'pgsql-test/utils';
const idHash: IdHash = {
'550e8400-e29b-41d4-a716-446655440001': 'alice',
'550e8400-e29b-41d4-a716-446655440002': 'bob'
};
const result = [
{ id: '550e8400-e29b-41d4-a716-446655440001', name: 'Alice' },
{ id: '550e8400-e29b-41d4-a716-446655440002', name: 'Bob' }
];
expect(snapshot(result, idHash)).toMatchSnapshot();
// [
// { id: '[ID-alice]', name: 'Alice' },
// { id: '[ID-bob]', name: 'Bob' }
// ]
Numeric ID tracking:
const idHash: IdHash = {};
let counter = 1;
// Assign IDs as you encounter them
for (const row of result) {
if (!idHash[row.id]) {
idHash[row.id] = counter++;
}
}
expect(snapshot(result, idHash)).toMatchSnapshot();
// [
// { id: '[ID-1]', name: 'Alice' },
// { id: '[ID-2]', name: 'Bob' }
// ]
Custom Pruners
composePruners()
Combine multiple pruners into one:
import { composePruners, pruneDates, pruneIds } from 'pgsql-test/utils';
const myPruner = composePruners(pruneDates, pruneIds);
const result = myPruner({ id: 123, created_at: new Date() });
// { id: '[ID]', created_at: '[DATE]' }
createSnapshot()
Create a custom snapshot function with specific pruners:
import { createSnapshot, pruneDates, pruneIds, pruneHashes } from 'pgsql-test/utils';
const mySnapshot = createSnapshot([pruneDates, pruneIds, pruneHashes]);
const result = await db.query('SELECT * FROM users');
expect(mySnapshot(result.rows)).toMatchSnapshot();
Default Pruners
The snapshot() function applies these pruners by default:
pruneTokens—token,*_tokenprunePeoplestamps—*_bypruneDates—*_at,*At, Date objectspruneIdArrays—*_idsarrayspruneUUIDs—uuid,queue_namepruneHashes—*_hashpruneIds—id,*_id
Error Code Extraction
Extract error codes from enhanced error messages:
import { getErrorCode } from 'pgsql-test/utils';
try {
await db.query('SELECT * FROM nonexistent');
} catch (err) {
const code = getErrorCode(err.message);
// Returns first line only, stripping debug context
expect(code).toBe('UNDEFINED_TABLE');
}
PostgreSQL Error Formatting
Format PostgreSQL errors for readable output:
import {
extractPgErrorFields,
formatPgError,
formatPgErrorFields
} from 'pgsql-test/utils';
try {
await db.query('invalid sql');
} catch (err) {
const fields = extractPgErrorFields(err);
console.log(formatPgError(err));
// Formatted error with context
}
Complete Test Example
import { getConnections, PgTestClient } from 'pgsql-test';
import { snapshot, IdHash } from 'pgsql-test/utils';
let db: PgTestClient;
let teardown: () => Promise<void>;
beforeAll(async () => {
({ db, teardown } = await getConnections());
});
afterAll(async () => {
await teardown();
});
beforeEach(async () => {
await db.beforeEach();
});
afterEach(async () => {
await db.afterEach();
});
describe('User queries', () => {
it('returns users with stable snapshot', async () => {
// Seed test data
await db.query(`
INSERT INTO users (email, name) VALUES
('alice@example.com', 'Alice'),
('bob@example.com', 'Bob')
`);
const result = await db.query('SELECT * FROM users ORDER BY email');
// Snapshot with ID tracking
const idHash: IdHash = {};
result.rows.forEach((row, i) => {
idHash[row.id] = i + 1;
});
expect(snapshot(result.rows, idHash)).toMatchSnapshot();
});
});
Best Practices
- Use snapshot() by default: Covers most common dynamic fields
- Track IDs with IdHash: When relationships between records matter
- Custom pruners for special fields: Create domain-specific pruners
- Order results: Use ORDER BY for deterministic row order
- Prune before comparing: Apply pruners before any assertions
References
- Related skill:
pgsql-test-seedingfor seeding test data - Related skill:
pgsql-test-rlsfor RLS testing - Related skill:
drizzle-orm-testfor Drizzle ORM integration