pgtap是一个使用PL/pgSQL和PL/SQL编写的单元测试框架,是PolarDB PostgreSQL版(兼容Oracle)的一个TAP测试框架,包括一个全面的TAP断言功能集合,并具备集成其他TAP测试框架的能力。

术语

  • 单元测试(Unit Testing): 又称模块测试,允许针对系统的不同模块进行正确性检验的测试工作。
  • TAP(Test Anything Protocol): 最初为Perl语言开发,提供了一种将测试结果传达给测试工具的机制,同时保持语言无关性并简化了测试过程中的错误报告形式。

使用方法

  • 创建插件
    执行以下命令,创建插件。
    CREATE EXTENSION pgtap;
    说明 如果需要将pgtap插件用于所有新建的数据库中,可以使用以下命令将其添加至template1模板数据库。
    psql -d template1 -C "CREATE EXTENSION pgtap"
  • 卸载插件
    执行以下命令,卸载插件。
    DROP EXTENSION pgtap;

测试示例

pgtap插件提供了关于表空间、模式、表、列、视图、序列、索引、触发器、函数、策略、用户、语言、规则、类型、操作符、扩展等丰富的测试方法。

以表、策略、列、函数为例,对pgtap插件的使用方法进行具体的介绍。

  • 表/索引/视图测试
    如果需要检测某个表/索引/视图是否存在,可以参考以下测试示例:
    BEGIN;
    SELECT plan(6);
    
    -- 检测表 "tap_table" 是否存在
    SELECT has_table('tap_table');
    
    -- 检测表 "tap_table_non_exist" 是否不存在
    SELECT hasnt_table('tap_table_non_exist');
    
    -- 检测视图 "tap_view" 是否存在
    SELECT has_view('tap_view');
    
    -- 检测物化视图 "materialized_tap_view" 是否存在
    SELECT has_materialized_view('materialized_tap_view');
    
    -- 检测表 "tap_table" 是否存在 "tap_table_index" 索引
    SELECT has_index('tap_table', 'tap_table_index');
    
    -- 检测 "tap_table" 是否为relation
    SELECT has_relation('tap_table');
    
    SELECT * FROM finish();
    ROLLBACK;
    说明 其中:
    • has_tablehasnt_table用于检测指定的表是否存在。
    • has_view用于检测指定的视图是否存在。
    • has_materialized_view用于检测指定的物化视图是否存在。
    • has_index用于检测表是否包含对应的索引。
    • has_relation用于检测是否存在指定的relation,包括表、索引、序列等。
    通过执行上述命令可以获得以下返回结果。发现相应的测试均失败了,这是由于指定的表、索引等对象均不存在。
                        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)
    执行以下命令,创建相应的表/索引/视图:
    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;
    当表/索引/视图创建成功后,再次执行上述TAP测试,结果如下所示:
                 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)
  • RLS策略测试
    如果需要检测表是否存在某个行级别安全(Row-Level Security)策略,可以参考以下测试示例:
    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', -- 检测用户 "tap_user_1" 中是否为RLS策略 "tap_policy" 限制用户
        'tap_user_2' -- 检测用户 "tap_user_2" 中是否为RLS策略 "tap_policy" 限制用户
      ]
    );
    SELECT policies_are(
      'public',
      'tap_table',
      ARRAY [
        'tap_policy' -- 检测表 "tap_policy" 中是否存在RLS策略 "tap_policy"
      ]
    );
    SELECT * FROM check_test(
        policy_roles_are(
          'public',
          'tap_table',
          'tap_policy',
          ARRAY [
            'tap_user_1' -- 检测用户 "tap_user_1" 中是否为RLS策略 "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;
    说明 其中:
    • policy_cmd_is用于检测RLS策略应用的语句类型。
    • policy_roles_are用于检测RLS策略是否应用于其指定的所有用户。当且仅当其中指定了该RLS策略应用的所有用户,函数才会返回TRUE
    • policies_are用于检测表中是否含有某RLS策略。
    为了测试预期内的错误结果时,可以使用check_test方法指定预期结果为TRUEFALSE。上述测试将返回如下结果:
                                       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)
  • 列测试
    如果需要检测表中的某列是否存在以及该列是否为主键/外键等内容,可以参考以下测试示例:
    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);
    -- 测试 "col" 列是否为 "tap_table" 表的主键
    SELECT col_is_pk('tap_table', 'col');
    -- 测试 "tap_desc" 列是否为 "tap_table" 表的非主键列
    SELECT col_isnt_pk('tap_table', 'tap_desc');
    -- 测试 "col" 列是否为 "tap_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');
    -- 测试 "col" 列是否为 "tap_table" 表的非外键列
    SELECT col_isnt_fk('tap_table', 'col');
    -- 测试 "col" 列是否存在于 "tap_table" 表中
    SELECT has_column('tap_table', 'col');
    -- 测试 "non_col" 列是否不存在于 "tap_table" 表中
    SELECT hasnt_column('tap_table', 'non_col');
    SELECT * FROM finish();
    ROLLBACK;
    
    DROP TABLE tap_table;
    说明 其中:
    • col_is_pk用于检测某列是否为表的主键列。
    • col_isnt_pk用于检测某列是否为表的非主键列。
    • col_isnt_fk用于检测某列是否为表的非外键列。
    • has_column用于检测表中是否含有某列。
    • hasnt_column用于检测表中是否不含有某列。
    上述测试将返回如下结果:
                          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)
  • 函数测试
    如果需要对函数的返回类型、是否为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);
    -- 检测函数 "tap_function" 返回类型是否为 "text"
    SELECT function_returns('tap_function', 'text');
    -- 检测参数列表为 'integer', 'boolean', 'text'的函数 "tap_function_bool" 返回类型是否为 "boolean"
    SELECT function_returns('tap_function_bool', ARRAY['integer', 'boolean', 'text'], 'boolean');
    -- 检测函数 "tap_function" 是否为 "SECURITY DEFINER"
    SELECT is_definer('tap_function');
    -- 检测函数 "tap_function_bool" 是否为非 "SECURITY DEFINER"
    SELECT isnt_definer('tap_function_bool');
    -- 检测函数 "tap_function_bool" 是否为 "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;
    说明 其中:
    • function_returns用于检测函数返回类型是否为指定类型,同时可以指定该函数的参数列表。
    • is_definer方法用于检测指定函数是否为SECURITY DEFINER
    • isnt_definer方法用于检测指定函数是否为非SECURITY DEFINER
    上述测试示例将返回以下结果:
     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)
  • 其他

    pgtap插件提供了包括上述测试在内丰富的测试方法,虽然测试内容不同,但在整体使用方式上与上述示例一致。如需获取更多信息,请参见pgtap官方文档