Pgtap
Unit testing for Postgresql

Lucio Grenzi
l.grenzi@gmail.com

PGDay.IT 2013 – 25 Ottobre 2013 - Prato

1 di 24
Who I am
Delphi developer since 1999
IT Consultant
Front end web developer
Postgresql addicted
Nonantolando.blogspot.com
lucio.grenzi
lucio grenzi

PGDay.IT 2013 – 25 Ottobre 2013 - Prato

2 di 24
Agenda
PgTap: introduction
Why use this tool
Best practices
Q&A

PGDay.IT 2013 – 25 Ottobre 2013 - Prato

3 di 24
Question before starting

Why would you want to unit
test your database?

PGDay.IT 2013 – 25 Ottobre 2013 - Prato

4 di 24
Why use pgTap
Backend application development
Test schema object validation
Module development
Continuos integration

PGDay.IT 2013 – 25 Ottobre 2013 - Prato

5 di 24
Tap protocol
The Test Anything Protocol (TAP) is a protocol to allow
communication between unit tests and a test harness. It
allows individual tests (TAP producers) to communicate
test results to the testing harness in a language-agnostic
way. Originally developed for unit testing of the Perl
interpreter in 1987, producers and parsers are now
available for many development platforms.
-wikipedia-

PGDay.IT 2013 – 25 Ottobre 2013 - Prato

6 di 24
PgTap

pgTAP is a unit testing framework for PostgreSQL written
in PL/pgSQL and PL/SQL. It includes a comprehensive
collection of TAP-emitting assertion functions, as well as
the ability to integrate with other TAP-emitting test
frameworks. It can also be used in the xUnit testing style.
-http://pgtap.org/-

PGDay.IT 2013 – 25 Ottobre 2013 - Prato

7 di 24
PgTap now
www.pgtap.org
Latest version is 0.93.0
Already packaged for the most important linux
distributions
make
make installcheck
make install

PGDay.IT 2013 – 25 Ottobre 2013 - Prato

8 di 24
Requirements
PostgreSQL 8.1 or higher
with 8.4 or higher recommended for full use of its API

PL/pgSQL
On Windows servers is necessary to install Perl
Perl on Linux is no more necessary but it is required by
pg_prove

PGDay.IT 2013 – 25 Ottobre 2013 - Prato

9 di 24
Adding PgTap to a database
Install pgtap in a database
psql -d dbname -f pgtap.sql

include the call to pgtap.sql in your script with
/pgtap.sql

i

remove pgtap from a database
psql -d dbname -f uninstall_pgtap.sql

PGDay.IT 2013 – 25 Ottobre 2013 - Prato

10 di 24
Tap in practice
Test output is easy to understand
BEGIN;
SELECT plan(); ---- how many test?
…put your tests here…
SELECT * FROM finish(); ---- test finished, print report
ROLLBACK;

PGDay.IT 2013 – 25 Ottobre 2013 - Prato

11 di 24
PgTap functions - compare
ok()
is()
isnt()
matches()
doesnt_match()
alike()
unalike()
cmp_ok()
pass()
fail()

SELECT ok( :boolean, :description );
SELECT is ( :have, :want, :description);
SELECT isnt(:have, :want, :description);
SELECT matches( :have, :regex, :description );
SELECT doesnt_match( :have, :regex, :description );
SELECT alike( :this, :like, :description );
SELECT unalike( :this, :like, :description );
SELECT cmp_ok( :have, :op, :want, :description );
SELECT pass( :description );
SELECT fail( :description );

PGDay.IT 2013 – 25 Ottobre 2013 - Prato

12 di 24
PgTap functions – test failures
throws_ok()
throws_like()
throws_matching()
lives_ok()
performs_ok()

SELECT throws_ok( :sql, :errcode, :ermsg,
:description );

PGDay.IT 2013 – 25 Ottobre 2013 - Prato

13 di 24
PgTap functions – test objects
tablespaces_are()
schemas_are()
tables_are()
views_are()
sequences_are()
columns_are()
indexes_are()
triggers_are()
functions_are()
roles_are()
users_are()

groups_are()
languages_are()
opclasses_are()
rules_are()
types_are()
domains_are()
enums_are()
casts_are()
operators_are()

PGDay.IT 2013 – 25 Ottobre 2013 - Prato

