Database changes can be easy to test manually but harder to validate consistently in a CI pipeline. A developer may run a few SQL commands locally, while the same change can behave differently when executed against a clean database.
pgsql-test provides a way to organize and run PostgreSQL tests automatically. It can be useful when database logic, schema changes, functions, procedures, or queries are important parts of an application.
The main question is not whether database testing is useful. It is whether the testing approach is simple enough to run reliably as part of CI.
Why Test PostgreSQL Changes?
A database contains more than tables.
A typical PostgreSQL application may include:
Database
|
+-- Tables
+-- Indexes
+-- Constraints
+-- Views
+-- Functions
+-- Procedures
+-- Triggers
+-- PermissionsA change to one part can affect another.
For example, adding a constraint may cause an existing application test to fail. Changing a function can also change the result returned by an API.
Automated database tests make these problems easier to detect before deployment.
What Is pgsql-test?
pgsql-test is a PostgreSQL testing framework designed to execute database tests and verify expected results.
A database test can validate behavior directly inside PostgreSQL rather than testing only through an application's API.
Conceptually:
Test Case
|
v
PostgreSQL
|
+-- SQL
+-- Function
+-- Procedure
+-- Trigger
|
v
Expected ResultThis makes database-level testing useful for logic that belongs inside PostgreSQL.
What Should You Test?
Not every SQL statement needs a dedicated test.
Focus on database behavior that can break an application.
Good candidates include:
Stored functions
Stored procedures
Complex queries
Constraints
Triggers
Data validation
Permission rules
Important business calculations
Migration behavior
For example, if an application calculates an order total inside PostgreSQL, that calculation is a good candidate for an automated test.
A Simple Database Test
A basic test can create known input data and verify the result.
For example, suppose the database contains an order calculation function:
CREATE OR REPLACE FUNCTION calculate_total(
price numeric,
quantity integer
)
RETURNS numeric
LANGUAGE sql
AS $$
SELECT price * quantity;
$$;A test can check the expected result:
SELECT calculate_total(25, 4);The expected value is:
100The important idea is that the test verifies database behavior rather than relying on a developer to manually inspect the result.
Test Positive and Negative Cases
A useful test suite should not test only valid input.
For example:
Valid quantity
Invalid quantity
Zero quantity
Large quantity
Null valueFor a business function, these cases can expose problems that a simple happy-path test will miss.
The test strategy can look like this:
Function
|
+-- Valid Input
|
+-- Invalid Input
|
+-- Boundary Value
|
+-- Null InputTest Database Constraints
Constraints are another good candidate.
For example:
CREATE TABLE products
(
id integer PRIMARY KEY,
name text NOT NULL,
price numeric CHECK (price >= 0)
);Tests should verify that invalid data is rejected.
For example:
INSERT INTO products
(id, name, price)
VALUES
(1, 'Keyboard', -10);The test should expect this operation to fail because the price violates the constraint.
Testing failure behavior is important because database constraints often provide the final protection against invalid data.
Test Functions and Procedures Separately
If business logic is implemented inside PostgreSQL, test it directly.
For example:
CREATE OR REPLACE FUNCTION get_order_status(
order_id integer
)
RETURNS text
LANGUAGE sql
AS $$
SELECT status
FROM orders
WHERE id = order_id;
$$;A test can prepare a known order:
INSERT INTO orders
(id, status)
VALUES
(1001, 'Completed');Then verify:
SELECT get_order_status(1001);Expected:
CompletedThis isolates the database logic from the application layer.
Keep Test Data Predictable
Database tests become unreliable when they depend on existing data.
Avoid tests that assume:
Customer 101 already exists
Order 500 already exists
Product 10 already existsInstead, create the required test data as part of the test setup.
A predictable test follows this pattern:
Setup
|
v
Insert Test Data
|
v
Execute Operation
|
v
Check Result
|
v
Clean UpThis makes tests repeatable.
Use Transactions Where Appropriate
Transactions can help isolate database tests.
A typical approach is:
BEGIN;
-- Test setup
-- Execute test
-- Validate result
ROLLBACK;This allows temporary test data to be removed automatically when the test finishes.
However, not every test can be safely wrapped in a transaction. Tests involving transaction behavior itself, commits, or certain database operations may require a different setup.
Running Tests in CI
The real value of database testing appears when tests run automatically.
A typical pipeline can look like this:
Developer Push
|
v
CI Pipeline
|
v
Start PostgreSQL
|
v
Apply Migrations
|
v
Run Database Tests
|
v
Application Tests
|
v
Build / Deploy
Join the conversation! Your thoughts help the community grow.