Как стать автором
Обновить

Тестирование хранимых функций с помощью pgTAP

Время на прочтение4 мин
Количество просмотров14K
Недавно я выложил статью со «скелетом» схемы данных, который можно использовать для создания своих схем 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-тесты!
Теги:
Хабы:
+11
Комментарии2

Публикации

Изменить настройки темы

Истории

Работа

Ближайшие события

PG Bootcamp 2024
Дата16 апреля
Время09:30 – 21:00
Место
МинскОнлайн
EvaConf 2024
Дата16 апреля
Время11:00 – 16:00
Место
МоскваОнлайн
Weekend Offer в AliExpress
Дата20 – 21 апреля
Время10:00 – 20:00
Место
Онлайн