14 di 24
PgTap basics
set ON_ERROR_ROLLBACK 11
set ON_ERROR_ROLLBACK
set ON_ERROR_STOP true
set ON_ERROR_STOP true
set QUIET 11
set QUIET
BEGIN;
BEGIN;
SELECT plan(1);
SELECT plan(1);
SELECT pass( 'Hello PgDayit !'!');
SELECT pass( 'Hello PgDayit );
SELECT **FROM finish();
SELECT FROM finish();
ROLLBACK;
ROLLBACK;
save this as HelloPgDayit.txt and type: psql -U postgres -f HelloPgDayit.txt

1..1
1..1
ok 11- -Hello PgDayit ! !
ok
Hello PgDayit

PGDay.IT 2013 – 25 Ottobre 2013 - Prato

15 di 24
Let's create some tables
BEGIN;
i ./pgtap.sql
-- create two tables with referential constraint
create table table1 (id integer not null, t_text varchar(100), dt timestamp default now(), CONSTRAINT table1_pkey
PRIMARY KEY (id));
create table table2 (id integer not null, t_text varchar(100), id_ref integer, CONSTRAINT id_ref FOREIGN KEY
(id_ref) REFERENCES table1 (id));
insert into table1 (id,t_text) values (1,'test one');
insert into table1 (id,t_text) values (2,'test two');
insert into table1 (id,t_text) values (3,'test three');
insert into table2 (id,t_text,id_ref) values (1,'ref test one', 1);
insert into table2 (id,t_text,id_ref) values (2,'ref test two', 2);
insert into table2 (id,t_text,id_ref) values (3,'ref test three', 3);
SELECT plan(6);
## type tests here##
## get results here##
SELECT * FROM finish();
ROLLBACK;

PGDay.IT 2013 – 25 Ottobre 2013 - Prato

16 di 24
Test samples
PREPARE ids_fetched AS
PREPARE ids_fetched AS
select id from table1 where id in (1,2,3) order by id asc;
select id from table1 where id in (1,2,3) order by id asc;
PREPARE ids_expected AS VALUES (1),(2),(3);
PREPARE ids_expected AS VALUES (1),(2),(3);
SELECT results_eq( 'ids_fetched', 'ids_expected',
SELECT results_eq( 'ids_fetched', 'ids_expected',
'fetched the expected ids from table1');
'fetched the expected ids from table1');
PREPARE ids_fetched1 AS select id
PREPARE ids_fetched1 AS select id
from table1 where id in (1,2,3) order by id asc;
from table1 where id in (1,2,3) order by id asc;
PREPARE ids_fetched2 AS select id
PREPARE ids_fetched2 AS select id
from table2 where id in (1,2,3) order by id asc;
from table2 where id in (1,2,3) order by id asc;
SELECT results_eq( 'ids_fetched1', 'ids_fetched2');
SELECT results_eq( 'ids_fetched1', 'ids_fetched2');
PREPARE throw_error AS
PREPARE throw_error AS
insert into table1 (id,t_text)
insert into table1 (id,t_text)
values (1,'duplicate key error');
values (1,'duplicate key error');
SELECT throws_ok('throw_error','23505',NULL,
SELECT throws_ok('throw_error','23505',NULL,
'duplicated key found (id)');
'duplicated key found (id)');

PGDay.IT 2013 – 25 Ottobre 2013 - Prato

17 di 24
pg_prove
command-line application to run one or more pgTAP
tests in a PostgreSQL database
output of the tests is processed by TAP::Harness in
order to summarize the results
Tests can be written as:
SQL scripts
xUnit-style database functions

PGDay.IT 2013 – 25 Ottobre 2013 - Prato

18 di 24
pg_prove output
% pg_prove ­U postgres tests/
% pg_prove ­U postgres tests/
tests/coltap.....ok
tests/coltap.....ok
tests/hastap.....ok
tests/hastap.....ok
tests/moretap....ok
tests/moretap....ok
tests/pg73.......ok
tests/pg73.......ok
tests/pktap......ok
tests/pktap......ok
All tests successful.
All tests successful.
Files=5, Tests=100,  1 wallclock secs 
Files=5, Tests=100,  1 wallclock secs 
( 0.06 usr  0.02 sys +  0.08 cusr  0.07 csys =  0.23 CPU)
( 0.06 usr  0.02 sys +  0.08 cusr  0.07 csys =  0.23 CPU)
Result: PASS
Result: PASS

PGDay.IT 2013 – 25 Ottobre 2013 - Prato

19 di 24
pg_prove - xUnit Test Functions

EATE OR REPLACE FUNCTION setup_insert(
REATE OR REPLACE FUNCTION setup_insert(
RETURNS SETOF TEXT AS $$
 RETURNS SETOF TEXT AS $$
  RETURN NEXT is( MAX(lucio), NULL, 'Should have no users') FROM speakers;
   RETURN NEXT is( MAX(lucio), NULL, 'Should have no users') FROM speakers;
  INSERT INTO speakers (lucio) VALUES ('theory');
   INSERT INTO speakers (lucio) VALUES ('theory');
 LANGUAGE plpgsql;
$ LANGUAGE plpgsql;

eate OR REPLACE FUNCTION test_user(
reate OR REPLACE FUNCTION test_user(
RETURNS SETOF TEXT AS $$
 RETURNS SETOF TEXT AS $$
  SELECT is( lucio, 'theory', 'Should have nick') FROM speakers;
   SELECT is( lucio, 'theory', 'Should have nick') FROM speakers;
D;
ND;
 LANGUAGE sql;
$ LANGUAGE sql;
% pg_prove ­­dbname pgdayit ­­runtests
% pg_prove ­­dbname pgdayit ­­runtests
runtests()....ok
runtests()....ok
All tests successful.
All tests successful.
Files=1, Tests=16,  0 wallclock secs 
Files=1, Tests=16,  0 wallclock secs 
( 0.02 usr  0.01 sys +  0.01 cusr  0.00 csys =  0.04 CPU)
( 0.02 usr  0.01 sys +  0.01 cusr  0.00 csys =  0.04 CPU)
Result: PASS
Result: PASS

PGDay.IT 2013 – 25 Ottobre 2013 - Prato

20 di 24
Conclusions
There are functions for almost everything in your
postgresql db
Triggers, Functions, Schemas, Tablespaces, ….
It is possible create relationships of, or better conditional,
tests

Stable

PGDay.IT 2013 – 25 Ottobre 2013 - Prato

21 di 24
Risorse
Citare tutte le risorse utili:
www.pgtap.org
https://github.com/theory/pgtap

PGDay.IT 2013 – 25 Ottobre 2013 - Prato

22 di 24
Questions

PGDay.IT 2013 – 25 Ottobre 2013 - Prato

23 di 24
PGDay.IT 2013 – 25 Ottobre 2013 - Prato

24 di 24

Pg tap

  • 1.
  • 2.
    Who I am Delphideveloper since 1999 IT Consultant Front end web developer Postgresql addicted Nonantolando.blogspot.com lucio.grenzi lucio grenzi PGDay.IT 2013 – 25 Ottobre 2013 - Prato 2 di 24
  • 3.
    Agenda PgTap: introduction Why usethis tool Best practices Q&A PGDay.IT 2013 – 25 Ottobre 2013 - Prato 3 di 24
  • 4.
    Question before starting Whywould you want to unit test your database? PGDay.IT 2013 – 25 Ottobre 2013 - Prato 4 di 24
  • 5.
    Why use pgTap Backendapplication development Test schema object validation Module development Continuos integration PGDay.IT 2013 – 25 Ottobre 2013 - Prato 5 di 24
  • 6.
    Tap protocol The TestAnything Protocol (TAP) is a protocol to allow communication between unit tests and a test harness. It allows individual tests (TAP producers) to communicate test results to the testing harness in a language-agnostic way. Originally developed for unit testing of the Perl interpreter in 1987, producers and parsers are now available for many development platforms. -wikipedia- PGDay.IT 2013 – 25 Ottobre 2013 - Prato 6 di 24
  • 7.
    PgTap pgTAP is aunit testing framework for PostgreSQL written in PL/pgSQL and PL/SQL. It includes a comprehensive collection of TAP-emitting assertion functions, as well as the ability to integrate with other TAP-emitting test frameworks. It can also be used in the xUnit testing style. -http://pgtap.org/- PGDay.IT 2013 – 25 Ottobre 2013 - Prato 7 di 24
  • 8.
    PgTap now www.pgtap.org Latest versionis 0.93.0 Already packaged for the most important linux distributions make make installcheck make install PGDay.IT 2013 – 25 Ottobre 2013 - Prato 8 di 24
  • 9.
    Requirements PostgreSQL 8.1 orhigher with 8.4 or higher recommended for full use of its API PL/pgSQL On Windows servers is necessary to install Perl Perl on Linux is no more necessary but it is required by pg_prove PGDay.IT 2013 – 25 Ottobre 2013 - Prato 9 di 24
  • 10.
    Adding PgTap toa database Install pgtap in a database psql -d dbname -f pgtap.sql include the call to pgtap.sql in your script with /pgtap.sql i remove pgtap from a database psql -d dbname -f uninstall_pgtap.sql PGDay.IT 2013 – 25 Ottobre 2013 - Prato 10 di 24
  • 11.
    Tap in practice Testoutput is easy to understand BEGIN; SELECT plan(); ---- how many test? …put your tests here… SELECT * FROM finish(); ---- test finished, print report ROLLBACK; PGDay.IT 2013 – 25 Ottobre 2013 - Prato 11 di 24
  • 12.
    PgTap functions -compare ok() is() isnt() matches() doesnt_match() alike() unalike() cmp_ok() pass() fail() SELECT ok( :boolean, :description ); SELECT is ( :have, :want, :description); SELECT isnt(:have, :want, :description); SELECT matches( :have, :regex, :description ); SELECT doesnt_match( :have, :regex, :description ); SELECT alike( :this, :like, :description ); SELECT unalike( :this, :like, :description ); SELECT cmp_ok( :have, :op, :want, :description ); SELECT pass( :description ); SELECT fail( :description ); PGDay.IT 2013 – 25 Ottobre 2013 - Prato 12 di 24
  • 13.
    PgTap functions –test failures throws_ok() throws_like() throws_matching() lives_ok() performs_ok() SELECT throws_ok( :sql, :errcode, :ermsg, :description ); PGDay.IT 2013 – 25 Ottobre 2013 - Prato 13 di 24
  • 14.
    PgTap functions –test objects tablespaces_are() schemas_are() tables_are() views_are() sequences_are() columns_are() indexes_are() triggers_are() functions_are() roles_are() users_are() groups_are() languages_are() opclasses_are() rules_are() types_are() domains_are() enums_are() casts_are() operators_are() PGDay.IT 2013 – 25 Ottobre 2013 - Prato 14 di 24
  • 15.
    PgTap basics set ON_ERROR_ROLLBACK11 set ON_ERROR_ROLLBACK set ON_ERROR_STOP true set ON_ERROR_STOP true set QUIET 11 set QUIET BEGIN; BEGIN; SELECT plan(1); SELECT plan(1); SELECT pass( 'Hello PgDayit !'!'); SELECT pass( 'Hello PgDayit ); SELECT **FROM finish(); SELECT FROM finish(); ROLLBACK; ROLLBACK; save this as HelloPgDayit.txt and type: psql -U postgres -f HelloPgDayit.txt 1..1 1..1 ok 11- -Hello PgDayit ! ! ok Hello PgDayit PGDay.IT 2013 – 25 Ottobre 2013 - Prato 15 di 24
  • 16.
    Let's create sometables BEGIN; i ./pgtap.sql -- create two tables with referential constraint create table table1 (id integer not null, t_text varchar(100), dt timestamp default now(), CONSTRAINT table1_pkey PRIMARY KEY (id)); create table table2 (id integer not null, t_text varchar(100), id_ref integer, CONSTRAINT id_ref FOREIGN KEY (id_ref) REFERENCES table1 (id)); insert into table1 (id,t_text) values (1,'test one'); insert into table1 (id,t_text) values (2,'test two'); insert into table1 (id,t_text) values (3,'test three'); insert into table2 (id,t_text,id_ref) values (1,'ref test one', 1); insert into table2 (id,t_text,id_ref) values (2,'ref test two', 2); insert into table2 (id,t_text,id_ref) values (3,'ref test three', 3); SELECT plan(6); ## type tests here## ## get results here## SELECT * FROM finish(); ROLLBACK; PGDay.IT 2013 – 25 Ottobre 2013 - Prato 16 di 24
  • 17.
    Test samples PREPARE ids_fetchedAS PREPARE ids_fetched AS select id from table1 where id in (1,2,3) order by id asc; select id from table1 where id in (1,2,3) order by id asc; PREPARE ids_expected AS VALUES (1),(2),(3); PREPARE ids_expected AS VALUES (1),(2),(3); SELECT results_eq( 'ids_fetched', 'ids_expected', SELECT results_eq( 'ids_fetched', 'ids_expected', 'fetched the expected ids from table1'); 'fetched the expected ids from table1'); PREPARE ids_fetched1 AS select id PREPARE ids_fetched1 AS select id from table1 where id in (1,2,3) order by id asc; from table1 where id in (1,2,3) order by id asc; PREPARE ids_fetched2 AS select id PREPARE ids_fetched2 AS select id from table2 where id in (1,2,3) order by id asc; from table2 where id in (1,2,3) order by id asc; SELECT results_eq( 'ids_fetched1', 'ids_fetched2'); SELECT results_eq( 'ids_fetched1', 'ids_fetched2'); PREPARE throw_error AS PREPARE throw_error AS insert into table1 (id,t_text) insert into table1 (id,t_text) values (1,'duplicate key error'); values (1,'duplicate key error'); SELECT throws_ok('throw_error','23505',NULL, SELECT throws_ok('throw_error','23505',NULL, 'duplicated key found (id)'); 'duplicated key found (id)'); PGDay.IT 2013 – 25 Ottobre 2013 - Prato 17 di 24
  • 18.
    pg_prove command-line application torun one or more pgTAP tests in a PostgreSQL database output of the tests is processed by TAP::Harness in order to summarize the results Tests can be written as: SQL scripts xUnit-style database functions PGDay.IT 2013 – 25 Ottobre 2013 - Prato 18 di 24
  • 19.
  • 20.
    pg_prove - xUnitTest Functions EATE OR REPLACE FUNCTION setup_insert( REATE OR REPLACE FUNCTION setup_insert( RETURNS SETOF TEXT AS $$  RETURNS SETOF TEXT AS $$   RETURN NEXT is( MAX(lucio), NULL, 'Should have no users') FROM speakers;    RETURN NEXT is( MAX(lucio), NULL, 'Should have no users') FROM speakers;   INSERT INTO speakers (lucio) VALUES ('theory');    INSERT INTO speakers (lucio) VALUES ('theory');  LANGUAGE plpgsql; $ LANGUAGE plpgsql; eate OR REPLACE FUNCTION test_user( reate OR REPLACE FUNCTION test_user( RETURNS SETOF TEXT AS $$  RETURNS SETOF TEXT AS $$   SELECT is( lucio, 'theory', 'Should have nick') FROM speakers;    SELECT is( lucio, 'theory', 'Should have nick') FROM speakers; D; ND;  LANGUAGE sql; $ LANGUAGE sql; % pg_prove ­­dbname pgdayit ­­runtests % pg_prove ­­dbname pgdayit ­­runtests runtests()....ok runtests()....ok All tests successful. All tests successful. Files=1, Tests=16,  0 wallclock secs  Files=1, Tests=16,  0 wallclock secs  ( 0.02 usr  0.01 sys +  0.01 cusr  0.00 csys =  0.04 CPU) ( 0.02 usr  0.01 sys +  0.01 cusr  0.00 csys =  0.04 CPU) Result: PASS Result: PASS PGDay.IT 2013 – 25 Ottobre 2013 - Prato 20 di 24
  • 21.
    Conclusions There are functionsfor almost everything in your postgresql db Triggers, Functions, Schemas, Tablespaces, …. It is possible create relationships of, or better conditional, tests Stable PGDay.IT 2013 – 25 Ottobre 2013 - Prato 21 di 24
  • 22.
    Risorse Citare tutte lerisorse utili: www.pgtap.org https://github.com/theory/pgtap PGDay.IT 2013 – 25 Ottobre 2013 - Prato 22 di 24
  • 23.
    Questions PGDay.IT 2013 –25 Ottobre 2013 - Prato 23 di 24
  • 24.
    PGDay.IT 2013 –25 Ottobre 2013 - Prato 24 di 24