Данная библиотека предназначена для модульного и интеграционного тестирование приложений в основе, которых лежит бизнес-логика, зашитая в PL/PgSQL код.
Библиотека состоит из десятка хранимых процедуры, необходимых для минимального функционирования тестовой инфраструктуры.
Надо протестировать работу хранимой процедуры get_cart_item_count(), которая возвращает количество товаров в корзине покупателя.
CREATE OR REPLACE FUNCTION __test_get_cart_item_count() RETURNS void AS
$BODY$
DECLARE
  _count int;                     -- Кол-во товаров в корзине, возращенное процедурой get_cart_item_count()
  _i int;                         -- Счетчик в цикле
  _ITEM_COUNT CONSTANT int := 7;  -- Ожидаемое кол-во товаров в корзине
BEGIN
  -- Тут может быть код, создающий тестового покупателя
  ...
  PERFORM clear_cart(...);                                                -- Очистка корзины
  FOR _i IN 1.._ITEM_COUNT                                                -- Добавление в корзину 7 товаров
  LOOP
    PERFORM add_item_to_cart(...);
  END LOOP;
  _count := get_cart_item_count(...);                                     -- Получение числа товаров в корзине
  PERFORM pgunit.assert_equals(_ITEM_COUNT, _count, 'Incorrect value');   -- Проверка значения
END
$BODY$ LANGUAGE plpgsql;
Теперь надо запустить тестовую процедуру внутри специальной процедуры, которая может выполнять дополнительные операции.
SELECT pgunit.run_test('__test_get_cart_item_count()');
---
#OK
Тест прошел успешно.
Благодаря тому, что процедура-обертка run_test откатывает данные, которые были созданы, изменены, удалены в процедуре тестирования, то после выполнения теста база остаётся в том же состоянии, в котором она была до начала тестирования.
Ниже располагается документация по установке и использованию библиотеки.
Скачать библиотеку можно с GitHub'а - https://github.com/danblack/pgunit-sql/archive/master.zip
Установка производится с помощью утилиты psql, которую надо запустить из папки библиотеки
$ psql -U sysdba database_to_test < reinstall.sql
Данный способ установки гарантирует, что все процедуры установились без ошибок, так как в процессе установки происходит самотестирование библиотеки.
В основе библиотеки лежат хранимые процедуры, выполняющие сравнения переменных между собой или сравнение переменных с различными константами.
Сравнивает два элемента. Если они не равны, то формируется исключение #assert_equalsn{custom_message}
Сравнивает два элемента. Если они равны, то формируется исключение #assert_not_equalsn{custom_message}
Сравнивает два массива. Массивы считаются равными, если эти массивы имеют одинаковые элементы и размеры массивов равны. Если массивы не равны, то генерируется исключение с текстом #assert_array_equalsn{custom_message}
Сравнивает _value c True. Если они не равны, то формируется исключение #assert_truen{custom_message}
Сравнивает _value c False. Если они не равны, то формируется исключение #assert_falsen{custom_message}
Сравнивает _value c NULL. Если они не равны, то формируется исключение #assert_nulln{custom_message}
Сравнивает _value c NULL. Если они равны, то формируется исключение #assert_not_nulln{custom_message}
Генерирует исключение с текстом #assert_failn{custom_message}
Запускает указанную хранимую процедуру внутри тестовой инфраструктуры. После запуска тестовой процедуры происходит откат данных.
Этот тип теста можно отнести к модульному, так как тестируется только математика.
Процедура, которую надо протестировать.
CREATE OR REPLACE FUNCTION get_vat_amount(_cost numeric, _pct numeric) RETURNS numeric AS
$BODY$
DECLARE
BEGIN
  IF _cost IS NULL OR _pct IS NULL THEN
    RAISE EXCEPTION '#error:param_is_null';
  END IF;
  IF _pct = 0 THEN
    RAISE EXCEPTION '#error:vat_is_zero';
  END IF;
  RETURN _cost * _pct / 100;
