Недавно я выложил статью со «скелетом» схемы данных, который можно использовать для создания своих схем PostgreSQL.
Помимо собственно скриптов разворачивания схемы, создания объектов, там были примеры хранимых функций и Unit-тесты на них.
В этой статье я хочу на примере pg_skeleton подробней остановиться на том, как писать тесты для хранимых функций PostgreSQL при помощи pgTAP.
Тесты pgTAP, как следует из названия, на выходе выдают текст в простом текстовом формате TAP (Test Anything Protocol). Этот формат принимается многими CI системами. Мы используем Jenkins TAP Plugin.
При установке расширения, в базе создаются хранимые функции (по-умолчанию в схеме public), которые мы будем использовать при написании тестов. Большая часть функций — различные assert'ы. С полным списком можно ознакомиться здесь: http://pgtap.org/documentation.html
Тестировать будем функции из примера схемы test_user:
Сначала устанавливаем pg_skeleton. (Если хотите сразу писать тесты в своей схеме — из инструкции по установке pg_skeleton выполните лишь часть про pgtap и загрузите расширение в бд)
Тесты я постарался сделать похожими на те, что используются в реальных проектах, и использовать побольше разных ф-ий pgTAP.
Перед началом выполнения тестов необходимо указать их количество, вызвав функцию plan(int).
В нашем примере этот вызов находится в файле test/tests/run_user.sql:
select plan(7+2+1);
В данном случае, 7 — это количество тестов запускаемых из файла user_crud.sql (тесты ф-ий), 2 — количество тестов в файле user_schema.sql, 1 — однострочный тест (проверяющий покрытие функций тестами) непосредственно в файле run_user.sql.
В документации pgTAP рассматриваются в-основном тесты вызываемые отдельными select запросами — это подходит для тестирования схемы, или проверки простых ф-ий, не имеющих побочных эффектов (такие тесты в user_schema.sql).
Но при тестировании сложных сценариев, когда необходимо вызывать несколько ф-ий, и в следующую передаётся результат предыдущей, можно объединять тесты в хранимую функцию, которая будет выполнять сценарий, содержащий несколько тестов. Пример такой функции в файле test/functions_user.sql.
Функцию нужно объявлять, как возвращающую множество строк:
create or replace function test.test_user_0010()
returns setof text as $$
--Номер в названии ф-ии необязателен, но может быть полезен для запуска тестовых ф-ий
--в определённом порядке при запуске тестов при помощи runtests().
--Объявим переменную, в которой будем хранить идентификатор добавленной записи:
declare
v_user_id integer;
begin
--Первый тест - проверяем, что функция добавления отрабатывает, не вызывая исключений:
return next lives_ok('select test_user.add_user(''testuser unique''::varchar);',
'test_user.add_user doesnt throw exception');
--Добавляем ещё одну запись, сохраняем её идентификатор, проверяем, что он корректен (>0):
v_user_id := test_user.add_user('blah blah');
return next cmp_ok(v_user_id,
'>',
0,
'test_user.add_user: returns ok');
--Проверим, что запись действительно есть.
return next results_eq('select user_name::varchar from test_user.users where user_id=' || v_user_id::varchar,
'select ''blah blah''::varchar',
'test_user.add_user inserts ok');
--Функция изменения пользователя должна возвращать идентификатор пользователя:
return next is(test_user.alter_user(v_user_id,'new user name blah'),
v_user_id,
'test_user.alter_user: returns ok');
--Проверим, что запись действительно изменилась:
return next results_eq('select user_name::varchar from test_user.users where user_id=' || v_user_id::varchar,
'select ''new user name blah''::varchar',
'test_user.alter_user updates record');
--Функция удаления пользователя должна возвращать его id:
return next is(test_user.delete_user(v_user_id),
v_user_id,
'test_user.delete_user: returns ok');
--Последний тест. Проверим, что пользователь действительно удалён:
return next is_empty('select 1 from test_user.users where user_id=' || v_user_id::varchar,
'test_user.delete_user: deletes ok');
end;
$$ language plpgsql;
Запускать тесты можно непосредственно из sql:
psql -h $db_host -p $db_port -U $db_user $db_name -f tests/run_user.sql
в этом случае мы получим чистый TAP на выходе:
plan|1..10 test_user_0010|ok 1 - test_user.add_user doesnt throw exception test_user_0010|ok 2 - test_user.add_user: returns ok test_user_0010|ok 3 - test_user.add_user inserts record test_user_0010|ok 4 - test_user.alter_user: returns ok test_user_0010|ok 5 - test_user.alter_user updates record test_user_0010|ok 6 - test_user.delete_user: returns ok test_user_0010|ok 7 - test_user.delete_user: deletes ok tables_are|ok 8 - Schema test_user contains users table columns_are|ok 9 - test_user.users column check test_scheme_check_func|ok 10 - All functions in schema test_user are covered with tests.
Либо при помощи утилиты pg_prove:
pg_prove -h $db_host -p $db_port -d $db_name -U $db_user tests/run_*.sql
Тогда вывод будет более чиловекочитаемый:
tests/run_user.sql .. ok All tests successful. Files=1, Tests=10, 0 wallclock secs ( 0.04 usr + 0.00 sys = 0.04 CPU) Result: PASS
В pg_skeleton переменные для хоста, порта, имени пользователя и бд за вас подставит скрипт /test/run_tests.sh
Надеюсь, что теперь у всех, у кого есть код в хранимых функциях PostgreSQL, будут unit-тесты!
Автор: CPro