Skip to content

Test templates

Just like query_templates, test_templates are also Jinja template files. But while one query template could be used to generate several queries, one test template can be used to generate only one pgTAP test file.

However, many test templates can be associated with a single query. In other words, if multiple pgTAP test suites are to be written for the same query, that's possible.

The test syntax is SQL only but with some additional functions installed by pgTAP. If you are not familiar with pgTAP you can go through it's documentation. Important thing to note is that the Jinja variable {{ prepared_statement }} is made available to every test template, and at the time of rendering, it will expand to the actual query.

Let's look at a templates from the chinook example.

Refer to the test template songs_formats-afa_test.sql.j2. The first few lines are:

PREPARE song_formats (varchar, varchar) AS
{{ prepared_statement }};

Here we're using the prepared_statement Jinja variable to create a prepared statement for the user session. The name of the prepared statement is song_formats and it takes two positional args, both of type varchar.

Later in the same file, the prepared statement is executed as part of a pgTAP test case,

SELECT results_eq(
    'EXECUTE song_formats(''Iron Maiden'', ''Protected AAC audio file'')',
    $$VALUES
      ...
      ...
    $$,
    'Verify return value'
);

Check the songs_formats-afa_test.sql output file to see how the actual test file looks like.

Note

Note that the SQL query that prepared_statement Jinja var expands to will always have posargs based placeholders, even if the placeholder config in manifest file is set to variables. That's the reason why the Jinja var is named prepared_statement

Function instead of PS

Sometimes it's tedious to test for result sets returned by the query. In such cases, it helps to manipulate the result returned by the query and compare a derived property. E.g. If a query results too many rows, it's easier to compare the count than the actual values in the rows.

One limitation of prepared statements and the EXECUTE syntax for executing them is that it's not sub-query friendly i.e. it's not possible to execute a prepared statement as part of another query.

The following is NOT valid SQL

SELECT
    count(*)
FROM (EXECUTE song_formats ('Iron Maiden', 'Protected AAC audio file'));

In such cases, we can define a SQL function using the same prepared_statement Jinja variable.

An example of this can be found in the chinook example - all_artists_long_songs_test.sql.j2

CREATE OR REPLACE FUNCTION all_artists_long_songs ()
RETURNS SETOF record
AS $$
{{ prepared_statement }}
$$ LANGUAGE sql;

BEGIN;
SELECT
    plan (1);

-- start(noformat)
-- Run the tests.
SELECT is(count(*), 204::bigint) from all_artists_long_songs() AS (artist_id int, name text, duration interval);
-- Finish the tests and clean up.
-- end(noformat)

SELECT
    *
FROM
    finish ();
ROLLBACK;

Test fixtures

When it comes to automated tests, It's a very common requirement to setup some test data to be able to write test cases. pgTAP tests are not any different. In case of pgTAP one needs to create test data in the database.

Since pgTAP tests are just SQL files, test data creation can be done using SQL itself in the same file. Reusable setup code can also be extracted into SQL functions that can be created as part of importing the database schema.

The chinook directory doesn't include an example of this. But here's an example from one of my real projects that uses tapestry.

In my project, there are two entities categories and items (having tables of the same names) with one-to-many relationship i.e. one category can have multiple items.

In several pgTAP tests, a few categories and items need to be created. To do this, a function is defined as follows,

CREATE OR REPLACE FUNCTION tapestry.setup_category_n_items (cat_id varchar, item_idx_start integer, item_idx_end integer)
    RETURNS void
    AS $$
    INSERT INTO categories (id, name)
        VALUES (cat_id, initcap(replace(cat_id, '-', ' ')));
    INSERT INTO items (id, name, category_id)
    SELECT
        'item-' || t AS id,
        'Item ' || t AS name,
        cat_id AS category_id
    FROM
        generate_series(item_idx_start, item_idx_end) t;
$$
LANGUAGE sql;

And then it's used in pgTAP tests like this,

...

BEGIN;
SELECT plan(1);

-- Fixtures
-- create 2 categories, 'cat-a' and 'cat-b' each having 5 items
SELECT
    tapestry.setup_category_n_items ('cat-a', 1, 5);
SELECT
    tapestry.setup_category_n_items ('cat-b', 6, 10);

-- Test cases

...