Pgtap (unit testing)
pgtap is a unit testing framework written in PL/pgSQL and PL/SQL. It is a Test Anything Protocol (TAP) framework for PolarDB for PostgreSQL that includes a comprehensive set of TAP assertion functions and integrates with other TAP-based testing frameworks.
Prerequisites
The following versions of PolarDB for PostgreSQL are supported:
PostgreSQL 14 (minor kernel version 14.5.3.0 or later)
PostgreSQL 11 (minor kernel version 1.1.30 or later)
You can run the following commands to check the PolarDB for PostgreSQL minor kernel version:
PostgreSQL 14
SELECT version();PostgreSQL 11
SHOW polar_version;
Terminology
Unit testing: A software testing method that tests individual components or modules of a system to verify their correctness.
Test Anything Protocol (TAP): A language-agnostic protocol for communicating test results between test harnesses. Originally developed for Perl, it simplifies error reporting during testing.
Usage
The pgtap extension must be installed by a superuser. Please contact us for assistance.
Install the extension
CREATE EXTENSION pgtap;Uninstall the extension
DROP EXTENSION pgtap;
Examples
The pgtap extension provides a wide range of test functions for database objects, including tablespaces, schemas, tables, columns, views, sequences, indexes, triggers, functions, policies, users, languages, rules, types, operators, and extensions.
The following examples show how to test tables, policies, columns, and functions with the pgtap extension.
Test tables, indexes, and views
To check if a table, index, or view exists, use the following example:
BEGIN; SELECT plan(6); -- Check if the "tap_table" table exists SELECT has_table('tap_table'); -- Check if the "tap_table_non_exist" table does not exist SELECT hasnt_table('tap_table_non_exist'); -- Check if the "tap_view" view exists SELECT has_view('tap_view'); -- Check if the "materialized_tap_view" materialized view exists SELECT has_materialized_view('materialized_tap_view'); -- Check if the "tap_table" table has an index named "tap_table_index" SELECT has_index('tap_table', 'tap_table_index'); -- Check if "tap_table" is a relation SELECT has_relation('tap_table'); SELECT * FROM finish(); ROLLBACK;NoteFunction descriptions:
has_tablechecks if a table exists, whilehasnt_tablechecks if it does not.has_viewchecks if a specified view exists.has_materialized_viewchecks if a specified materialized view exists.has_indexchecks if a table contains a specific index.has_relationchecks if a specified relation, such as a table, index, or sequence, exists.
Running the preceding commands returns the following output. The tests fail because the specified 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)Run the following commands to create the required table, index, and views:
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;After creating the objects, run the TAP test again. The following output is returned:
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)Test RLS policies
To check if a table has a specific row-level security (RLS) policy, use the following example:
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 [ 'tap_user_1', -- Check if the "tap_policy" RLS policy applies to the user "tap_user_1" 'tap_user_2' -- Check if the "tap_policy" RLS policy applies to the user "tap_user_2" ] ); SELECT policies_are( 'public', 'tap_table', ARRAY [ 'tap_policy' -- Check if the "tap_table" table has the "tap_policy" RLS policy ] ); SELECT * FROM check_test( policy_roles_are( 'public', 'tap_table', 'tap_policy', ARRAY [ 'tap_user_1' -- Test for an expected failure by checking an incomplete list of roles ]), 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;NoteFunction descriptions:
policy_cmd_ischecks the command type to which the RLS policy applies.policy_roles_areis used to check whether an RLS policy is applied to all of its specified users. The function returnsTRUEif and only if all users to whom the RLS policy applies are specified.policies_arechecks if a table has a specific set of RLS policies.
To test for an expected failure, you can use the
check_testfunction and set the expected result toTRUEorFALSE. The test returns the following results: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)Test columns
To check if a column exists in a table or if it is a primary key or foreign key, use the following example:
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); -- Test if the "col" column is the primary key of the "tap_table" table SELECT col_is_pk('tap_table', 'col'); -- Test if the "tap_desc" column is not the primary key of the "tap_table" table SELECT col_isnt_pk('tap_table', 'tap_desc'); -- Test if the "col" column is a foreign key of the "tap_table" table 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'); -- Test if the "col" column is not a foreign key of the "tap_table" table SELECT col_isnt_fk('tap_table', 'col'); -- Test if the "col" column exists in the "tap_table" table SELECT has_column('tap_table', 'col'); -- Test if the "non_col" column does not exist in the "tap_table" table SELECT hasnt_column('tap_table', 'non_col'); SELECT * FROM finish(); ROLLBACK; DROP TABLE tap_table;NoteFunction descriptions:
col_is_pkchecks if a column is the primary key of a table.col_isnt_pkchecks if a column is not the primary key of a table.col_isnt_fkchecks if a column is not a foreign key.has_columnchecks if a table contains a specific column.hasnt_columnchecks if a table does not contain a specific column.
The test returns the following results:
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)Test functions
To check a function's return type or verify if it is a
SECURITY DEFINERfunction, use the following example: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 if the "tap_function" function returns the "text" type SELECT function_returns('tap_function', 'text'); -- Check if the "tap_function_bool" function with the argument list ('integer', 'boolean', 'text') returns the "boolean" type SELECT function_returns('tap_function_bool', ARRAY['integer', 'boolean', 'text'], 'boolean'); -- Check if the "tap_function" function is a SECURITY DEFINER SELECT is_definer('tap_function'); -- Check if the "tap_function_bool" function is not a SECURITY DEFINER SELECT isnt_definer('tap_function_bool'); -- Check if the "tap_function_bool" function is a 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;NoteFunction descriptions:
function_returnschecks if a function, with an optionally specified argument list, returns a specific type.is_definerchecks if a specified function is aSECURITY DEFINERfunction.isnt_definerchecks if a specified function is not aSECURITY DEFINERfunction.
The test returns the following results:
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)Others
The pgtap extension includes many test functions not covered in these examples. While the functions themselves differ, they follow a similar usage pattern. For a complete list of functions, see the pgtap official documentation.