pgtap
pgTAP is a unit testing framework for PolarDB for PostgreSQL (Compatible with Oracle), written in PL/pgSQL and PL/SQL. As a Test Anything Protocol (TAP) testing framework, it provides a comprehensive set of TAP-emitting assertion functions and integrates with other TAP-emitting test frameworks.
Use pgTAP to write SQL-based tests that verify your database schema structure and behavior — tables, columns, views, indexes, functions, row-level security policies, and more — directly inside the database.
Prerequisites
Before you begin, make sure that you have:
A PolarDB for PostgreSQL (Compatible with Oracle) cluster running one of the following revision versions:
Version 2.0, revision 2.0.14.3.0 or later
Version 1.0, revision 1.1.30 or later
Superuser permissions on the cluster (required to create the extension). If you need superuser access, contact Alibaba Cloud support.
To check your cluster's revision version, run:
SHOW polar_version;Key concepts
Unit testing: Tests the correctness of individual modules in isolation. Also known as module testing.
Test Anything Protocol (TAP): A language-independent protocol for communicating test results to a test harness. Originally developed for the Perl language.
Assertion function: A function that tests a condition and outputs an
okornot okresult in TAP format.
How it works
Each pgTAP test script follows the same structure:
BEGIN;
SELECT plan(<number_of_assertions>); -- Declare how many assertions to run
-- Assertion calls go here
SELECT has_table('my_table');
SELECT * FROM finish(); -- Validate the assertion count
ROLLBACK; -- Roll back any changes made during testingThe plan() call declares the total number of assertions. finish() verifies that exactly that many assertions ran, catching missing or extra tests. Wrapping everything in a transaction ensures tests do not affect production data.
Install and remove the extension
Before creating the pgTAP extension, set polar_enable_empty_string_is_null to off on the target database:
ALTER DATABASE <dbname> SET polar_enable_empty_string_is_null=off;To create the extension:
CREATE EXTENSION pgtap;To remove the extension:
DROP EXTENSION pgtap;Test examples
pgTAP supports test methods for tablespaces, schemas, tables, columns, views, sequences, indexes, triggers, functions, policies, users, languages, rules, classes, operators, and extensions. The following examples cover the most common categories.
Table, index, and view tests
The following script checks whether a set of database objects exist:
BEGIN;
SELECT plan(6);
-- Check whether the tap_table table exists.
SELECT has_table('tap_table');
-- Check whether the tap_table_non_exist table does not exist.
SELECT hasnt_table('tap_table_non_exist');
-- Check whether the tap_view view exists.
SELECT has_view('tap_view');
-- Check whether the materialized_tap_view materialized view exists.
SELECT has_materialized_view('materialized_tap_view');
-- Check whether the tap_table table has the tap_table_index index.
SELECT has_index('tap_table', 'tap_table_index');
-- Check whether tap_table is a relation.
SELECT has_relation('tap_table');
SELECT * FROM finish();
ROLLBACK;Assertion functions used:
| Function | Description |
|---|---|
has_table | Checks whether the specified table exists |
hasnt_table | Checks whether the specified table does not exist |
has_view | Checks whether the specified view exists |
has_materialized_view | Checks whether the specified materialized view exists |
has_index | Checks whether a table has the specified index |
has_relation | Checks whether the specified relation exists (can be a table, an index, or a sequence) |
For full function signatures and optional parameters, see the pgTAP documentation.
Sample output when the objects do not exist:
has_table
-------------------------------------------------
not ok 1 - Table tap_table should exist +
# Failed test 1: "Table tap_table should exist"
(1 row)
hasnt_table
---------------------------------------------------
ok 2 - Table tap_table_non_exist should not exist
(1 row)
has_view
-----------------------------------------------
not ok 3 - View tap_view should exist +
# Failed test 3: "View tap_view should exist"
(1 row)
has_materialized_view
-------------------------------------------------------------------------
not ok 4 - Materialized view materialized_tap_view should exist +
# Failed test 4: "Materialized view materialized_tap_view should exist"
(1 row)
has_index
-------------------------------------------------------
not ok 5 - Index tap_table_index should exist +
# Failed test 5: "Index tap_table_index should exist"
(1 row)
has_relation
----------------------------------------------------
not ok 6 - Relation tap_table should exist +
# Failed test 6: "Relation tap_table should exist"
(1 row)
finish
--------------------------------------
# Looks like you failed 5 tests of 6
(1 row)Lines starting with # are diagnostic messages that tell you exactly which test failed. The finish output summarizes how many tests passed.
Create the missing objects and run the test again:
CREATE TABLE tap_table(col INT PRIMARY KEY, tap_desc TEXT);
CREATE INDEX tap_table_index on tap_table(col);
CREATE VIEW tap_view AS SELECT * FROM tap_table;
CREATE MATERIALIZED VIEW materialized_tap_view AS SELECT * FROM tap_table;Sample output after creating the objects:
has_table
-------------------------------------
ok 1 - Table tap_table should exist
(1 row)
hasnt_table
---------------------------------------------------
ok 2 - Table tap_table_non_exist should not exist
(1 row)
has_view
-----------------------------------
ok 3 - View tap_view should exist
(1 row)
has_materialized_view
-------------------------------------------------------------
ok 4 - Materialized view materialized_tap_view should exist
(1 row)
has_index
-------------------------------------------
ok 5 - Index tap_table_index should exist
(1 row)
has_relation
----------------------------------------
ok 6 - Relation tap_table should exist
(1 row)
finish
--------
(0 rows)An empty finish result means all assertions passed.
RLS policy tests
The following script checks whether a row-level security (RLS) policy is correctly defined on a table:
CREATE USER tap_user_1;
CREATE USER tap_user_2;
CREATE TABLE tap_table(col INT PRIMARY KEY, tap_desc TEXT);
CREATE POLICY tap_policy ON tap_table FOR select TO tap_user_1, tap_user_2;
BEGIN;
SELECT plan(5);
SELECT policy_cmd_is(
'public',
'tap_table',
'tap_policy'::NAME,
'select'
);
SELECT policy_roles_are(
'public',
'tap_table',
'tap_policy',
ARRAY [
'tab_user_1', -- Check whether tab_user_1 is restricted by the RLS policy tap_policy.
'tab_user_2' -- Check whether tab_user_2 is restricted by the RLS policy tap_policy.
]
);
SELECT policies_are(
'public',
'tap_table',
ARRAY [
'TAP_POLICE' -- Check whether tap_table contains the RLS policy tap_policy.
]
);
SELECT * FROM check_test(
policy_roles_are(
'public',
'tap_table',
'tap_policy',
ARRAY [
'tape_user_1' -- Verify that tab_user_1 is the only user restricted by tap_policy.
]),
false,
'check policy roles',
'Policy tap_policy for table public.tap_table should have the correct roles');
SELECT * FROM finish();
ROLLBACK;
DROP POLICY tap_policy ON tap_table;
DROP TABLE tap_table;
DROP USER tap_user_1;
DROP USER tap_user_2;Assertion functions used:
| Function | Description |
|---|---|
policy_cmd_is | Checks whether the specified RLS policy exists |
policy_roles_are | Checks whether the RLS policy applies to exactly the specified set of users; returns TRUE if and only if all users are specified |
policies_are | Checks whether a table contains an RLS policy |
check_test | Wraps an assertion and lets you specify whether the expected result is TRUE or FALSE — useful for testing that an assertion fails as expected |
For full function signatures and optional parameters, see the pgTAP documentation.
Sample output:
policy_cmd_is
------------------------------------------------------------------------------------
ok 1 - Policy tap_policy for table public.tap_table should apply to SELECT command
(1 row)
policy_roles_are
-----------------------------------------------------------------------------------
ok 2 - Policy tap_policy for table public.tap_table should have the correct roles
(1 row)
policies_are
----------------------------------------------------------------
ok 3 - Table public.tap_table should have the correct policies
(1 row)
check_test
--------------------------------------------------------------
ok 4 - check policy roles should fail
ok 5 - check policy roles should have the proper description
(2 rows)
finish
--------
(0 rows)Column tests
The following script checks column properties — primary key, foreign key, and existence:
CREATE TABLE tap_table(col INT PRIMARY KEY, tap_desc TEXT);
CREATE INDEX tap_table_index ON tap_table(col);
CREATE UNIQUE INDEX tap_table_unique_index ON tap_table(col);
BEGIN;
SELECT plan(7);
-- Check whether col is the primary key of tap_table.
SELECT col_is_pk('tap_table', 'col');
-- Check whether tap_desc is not a primary key of tap_table.
SELECT col_isnt_pk('tap_table', 'tap_desc');
-- Verify that col_is_fk fails as expected (col is not a foreign key).
SELECT * FROM check_test(
col_is_fk('tap_table', 'col'),
false,
'check foreign key of table',
'Column tap_table(col) should be a foreign key');
-- Check whether col is not a foreign key of tap_table.
SELECT col_isnt_fk('tap_table', 'col');
-- Check whether tap_table contains the col column.
SELECT has_column('tap_table', 'col');
-- Check whether tap_table does not contain a non_col column.
SELECT hasnt_column('tap_table', 'non_col');
SELECT * FROM finish();
ROLLBACK;
DROP TABLE tap_table;Assertion functions used:
| Function | Description |
|---|---|
col_is_pk | Checks whether a column is the primary key of a table |
col_isnt_pk | Checks whether a column is not the primary key of a table |
col_isnt_fk | Checks whether a column is not a foreign key of a table |
has_column | Checks whether a table contains a column |
hasnt_column | Checks whether a table does not contain a column |
For full function signatures and optional parameters, see the pgTAP documentation.
Sample output:
col_is_pk
------------------------------------------------------
ok 1 - Column tap_table(col) should be a primary key
(1 row)
col_isnt_pk
---------------------------------------------------------------
ok 2 - Column tap_table(tap_desc) should not be a primary key
(1 row)
check_test
----------------------------------------------------------------------
ok 3 - check foreign key of table should fail
ok 4 - check foreign key of table should have the proper description
(2 rows)
col_isnt_fk
----------------------------------------------------------
ok 5 - Column tap_table(col) should not be a foreign key
(1 row)
has_column
------------------------------------------
ok 6 - Column tap_table.col should exist
(1 row)
hasnt_column
--------------------------------------------------
ok 7 - Column tap_table.non_col should not exist
(1 row)
finish
--------
(0 rows)Function tests
The following script checks a function's return type and whether it is declared as SECURITY DEFINER:
CREATE OR REPLACE FUNCTION tap_function()
RETURNS text
AS $$
BEGIN
RETURN 'This is tap test function';
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
CREATE OR REPLACE FUNCTION tap_function_bool(arg1 integer, arg2 boolean, arg3 text)
RETURNS boolean
AS $$
BEGIN
RETURN true;
END;
$$ LANGUAGE plpgsql;
BEGIN;
SELECT plan(6);
-- Check whether tap_function returns text.
SELECT function_returns('tap_function', 'text');
-- Check whether tap_function_bool(integer, boolean, text) returns boolean.
SELECT function_returns('tap_function_bool', ARRAY['integer', 'boolean', 'text'], 'boolean');
-- Check whether tap_function is SECURITY DEFINER.
SELECT is_definer('tap_function');
-- Check whether tap_function_bool is not SECURITY DEFINER.
SELECT isnt_definer('tap_function_bool');
-- Verify that is_definer fails for tap_function_bool (it is not SECURITY DEFINER).
SELECT * FROM check_test(
is_definer('tap_function_bool'),
false,
'check function security definer',
'Function tap_function_bool() should be security definer');
SELECT * FROM finish();
ROLLBACK;
DROP FUNCTION tap_function;
DROP FUNCTION tap_function_bool;Assertion functions used:
| Function | Description |
|---|---|
function_returns | Checks whether a function's return type matches the specified type; optionally accepts a parameter type list to distinguish overloaded functions |
is_definer | Checks whether the specified function is SECURITY DEFINER |
isnt_definer | Checks whether the specified function is not SECURITY DEFINER |
For full function signatures and optional parameters, see the pgTAP documentation.
Sample output:
function_returns
---------------------------------------------------
ok 1 - Function tap_function() should return text
(1 row)
function_returns
---------------------------------------------------------------------------------
ok 2 - Function tap_function_bool(integer, boolean, text) should return boolean
(1 row)
is_definer
-----------------------------------------------------------
ok 3 - Function tap_function() should be security definer
(1 row)
isnt_definer
--------------------------------------------------------------------
ok 4 - Function tap_function_bool() should not be security definer
(1 row)
check_test
---------------------------------------------------------------------------
ok 5 - check function security definer should fail
ok 6 - check function security definer should have the proper description
(2 rows)
finish
--------
(0 rows)What's next
pgTAP provides additional test methods beyond the examples above, covering the same general script structure: BEGIN → plan → assertions → finish → ROLLBACK. For the complete list of assertion functions across all supported object types, see the pgTAP documentation.