END
$BODY$ LANGUAGE plpgsql;
Процедура тестирования.
CREATE OR REPLACE FUNCTION __test_vat() RETURNS void AS
$BODY$
DECLARE
  _vat numeric;
BEGIN
  -----------------------------------------
  RAISE DEBUG '#trace Check a simple case';
  -----------------------------------------
  _vat := get_vat_amount(200, 18);
  PERFORM pgunit.assert_equals(36::numeric, _vat, 'VAT is not 36');  -- Compare a returned vat and an expected value
  -------------------------------------------
  RAISE DEBUG '#trace Check null parameters';
  -------------------------------------------
  BEGIN
    PERFORM get_vat_amount(NULL, NULL);
    PERFORM pgunit.fail('Parameters are null');   -- Raise an error if get_vat_amount runs without an exception
  EXCEPTION
    WHEN others THEN
      IF SQLERRM <> '#error:param_is_null' THEN   -- Check an error text
        RAISE;                                    -- It's not an expected error, throw an exception forward
      END IF;
  END;
  BEGIN
    PERFORM get_vat_amount(100, NULL);
    PERFORM pgunit.fail('Parameters are null');
  EXCEPTION
    WHEN others THEN
      IF SQLERRM <> '#error:param_is_null' THEN
        RAISE;
      END IF;
  END;
  BEGIN
    PERFORM get_vat_amount(NULL, 18);
    PERFORM pgunit.fail('Parameters are null');
  EXCEPTION
    WHEN others THEN
      IF SQLERRM <> '#error:param_is_null' THEN
        RAISE;
      END IF;
  END;
  _vat := get_vat_amount(0, 18);
  PERFORM pgunit.assert_equals(0::numeric, _vat, 'VAT is not zero');
  BEGIN
    PERFORM get_vat_amount(100, 0);
    PERFORM pgunit.fail('Zero VAT');
  EXCEPTION
    WHEN others THEN
      IF SQLERRM <> '#error:vat_is_zero' THEN
        RAISE;
      END IF;
  END;
  ---------------------------------------
  RAISE DEBUG '#trace Check other cases';
  ---------------------------------------
  _vat := get_vat_amount(300, 18);
  PERFORM pgunit.assert_equals(54::numeric, _vat, 'VAT is not 54');
END
$BODY$ LANGUAGE plpgsql;
Запуск теста.
SELECT pgunit.run_test('__test_vat()');
---
#OK
Этот тест можно отнести к интеграционным, так как тестирование проводится в окружении, приближенном к реальному.
Схема базы данных
CREATE TABLE customers(
    id serial PRIMARY KEY,
    name varchar NOT NULL UNIQUE
);
CREATE TABLE payments(
    id serial PRIMARY KEY,
    date date NOT NULL,
    customer_id int NOT NULL REFERENCES customers(id),
    amount numeric(19, 2) NOT NULL
);
CREATE TABLE orders(
    id serial PRIMARY KEY,
    date date NOT NULL,
    customer_id int NOT NULL REFERENCES customers(id),
    amount numeric(19, 2) NOT NULL
);
Процедура расчета баланса
CREATE OR REPLACE FUNCTION get_balance(_customer_id int, _date date) RETURNS numeric AS
$BODY$
DECLARE
  _payment_amount numeric;
  _order_amount numeric;
BEGIN
  IF NOT EXISTS(SELECT 1 FROM customers WHERE id = _customer_id) THEN
    RAISE EXCEPTION '#error:customer_not_found';
  END IF;
  SELECT COALESCE(sum(amount), 0) INTO _payment_amount FROM payments WHERE customer_id = _customer_id AND date < _date;
  SELECT COALESCE(sum(amount), 0) INTO _order_amount FROM orders WHERE customer_id = _customer_id AND date < _date;
  RETURN _payment_amount - _order_amount;
