pgTAP Documentation

repository·main·Indexed 22 days ago

https://github.com/theory/pgtap

A unit testing framework for PostgreSQL written in PL/pgSQL and PL/SQL. pgTAP provides TAP-emitting assertion functions to test database schemas, data, and logic, including equality checks, pattern matching, and xUnit-style testing via runtests().

Tokens
34.1K
Snippets
188
Records
196
Agent score
28%

What's inside pgTAP

  1. Implement xUnit-style testing with runtests()

    main

    Instead of writing procedural SQL scripts, you can collect tests into database functions and run them all at once using runtests(). This approach supports setup and teardown functions and runs each test in its own transaction.

    Pattern:

    1. Create a setup function that returns SETOF TEXT (using assertions).
    2. Create test functions that return SETOF TEXT.
    3. Call runtests() to execute them.

    Example:

    CREATE OR REPLACE FUNCTION setup_insert() RETURNS SETOF TEXT AS $$ 
    BEGIN
        RETURN NEXT is( MAX(nick), NULL, 'Should have no users') FROM users;
        INSERT INTO users (nick) VALUES ('theory');
    END;
    $$ LANGUAGE plpgsql;
    
    CREATE OR REPLACE FUNCTION test_user() RETURNS SETOF TEXT AS $$ 
        SELECT is( nick, 'theory', 'Should have nick') FROM users;
    $$ LANGUAGE sql;
    
    -- To run:
    SELECT * FROM runtests();
    SELECT * FROM runtests();
  2. How to write custom test functions

    main

    You can extend pgTAP by writing your own test functions. The core requirement is that your function must return a boolean indicating success and use pgTAP's ok() function to ensure the result is properly recorded, formatted, and sequenced in the test output.

    Implementation Patterns

    Using PL/pgSQL (for complex logic and diagnostics): Use ok(result, description) and append diagnostics using diag() if the test fails.

    CREATE OR REPLACE FUNCTION lc_is (text, text, text)
    RETURNS TEXT AS $$
    DECLARE
        result BOOLEAN;
    BEGIN
        result := LOWER($1) = LOWER($2);
        RETURN ok( result, $3 ) || CASE WHEN result THEN '' ELSE E'\n' || diag(
               '    Have: ' || $1 ||
            E'\n    Want: ' || $2
    );
    END;
    $$ LANGUAGE plpgsql;

    Using SQL (for simple wrappers): If your logic can be expressed in standard SQL, you can simply wrap an existing pgTAP function like is().

    CREATE OR REPLACE FUNCTION lc_is ( TEXT, TEXT, TEXT )
    RETURNS TEXT AS $$
        SELECT is( LOWER($1), LOWER($2), $3);
    $$ LANGUAGE sql;
    CREATE OR REPLACE FUNCTION lc_is (text, text, text)
    RETURNS TEXT AS $$
    DECLARE
        result BOOLEAN;
    BEGIN
        result := LOWER($1) = LOWER($2);
        RETURN ok( result, $3 ) || CASE WHEN result THEN '' ELSE E'\n' || diag(
               '    Have: ' || $1 ||
            E'\n    Want: ' || $2
    );
    END;
    $$ LANGUAGE plpgsql;
  3. Manage TODO tests with todo_start and todo_end

    main

    You can group multiple tests under a single TODO block using todo_start() and todo_end(). This is useful for marking a section of tests as work-in-progress. You can also nest these blocks, though it is generally not recommended.

    If you use the todo() function inside a todo_start() block, you can specify the number of tests that the TODO applies to.

    Warning: todo_end() is fatal if called without a preceding todo_start() call.

    SELECT todo_start('working on this');
    -- lots of code
    SELECT todo_start('working on that');
    -- more code
    SELECT todo_end();
    SELECT todo_end();
  4. How to describe tests for better diagnostics

    main

    By convention, pgTAP assigns numbers to tests. You can provide an optional :description argument to most test functions. Using descriptions is highly recommended because:

    1. It makes it easier to identify which test failed in your script.
    2. It provides context in the TAP output (e.g., ok 4 - basic multi-variable instead of just ok 4).
    3. It allows for better failure diagnostics.
  5. Verify database schema identifiers

    main

    When testing for the existence of tables, schemas, or functions, pgTAP uses a simple equivalence test (=) for identifiers.

    Important: You should generally use lowercase strings for identifier arguments. If an object was created with double quotes to preserve mixed case (e.g., CREATE TABLE "Foo" ...), you must use the exact case in your test (e.g., SELECT has_table('Foo');). Otherwise, use lowercase (e.g., SELECT has_table('foo');).

  6. Manage test plans with plan() and finish()

    main

    To ensure test integrity and proper TAP output, you must manage the test lifecycle.

    1. Declare a plan: Before running assertions, declare how many tests you intend to run. This prevents premature failure if a test is skipped or fails to run.

    SELECT plan(42); -- Declare exactly 42 tests
    -- Or calculate dynamically:
    SELECT plan( COUNT(*) ) FROM foo;
    -- Or if the number is unknown (not recommended):
    SELECT * FROM no_plan();

    2. Finalize the tests: At the end of your script, call finish() to output diagnostics and check if the number of tests run matches the plan.

    SELECT * FROM finish();
    -- To throw an exception if tests failed:
    SELECT * FROM finish(true);
    SELECT plan(1);
    SELECT pass('test');
    SELECT * FROM finish();
  7. Install pgTAP

    main

    pgTAP must be installed on a host with a running PostgreSQL server; it cannot be installed remotely.

    Linux (Debian/Ubuntu/Mint):

    sudo apt-get install pgtap

    Manual Build (Other systems):

    1. Download pgTAP from PGXN.
    2. Extract the zip and navigate to the folder.
    3. Run the following commands:
    make
    make install
    make installcheck

    Note for Docker users: You must install pgTAP inside the Docker container.

    sudo apt-get install pgtap
  8. Install pgTAP to a custom prefix (PostgreSQL 18+)

    main

    For PostgreSQL 18 or later, you can install the extension into a custom prefix by passing the prefix argument to the install target.

    Note: Only use the prefix argument with make install, not with other targets.

    make install prefix=/usr/local/extras

    After installation, you must update your postgresql.conf to include the new paths:

    extension_control_path = '/usr/local/extras/postgresql/share:$system'
    dynamic_library_path   = '/usr/local/extras/postgresql/lib:$libdir'
  9. Run tests using pg_prove

    main

    The pg_prove Perl program (from the TAP::Parser::SourceHandler::pgTAP CPAN distribution) is the recommended way to run batches of test scripts or xUnit functions.

    Running SQL test scripts:

    pg_prove -U postgres sql/*.sql

    Running xUnit-style test functions: If you have defined your tests as database functions, use the --runtests flag:

    pg_prove -d myapp --runtests

    Useful flags:

    • --verbose: See individual test descriptions.
    • --help: View supported options.
    • --man: View full documentation.
    pg_prove -U postgres sql/*.sql
  10. DRY up tests using SQL relations

    main

    Since SQL is designed to operate on sets of rows, you can avoid manual loops for repetitive tests by using the VALUES command and SELECT statements. This allows you to run a single pgTAP test function against multiple inputs (like different schemas or columns) in one go.

    To test if a table exists across multiple schemas:

    SELECT has_table(sch, 'widgets', format('Has %I.widgets', sch))
      FROM (VALUES('amazon'), ('starbucks'), ('boeing')) F(sch);

    To test that various columns are NOT NULL in a specific table across multiple schemas, use a CROSS JOIN:

    SELECT col_not_null(sch, 'table1', col)
      FROM (VALUES('schema1'), ('schema1')) AS stmp (sch)
     CROSS JOIN (VALUES('col_pk'), ('col2'), ('col3')) AS ctmp (col);
    SELECT has_table(sch, 'widgets', format('Has %I.widgets', sch))
      FROM (VALUES('amazon'), ('starbucks'), ('boeing')) F(sch);
    
    SELECT col_not_null(sch, 'table1', col)
      FROM (VALUES('schema1'), ('schema1')) AS stmp (sch)
     CROSS JOIN (VALUES('col_pk'), ('col2'), ('col3')) AS ctmp (col);
  11. Run pgTAP tests in Docker

    main

    To test pgTAP in a local Docker environment using the latest PostgreSQL version:

    cd test
    docker compose build test
    # Start the postgres server in the background
    docker compose up -d test
    # Run regression tests (builds, installs, and runs installcheck)
    docker compose exec test make install installcheck
    # Run the full test suite with pg_prove
    docker compose exec test run
    # Shut down the container
    docker compose down

    To test against a specific PostgreSQL version, set the pgtag environment variable before running the commands:

    export pgtag=12-alpine
    docker compose exec test run
  12. Troubleshoot pgTAP installation issues

    main

    Common installation errors and their solutions:

    • Makefile: line 8: Need an operator: Use GNU make (gmake) instead of make.
    • make: pg_config: Command not found or pgTAP requires PostgreSQL 9.1 or later: Ensure pg_config is in your PATH. If using an RPM-based system, install the -devel package. You can manually point to the config tool:
      env PG_CONFIG=/path/to/pg_config make && make install && make installcheck
    • Fallback method: Copy the distribution to the contrib/ subdirectory of the PostgreSQL source tree and run:
      env NO_PGXS=1 make && make install && make installcheck
    • ERROR: must be owner of database regression: Run the test suite as a superuser:
      make installcheck PGUSER=postgres
    • ERROR: Missing extensions required for testing: citext ltree: Install the PostgreSQL contrib modules.
    make installcheck PGUSER=postgres