Содержание

Предыдущий раздел

Вредные советы для программистов

Следующий раздел

Статьи

Тестирование приложений на примере библиотеки pg_unit

Данная библиотека предназначена для модульного и интеграционного тестирование приложений в основе, которых лежит бизнес-логика, зашитая в 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

Данный способ установки гарантирует, что все процедуры установились без ошибок, так как в процессе установки происходит самотестирование библиотеки.

Функции

В основе библиотеки лежат хранимые процедуры, выполняющие сравнения переменных между собой или сравнение переменных с различными константами.

pgunit.assert_equals(_expected anyelement, actual anyelement, custom_message varchar)

Сравнивает два элемента. Если они не равны, то формируется исключение #assert_equalsn{custom_message}

pgunit.assert_not_equals(_expected anyelement, actual anyelement, custom_message varchar)

Сравнивает два элемента. Если они равны, то формируется исключение #assert_not_equalsn{custom_message}

pgunit.assert_array_equals(_expected anyelement[], actual[] anyelement, custom_message varchar)

Сравнивает два массива. Массивы считаются равными, если эти массивы имеют одинаковые элементы и размеры массивов равны. Если массивы не равны, то генерируется исключение с текстом #assert_array_equalsn{custom_message}

pgunit.assert_true(_value boolean, _custom_message varchar)

Сравнивает _value c True. Если они не равны, то формируется исключение #assert_truen{custom_message}

pgunit.assert_false(_value boolean, _custom_message varchar)

Сравнивает _value c False. Если они не равны, то формируется исключение #assert_falsen{custom_message}

pgunit.assert_null(_value boolean, _custom_message varchar)

Сравнивает _value c NULL. Если они не равны, то формируется исключение #assert_nulln{custom_message}

pgunit.assert_not_null(_value boolean, _custom_message varchar)

Сравнивает _value c NULL. Если они равны, то формируется исключение #assert_not_nulln{custom_message}

pgunit.fail(_custom_message varchar)

Генерирует исключение с текстом #assert_failn{custom_message}

pgunit.run_test(_sp varchar)

Запускает указанную хранимую процедуру внутри тестовой инфраструктуры. После запуска тестовой процедуры происходит откат данных.

Примеры

Тестирование расчета НДС

Этот тип теста можно отнести к модульному, так как тестируется только математика.

Процедура, которую надо протестировать.

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

Дополнительные материалы

Репозиторий библиотеки тестирования

comments powered by Disqus