pgmock
Описание
PostgreSQL 9.4+ extension for unit tests
Языки
PLpgSQL
- Shell
- Makefile
pgmock
Описание
Расширение для PostgreSQL 9.4+ для создания заглушек для юнит-тестов
Основная идея
Расширение
решает задачу изоляции тестируемых хранимых процедур от существующего контекста. По запросу оно создает заглушку для тестируемой хранимой процедуры с подмененным контекстом - тестовым контекстом. По завершению тест-сьюта тестовый контекст автоматически разрушается. Это возможно благодаря транзакционному
, а также специальной схеме
, в которой и создается тестовый контекст
Расширение имеет всего лишь одну функцию
, которая принимает на вход запрос по созданию тестового контекста в виде
-объекта:
select pgmock.mock($$ { "oid": "${'myfunction'::regproc}", "dependencies": ["${'mytable_1'::regclass}", "${'mytable_2'::regclass}"] }$$);
В запросе выше создается заглушка для функции
с подмененным контекстом - таблицами
и
. Теперь можно тестировать функцию, а точнее созданную вместо неё заглушку
, не опасаясь, что изменение данных в таблицах
или
может затронуть ваш тест-сьют
Также при вызове функции
генерируются две специальных функции:
- проводит настройку тестового контекстаpg_temp.setup
- проводит очистку тестового контекстаpg_temp.teardown
Расширение
придерживается следующей философии:
- тестовый контекст создается один раз в рамках тест-сьюта
- тестовый контекст создается в рамках транзакции
- тесты в рамках тест-сьюта должны быть "обернуты" вызовами
иpg_temp.setup
, что позволяет им быть контекстно-независимымиpg_temp.teardown
Таким образом, ваш тест-сьют может выглядеть следующим образом:
begin;
select pgmock.mock($$ { "oid": "${'myfunction'::regproc}", "dependencies": ["${'mytable_1'::regclass}", "${'mytable_2'::regclass}"] }$$);
select pg_temp.setup();
insert into pg_temp.mytable_1 (foo) values ('bar');insert into pg_temp.mytable_2 (foo) values ('baz');
select pg_temp.myfunction() = 'Ожидаемый результат функции на добавленных выше данных';
select pg_temp.teardown();
select pg_temp.setup();
insert into pg_temp.mytable_1 (foo) values ('bar2');insert into pg_temp.mytable_2 (foo) values ('baz2');
select pg_temp.myfunction() = 'Ожидаемый результат функции на других данных' ||' (ранее добавленные данные не помешают тесту,' ||' т.к. функция pg_temp.teardown позаботилась об' ||' очистке тестового контекста';
select pg_temp.teardown();
rollback;
Установка
Сборка
из исходников и его установка осуществляются так:
git clone https://github.com/avito-tech/pgmock.gitcd pgmocksudo make install
После установки прогоните тесты:
make installcheck
Включите
для вашей базы данных:
create schema pgmock;create extension pgmock with schema pgmock;
Установка расширения в свою схему настоятельно рекомендуется. Это позволит избежать конфликтов имен
Примеры использования
Примеры использования отсортированы от простых к сложным
Создание заглушки для функции без состояния
Допустим, у нас есть функция
:
create or replace function public.universal_answer() returns integer immutable language sql as$func$ select 42;$func$;
Тогда создание заглушки для функции будет выглядеть следующим образом:
select pgmock.mock($$"${'public.universal_answer'::regproc}"$$);
Для расшифровывания имен объектов в их идентификаторы используется специальный синтаксис
. Поддерживаются следующие типы объектов:
- для функцийregproc
- для таблицregclass
После вызова функции
создается заглушка
select public.universal_answer() = pg_temp.universal_answer() as is_equal; is_equal---------- t
Также есть возможность создать заглушку с другим именем:
select pgmock.mock($$ { "oid": "${'public.universal_answer'::regproc}", "mock_name": "another_universal_answer" }$$);
После вызова функции
создается заглушка
Создание заглушки для читающей функции
Допустим, мы хотим создать заглушку для функции
, которая читает данные из таблицы
:
create table public.cats ( cat_id serial primary key, cat_name text not null, cat_age_in_months smallint not null);
insert into public.cats (cat_name, cat_age_in_months)values ('Barsik', 12), ('Murzik', 10), ('Luska', 23), ('Rijik', 3), ('Snejok', 38), ('Barsik', 8);
create function public.get_cat_stats( out oldest_cat_name text, out youngest_cat_name text, out min_age_in_months smallint, out avg_age_in_months smallint, out max_age_in_months smallint) returns record language plpgsql as$func$begin select first_value(c.cat_name) over (order by c.cat_age_in_months desc) as oldest_cat_name, first_value(c.cat_name) over (order by c.cat_age_in_months asc) as youngest_cat_name, min(c.cat_age_in_months) over () as min_age_in_months, round(avg(c.cat_age_in_months) over ()) as avg_age_in_months, max(c.cat_age_in_months) over () as max_age_in_months into oldest_cat_name, youngest_cat_name, min_age_in_months, avg_age_in_months, max_age_in_months from public.cats c limit 1;
return;end;$func$;
Запрос создания заглушки будет выглядеть следующим образом:
select pgmock.mock($$ { "oid": "${'public.get_cat_stats'::regproc}", "dependencies": "${'public.cats'::regclass}" }$$);
Параметр запроса
говорит, что для функции существуют зависимости в виде указанных объектов (в данном случае таблица
), поэтому:
- должны быть созданы заглушки для зависимых объектов
- использование оригинальных объектов в функции
должно быть подменено на использование заглушекpublic.get_cat_stats
В результате будет создана таблица-заглушка
и заглушка для функции
. Это позволяет проводить тестирование в изоляции от существующих данных
Создание заглушки для пишущей функции
Допустим, что у нас есть функция
, которая читает данные из таблицы
, производит трансформацию считанных данных, а затем записывает их в таблицу
:
create table public.product_movement( movement_id serial primary key, product_id integer not null, movement integer not null);
create table public.product_movement_mv( product_id integer primary key, movement integer not null);
create function public.product_movement_aggregator() returns void language plpgsql as$func$begin with w_aggregated_movement as ( select pm.product_id, sum(pm.movement) as movement from public.product_movement pm group by pm.product_id having sum(pm.movement) != 0 ), w_deleted_movement as ( delete from public.product_movement_mv pmm where pmm.product_id in ( select a.product_id from w_aggregated_movement a ) ) insert into public.product_movement_mv (product_id, movement) select a.product_id, a.movement from w_aggregated_movement a;end;$func$;
insert into public.product_movement (product_id, movement)values (1, 10), (1, 20), (1, 30), (1, -10), (1, -20), (1, -30), (1, 5), (2, 10), (2, 10), (2, 10), (2, -10), (2, -10), (2, 15), (2, 10);
insert into public.product_movement_mv (product_id, movement)values (1, 5), (2, 25);
Тогда запрос на создание заглушки будет выглядеть следующим образом:
select pgmock.mock($$ { "oid": "${'public.product_movement_aggregator'::regproc}", "dependencies": [ "${'public.product_movement'::regclass}", "${'public.product_movement_mv'::regclass}" ] }$$);
Создание заглушки для таблицы с наследованием ограничений целостности
По умолчанию функция
создает заглушку для таблицы только с наследованием структуры таблицы. Допустим, мы хотим создать заглушку для таблицы
и унаследовать некоторые ограничения целостности:
create table public.cats ( cat_id serial not null, cat_name text not null, cat_age_in_month smallint not null check (cat_age_in_month > 0), is_kitten boolean not null, constraint cats_pkey primary key (cat_id), constraint cats_name_ukey unique (cat_name), constraint cats_is_kitten_ck check ( is_kitten and cat_age_in_month <= 2 or not is_kitten and cat_age_in_month > 2 ));
Тогда запрос на создание заглушки будет выглядеть следующим образом:
select pgmock.mock($$ { "oid": "${'public.cats'::regclass}", "constraints": ["cats_pkey", "cats_is_kitten_ck", "cats_name_ukey"], "not_nulls": ["cat_name", "cat_age_in_month"] }$$);
Функция
создаст заглушку следующего вида:
\d+ cats Table "pg_temp_3.cats" Column | Type | Modifiers | Storage | Stats target | Description------------------+----------+-----------+----------+--------------+------------- cat_id | integer | not null | plain | | cat_name | text | not null | extended | | cat_age_in_month | smallint | not null | plain | | is_kitten | boolean | | plain | |Indexes: "cats_pkey" PRIMARY KEY, btree (cat_id) "cats_name_ukey" UNIQUE CONSTRAINT, btree (cat_name)Check constraints: "cats_is_kitten_ck" CHECK (is_kitten AND cat_age_in_month <= 2 OR NOT is_kitten AND cat_age_in_month > 2)
Ограничение для колонки
не было унаследовано, т.к. мы этого не просили
Опциональный параметр
принимает на вход список имен ограничений целостности, которые нужно унаследовать от оригинальной таблицы. Поддерживаются следующие типы ограничений целостности:
- primary key
- unique constraint
- check constraint
ограничения задаются с помощью опционального параметра
, принимающего на вход список колонок таблицы, для которых нужно унаследовать
ограничение
Создание заглушки для таблицы с наследованием значений по умолчанию
Аналогично наследованию ограничений целостности также имеется возможность наследования значений по умолчанию для указанных колонок таблицы:
create table public.cats ( cat_id serial not null primary key, cat_name text not null default 'Kot', created_at timestamp with time zone not null default now());
Запрос может выглядеть следующим образом:
select pgmock.mock($$ { "oid": "${'public.cats'::regclass}", "defaults": ["cat_id", "cat_name", "created_at"] }$$);
Функция
создаст заглушку следующего вида:
\d+ cats Table "pg_temp_3.cats" Column | Type | Modifiers | Storage | Stats target | Description------------+--------------------------+----------------------------------------------+----------+--------------+------------- cat_id | integer | default nextval('cats_cat_id_seq'::regclass) | plain | | cat_name | text | default 'Kot'::text | extended | | created_at | timestamp with time zone | default now() | plain | |
Опциональный параметр
ожидает список колонок таблицы, для которых необходимо унаследовать значения по умолчанию
Создание заглушки для триггерной функции
Дальше - больше: создаем заглушку для триггерной функции. Допустим, у нас есть таблица
, для которой создан триггер
, заполняющий таблицу
:
create table public.cats ( cat_id serial not null primary key, cat_name text not null);
create table public.cat_toys ( toy_id serial not null primary key, cat_id integer not null references public.cats (cat_id) deferrable initially deferred, toy_name text not null);
create function public.cats_aid() returns trigger language plpgsql as$func$begin if TG_OP = 'INSERT' then insert into public.cat_toys (cat_id, toy_name) values (NEW.cat_id, format('Toy for %s', NEW.cat_name)); elsif TG_OP = 'DELETE' then delete from public.cat_toys ct where ct.cat_id = OLD.cat_id; end if;
return null;end;$func$;
create trigger cats_aid_trg after insert or delete on public.cats for each row execute procedure public.cats_aid();
Запрос на создание заглушек для всего этого добра будет выглядеть следующим образом:
select pgmock.mock($$ { "oid": "${'public.cats'::regclass}", "defaults": ["cat_id"], "triggers": [{ "name": "cats_aid_trg", "procedure": { "oid": "${'public.cats_aid'::regproc}", "dependencies": "${'public.cat_toys'::regclass}" } }] }$$);
\d+ cats Table "pg_temp_3.cats" Column | Type | Modifiers | Storage | Stats target | Description----------+---------+----------------------------------------------+----------+--------------+------------- cat_id | integer | default nextval('cats_cat_id_seq'::regclass) | plain | | cat_name | text | | extended | |Triggers: cats_aid_trg AFTER INSERT OR DELETE ON cats FOR EACH ROW EXECUTE PROCEDURE pg_temp_3.cats_aid()
Опциональный параметр
ожидает массив объектов с описанием триггеров для таблицы. Объект триггера имеет следующие поля:
- имя оригинального триггераname
- запрос на создание заглушки для триггерной функции (в формате обычного запроса на создание заглушки для функции)procedure
Повторяющиеся зависимости
В данном примере рассмотрим способ создания заглушек когда зависимости повторяются. Например, есть функция
, которая вызывает две других функции
и
, которые в свою очередь пишут и читают таблицу
:
create table public.cats ( cat_id serial not null, cat_name text not null unique, constraint cats_pkey primary key (cat_id));
create function public.add_cat( name text) returns integer volatile language sql as$func$ insert into public.cats (cat_name) values (name) returning cat_id;$func$;
create function public.get_cat( name text) returns integer volatile language sql as$func$ select c.cat_id from public.cats c where c.cat_name = name;$func$;
create function public.set_cat( name text) returns integer volatile language plpgsql as$func$declare cat_id integer;begin cat_id := public.get_cat(name);
if cat_id is null then cat_id := public.add_cat(name); end if;
return cat_id;end;$func$;
insert into public.cats (cat_name) values ('Barsik'), ('Snezhok'), ('Muska');
При этом запрос на создание заглушки хочется написать так, чтобы подзапрос на создание заглушки
описывался только один раз. При таких требованиях запрос будет выглядеть следующим образом:
select pgmock.mock($$ { "oid": "${'public.set_cat'::regproc}", "dependencies": [ { "oid": "${'public.add_cat'::regproc}", "dependencies": { "oid": "${'public.cats'::regclass}", "mock_name": "cats_mock", "constraints": ["cats_pkey"], "defaults": ["cat_id"] } }, { "oid": "${'public.get_cat'::regproc}", "dependencies": "${'public.cats'::regclass}" } ] }$$);
В подзапросе:
{ "oid": "${'public.get_cat'::regproc}", "dependencies": "${'public.cats'::regclass}"}
будет переиспользован запрос на создание заглушки для таблицы
. Функция
автоматически переиспользует уже разобранные объекты (различие объектов производится по их
). Разбор зависимых объектов описанных в виде массива производится в соответствии с их индексом в массиве. При иерархическом описании (при помощи параметра
) самый глубокий объект разбирается в первую очередь
Больше примеров
Больше примеров можно найти в тестах к данному расширению, в файлах