END
$BODY$ LANGUAGE plpgsql;
Процедура тестирования.
CREATE OR REPLACE FUNCTION __test_get_balance() RETURNS void AS
$BODY$
DECLARE
  _customer1_id int;
  _customer2_id int;
  _customer3_id int;
  _balance numeric;
BEGIN
  ---------------------------------------
  RAISE DEBUG '#trace Prepare customers';
  ---------------------------------------
  INSERT INTO customers(name) VALUES('customer1')
    RETURNING id INTO _customer1_id;
  INSERT INTO customers(name) VALUES('customer2')
    RETURNING id INTO _customer2_id;
  _customer3_id = -1;
  ----------------------------------------
  RAISE DEBUG '#trace Check zero balance';
  ----------------------------------------
  _balance := get_balance(_customer1_id, 'infinity');
  PERFORM pgunit.assert_equals(0::numeric, _balance, 'Balance is not zero');
  -------------------------------------------------
  RAISE DEBUG '#trace Check not existing customer';
  -------------------------------------------------
  BEGIN
    _balance := get_balance(_customer3_id, 'infinity');
    PERFORM pgunit.fail('Found not existing customer');
  EXCEPTION
    WHEN others THEN
      IF SQLERRM <> '#error:customer_not_found1' THEN
        RAISE;
      END IF;
  END;
  ---------------------------------------------------------
  RAISE DEBUG '#trace Check a customer with payments only';
  ---------------------------------------------------------
  INSERT INTO payments(customer_id, date, amount) VALUES(_customer1_id, '2020-01-02', 312);
  _balance := get_balance(_customer1_id, '-infinity');
  PERFORM pgunit.assert_equals(0::numeric, _balance, '');
  _balance := get_balance(_customer1_id, 'infinity');
  PERFORM pgunit.assert_equals(312::numeric, _balance, '');
  _balance := get_balance(_customer1_id, '2020-01-02');
  PERFORM pgunit.assert_equals(0::numeric, _balance, '');
  _balance := get_balance(_customer1_id, '2020-01-03');
  PERFORM pgunit.assert_equals(312::numeric, _balance, '');
  -- ... continue
  -------------------------------------------------------
  RAISE DEBUG '#trace Check a customer with orders only';
  -------------------------------------------------------
  INSERT INTO orders(customer_id, date, amount) VALUES(_customer2_id, '2020-01-02', 231);
  _balance := get_balance(_customer2_id, '-infinity');
  PERFORM pgunit.assert_equals(0::numeric, _balance, '');
  _balance := get_balance(_customer2_id, 'infinity');
  PERFORM pgunit.assert_equals(-231::numeric, _balance, '');
  _balance := get_balance(_customer2_id, '2020-01-02');
  PERFORM pgunit.assert_equals(0::numeric, _balance, '');
  _balance := get_balance(_customer2_id, '2020-01-03');
  PERFORM pgunit.assert_equals(-231::numeric, _balance, '');
  -- ... so on
  -------------------------------------------------------------
  RAISE DEBUG '#trace Check customer with payments and orders';
  -------------------------------------------------------------
  INSERT INTO orders(customer_id, date, amount) VALUES(_customer1_id, '2020-01-01', 231);
  _balance := get_balance(_customer1_id, '-infinity');
  PERFORM pgunit.assert_equals(0::numeric, _balance, '');
  _balance := get_balance(_customer1_id, '2020-01-01');
  PERFORM pgunit.assert_equals(0::numeric, _balance, '');
  _balance := get_balance(_customer1_id, '2020-01-02');
  PERFORM pgunit.assert_equals(-231::numeric, _balance, '');
  _balance := get_balance(_customer1_id, '2020-01-03');
  PERFORM pgunit.assert_equals(81::numeric, _balance, '');
  -- ... continue yourself
END
$BODY$ LANGUAGE plpgsql;
Запуск теста.
SELECT pgunit.run_test('__test_get_balance()');
---
#OK