Database testing strategies and fixtures
Maria Garcia
Feb 2026
2 tabs
-- Create test database
CREATE DATABASE myapp_test;
-- Test isolation with transactions
/*
beforeEach(async () => {
await db.query('BEGIN');
});
afterEach(async () => {
await db.query('ROLLBACK');
});
test('creates user', async () => {
await db.query('INSERT INTO users (username) VALUES ($1)', ['test']);
const result = await db.query('SELECT * FROM users WHERE username = $1', ['test']);
expect(result.rows).toHaveLength(1);
}); // Rolled back automatically
*/
-- Test fixtures (seed data)
CREATE TABLE test_users (
id SERIAL PRIMARY KEY,
username VARCHAR(50) UNIQUE,
email VARCHAR(100),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Load fixtures
TRUNCATE test_users CASCADE;
INSERT INTO test_users (id, username, email) VALUES
(1, 'alice', 'alice@test.com'),
(2, 'bob', 'bob@test.com'),
(3, 'carol', 'carol@test.com');
-- Reset sequence
SELECT setval('test_users_id_seq', (SELECT MAX(id) FROM test_users));
-- Factory function
CREATE OR REPLACE FUNCTION create_test_user(
username_prefix TEXT DEFAULT 'user'
)
RETURNS test_users AS $$
DECLARE
new_user test_users;
BEGIN
INSERT INTO test_users (username, email)
VALUES (
username_prefix || '_' || floor(random() * 1000000),
username_prefix || '@test.com'
)
RETURNING * INTO new_user;
RETURN new_user;
END;
$$ LANGUAGE plpgsql;
-- Usage
SELECT * FROM create_test_user('testuser');
-- Clean database helper
CREATE OR REPLACE FUNCTION truncate_all_tables()
RETURNS VOID AS $$
DECLARE
table_name TEXT;
BEGIN
FOR table_name IN
SELECT tablename
FROM pg_tables
WHERE schemaname = 'public'
LOOP
EXECUTE format('TRUNCATE TABLE %I CASCADE', table_name);
END LOOP;
END;
$$ LANGUAGE plpgsql;
-- Reset database for tests
SELECT truncate_all_tables();
-- Test data builder pattern
CREATE OR REPLACE FUNCTION build_test_order(
p_user_id INT DEFAULT NULL,
p_total DECIMAL DEFAULT 99.99,
p_status VARCHAR DEFAULT 'pending'
)
RETURNS orders AS $$
DECLARE
v_user_id INT;
new_order orders;
BEGIN
IF p_user_id IS NULL THEN
v_user_id := (SELECT id FROM create_test_user('order_user'));
ELSE
v_user_id := p_user_id;
END IF;
INSERT INTO orders (user_id, total, status)
VALUES (v_user_id, p_total, p_status)
RETURNING * INTO new_order;
RETURN new_order;
END;
$$ LANGUAGE plpgsql;
-- Create order with defaults
SELECT * FROM build_test_order();
-- Create order with specific values
SELECT * FROM build_test_order(1, 199.99, 'completed');
-- Assert functions for tests
CREATE OR REPLACE FUNCTION assert_equals(
expected ANYELEMENT,
actual ANYELEMENT,
message TEXT DEFAULT 'Assertion failed'
)
RETURNS VOID AS $$
BEGIN
IF expected IS DISTINCT FROM actual THEN
RAISE EXCEPTION '% - Expected: %, Actual: %',
message, expected, actual;
END IF;
END;
$$ LANGUAGE plpgsql;
-- Test example
DO $$
DECLARE
user_count INT;
BEGIN
SELECT COUNT(*) INTO user_count FROM test_users;
PERFORM assert_equals(3, user_count, 'User count should be 3');
END $$;
-- Snapshot testing (save and compare)
CREATE TABLE test_snapshots (
test_name VARCHAR(100) PRIMARY KEY,
snapshot JSONB,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Save snapshot
INSERT INTO test_snapshots (test_name, snapshot)
VALUES (
'user_list',
(SELECT jsonb_agg(row_to_json(u.*)) FROM test_users u)
);
-- Compare snapshot
DO $$
DECLARE
current_data JSONB;
saved_snapshot JSONB;
BEGIN
SELECT jsonb_agg(row_to_json(u.*)) INTO current_data
FROM test_users u;
SELECT snapshot INTO saved_snapshot
FROM test_snapshots
WHERE test_name = 'user_list';
IF current_data IS DISTINCT FROM saved_snapshot THEN
RAISE EXCEPTION 'Snapshot mismatch for user_list';
END IF;
END $$;
-- Parallel test databases
/*
// Node.js example - create database per worker
const workerId = process.env.JEST_WORKER_ID || 1;
const testDb = `myapp_test_${workerId}`;
beforeAll(async () => {
await adminClient.query(`DROP DATABASE IF EXISTS ${testDb}`);
await adminClient.query(`CREATE DATABASE ${testDb}`);
await adminClient.query(`
GRANT ALL PRIVILEGES ON DATABASE ${testDb} TO myapp_user
`);
// Run migrations
await runMigrations(testDb);
});
afterAll(async () => {
await adminClient.query(`DROP DATABASE ${testDb}`);
});
*/
-- Test migrations
-- Keep separate test migration table
CREATE TABLE test_schema_migrations (
version VARCHAR(255) PRIMARY KEY,
applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- Test slow queries
SET statement_timeout = '100ms';
-- This should fail if slow
SELECT * FROM large_table WHERE unindexed_column = 'value';
RESET statement_timeout;
-- Test constraints
DO $$
BEGIN
-- Should succeed
INSERT INTO users (username, email)
VALUES ('testuser', 'test@example.com');
-- Should fail (duplicate email)
BEGIN
INSERT INTO users (username, email)
VALUES ('another', 'test@example.com');
RAISE EXCEPTION 'Expected unique constraint violation';
EXCEPTION
WHEN unique_violation THEN
NULL; -- Expected
END;
END $$;
-- Test triggers
CREATE TABLE audit_test_log (
id SERIAL PRIMARY KEY,
event TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE OR REPLACE FUNCTION test_audit_trigger()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO audit_test_log (event)
VALUES (TG_OP || ' on ' || TG_TABLE_NAME);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER test_audit
AFTER INSERT OR UPDATE OR DELETE ON test_users
FOR EACH ROW
EXECUTE FUNCTION test_audit_trigger();
-- Test the trigger
INSERT INTO test_users (username, email) VALUES ('trigger_test', 'test@example.com');
-- Verify audit log
SELECT * FROM audit_test_log WHERE event = 'INSERT on test_users';
-- Mock/stub pattern
-- Replace function with mock during test
CREATE OR REPLACE FUNCTION send_email(recipient TEXT, subject TEXT)
RETURNS BOOLEAN AS $$
BEGIN
-- Production: Actually send email
-- Test: Log to table instead
INSERT INTO email_log (recipient, subject)
VALUES (recipient, subject);
RETURN TRUE;
END;
$$ LANGUAGE plpgsql;
-- Test data generators
CREATE OR REPLACE FUNCTION random_string(length INT)
RETURNS TEXT AS $$
SELECT string_agg(
substring('abcdefghijklmnopqrstuvwxyz', (random() * 26)::INT + 1, 1),
''
)
FROM generate_series(1, length);
$$ LANGUAGE SQL;
CREATE OR REPLACE FUNCTION random_email()
RETURNS TEXT AS $$
SELECT random_string(10) || '@test.com';
$$ LANGUAGE SQL;
-- Generate bulk test data
INSERT INTO test_users (username, email)
SELECT
'user_' || i,
'user_' || i || '@test.com'
FROM generate_series(1, 1000) i;
-- Performance testing
\timing on
SELECT COUNT(*) FROM test_users WHERE email LIKE '%test.com';
\timing off
-- Coverage: Check all tables have tests
SELECT
t.tablename,
EXISTS (
SELECT 1 FROM test_snapshots
WHERE test_name LIKE '%' || t.tablename || '%'
) AS has_test
FROM pg_tables t
WHERE t.schemaname = 'public'
ORDER BY has_test, t.tablename;
-- Integration test pattern
DO $$
DECLARE
v_user test_users;
v_order orders;
BEGIN
-- Arrange
v_user := create_test_user('integration');
-- Act
v_order := build_test_order(v_user.id, 150.00, 'pending');
UPDATE orders SET status = 'completed' WHERE id = v_order.id;
-- Assert
PERFORM assert_equals(
'completed',
(SELECT status FROM orders WHERE id = v_order.id),
'Order should be completed'
);
END $$;
2 files · sql
Explain with highlit
Database testing ensures schema and queries work correctly. I use transactions for test isolation—rollback after each test. Test fixtures provide consistent data. Factory patterns generate test data programmatically. Understanding schema migrations in tests prevents environment drift. Separate test database prevents production corruption. Parallel test execution requires database-per-worker. Seed data for development differs from test fixtures. Proper testing catches bugs before production. Essential for CI/CD pipelines, regression prevention. PostgreSQL transaction-based testing enables fast, isolated tests.