Проектирование тестируемого PL/SQL: пакеты, зависимости, моки и стабы
Unit-тесты в Oracle становятся быстрыми, детерминированными и изолированными не только благодаря utPLSQL и правильно настроенной среде, но и благодаря тому, как вы проектируете PL/SQL-код.
В прошлых материалах курса мы уже разобрали:
зачем нужны unit-тесты и как мыслить уровнями тестирования
как подготовить среду: схемы, права, транзакции, NLS и изоляцию
какие инструменты обычно используют: utPLSQL, SQL Developer, CLI/SQLclТеперь сделаем следующий практический шаг: научимся проектировать пакеты и зависимости так, чтобы тесты писать было легко, а не больно.
Полезные источники:
utPLSQL Documentation
utPLSQL GitHub
Oracle Database PL/SQL Language ReferenceЧто в PL/SQL делает код нетестируемым
Большинство проблем с тестированием в БД возникает из-за неявных зависимостей. В PL/SQL они часто скрыты в таких местах:
время: прямой вызов sysdate/systimestamp
контекст: sys_context(...), user, параметры сессии
I/O и внешние системы: HTTP-запросы, очереди, файловая система
доступ к данным: прямые select/insert/update/delete “внутри бизнес-логики”
глобальное состояние: пакетные переменные, которые “помнят” прошлый тест
автономные транзакции, которые оставляют следы даже при rollbackЕсли зависимость не контролируется тестом, тест становится:
нестабильным
медленным
требовательным к окружениюЗначит, задача проектирования тестируемого PL/SQL — сделать зависимости явными и заменяемыми.
Принцип: разделяйте вычисления и взаимодействие с миром
Для unit-тестов идеальна логика, которая зависит только от входных параметров и возвращает результат без побочных эффектов. В Oracle-проектах это редко бывает “чисто”, но к этому можно приблизиться.
Практичная схема разделения:
Доменная логика: вычисления, правила, проверки, маршрутизация.
Инфраструктурные зависимости: время, генерация идентификаторов, доступ к таблицам, внешние вызовы.!Схема показывает, что бизнес-пакет зависит от абстракций, а в тестах зависимости подменяются
Ключевая идея: бизнес-пакет должен зависеть не от “мира”, а от прослоек, которые можно заменить тестовыми двойниками.
Как проектировать пакеты: API, ответственность, границы
Делайте пакет модулем с ясным публичным API
Пакет в Oracle часто является естественной единицей тестирования. Чтобы тесты были устойчивыми:
ограничивайте публичный API тем, что является контрактом
держите детали реализации приватными (в теле пакета)
тестируйте поведение через публичные процедуры/функцииПрактический эффект: тесты меньше ломаются при рефакторинге.
Разделяйте ответственность (внутри БД это особенно важно)
Частая ошибка — пакет “делает всё”: читает таблицы, считает, логирует, ходит во внешний сервис, коммитит.
Удобнее для тестов, когда ответственность разнесена:
_api или _service: бизнес-операции и правила
*_repo: операции чтения/записи в таблицы (data access)
*_gateway: интеграции (HTTP, очереди)
clock: времяЭто не “архитектура ради архитектуры”: это способ управлять зависимостями.
Тестовые двойники: стабы, моки, фейки, шпионы
Термины часто путают, поэтому договоримся о смыслах.
Стаб: упрощённая реализация зависимости, которая возвращает заранее заданные значения.
Фейк: упрощённая, но работающая реализация (например, хранит данные в тестовой таблице или в памяти пакета).
Шпион: двойник, который записывает факты взаимодействия (какие параметры передали, сколько раз вызвали).
Мок: двойник, для которого вы заранее задаёте ожидания вызовов, а затем проверяете, что они выполнены.В PL/SQL чаще всего реально применяют стабы и шпионы, потому что они проще и надёжнее. Моки тоже возможны, но их часто реализуют как “ручные” (через пакетные переменные), а не как магию фреймворка.
Паттерны управления зависимостями в PL/SQL
Ниже — практичные, распространённые способы сделать зависимости заменяемыми.
Передавайте зависимость параметром (лучший вариант для функций)
Это идеальный вариант для детерминизма: зависимость становится частью входа.
Пример: вместо использования sysdate внутри функции передаём дату параметром.
Плюсы:
легко тестировать
нет скрытых зависимостейМинус:
не всегда удобно протаскивать параметр через длинную цепочку вызововКогда “протаскивать” неудобно — используйте следующий паттерн.
Введите провайдер (обёртку) для времени, контекста и прочих системных вызовов
Вместо прямого sysdate делаем пакет clock, который можно подменить.
#### Продовая реализация
#### Использование в бизнес-коде
В тесте вы хотите контролировать “текущее время”. Для этого нужен механизм подмены clock. В Oracle есть несколько вариантов подмены, и выбор зависит от политики среды.
Способы подмены зависимостей в Oracle
Подмена через синонимы (простой, но требует дисциплины)
Идея:
бизнес-код обращается к зависимости по имени clock
в “бою” clock указывает на app.clock
в тестовой схеме можно создать синоним clock на app_test.clock_stubЧто важно учитывать:
это работает, если имена разрешаются через текущую схему/синонимы ожидаемым образом
это требует аккуратной организации схем и прав (см. статью про тестовую среду)Подмена через Edition-Based Redefinition (мощно, но сложнее)
Если в организации используется EBR, можно держать тестовую реализацию зависимости в отдельной edition.
Это более продвинутый подход, обычно применяемый в больших enterprise-системах.
Подмена через конфигурацию в пакете (компромисс)
Идея:
бизнес-код вызывает “точку выбора” внутри пакета
в тесте вы переключаете реализацию на стабПример — “провайдер” времени с возможностью фиксировать дату.
Плюсы:
не требуется сложная инфраструктура
легко начатьМинусы:
появляется глобальное состояние (пакетная переменная)
требуется строгая очистка в teardown, иначе тесты начнут влиять друг на другаЕсли вы используете такой подход, обязательны:
сброс состояния (reset_now) после каждого теста
запрет на параллельный прогон в одной и той же схеме/сессии без изоляцииРазделение доступа к данным: репозитории и минимальные контрактные функции
Чтобы unit-тесты не превращались в интеграционные, бизнес-логика должна как можно меньше “знать” о таблицах.
Частый практический компромисс:
доменная логика отдельно (можно тестировать почти чисто)
доступ к данным через тонкий слой *_repoПример интерфейса репозитория:
В реальности pricing_repo читает таблицу. А для unit-тестов вы можете подменить репозиторий стабом, который возвращает нужные значения.
Внешние интеграции: gateway-пакеты и отказ от прямого HTTP в бизнес-логике
Если ваш PL/SQL вызывает HTTP (через utl_http) или очередь (AQ), unit-тесты быстро становятся хрупкими.
Рекомендуемый подход:
выносите интеграцию в *_gateway
бизнес-код работает с gateway как с зависимостью
в unit-тестах подменяете gateway стабомЭто не только про тесты: это снижает связность и упрощает сопровождение.
Шпионы и проверка взаимодействий
Иногда важно проверить не только результат, но и то, что зависимость была вызвана корректно. Для этого удобно использовать шпион.
Пример: стаб-шпион как пакет, который запоминает последние параметры.
Дальше бизнес-код должен быть устроен так, чтобы вместо реального gateway можно было подключить tax_gateway_spy.
Важно: шпион — это тоже глобальное состояние. Значит, снова требуется жёсткий teardown.
Definer rights и invoker rights: тестируемость и безопасность
Из статьи про тестовую среду вы уже знаете, что права выполнения в Oracle критичны.
Связь с проектированием:
если пакет бизнес-логики работает в режиме definer rights (по умолчанию), тестам обычно достаточно execute на API
если пакет использует invoker rights (authid current_user), то тестовая схема должна иметь объектные права на все используемые таблицы/представленияЭто влияет на архитектуру зависимостей:
при invoker rights становится сложнее “просто вызвать API”, потому что права должны быть у вызывающего
при definer rights проще изолировать права тестов, но нужно внимательнее относиться к безопасности самого APIПрактическая рекомендация для обучения и большинства прикладных модулей:
проектируйте публичный API так, чтобы его можно было тестировать через execute без раздачи широких прав
если invoker rights необходим, фиксируйте зависимости (какие объекты нужны) и выдавайте права минимальноУправление побочными эффектами: коммиты, автономные транзакции, логирование
Для unit-тестов полезно, когда тестируемая операция:
не делает commit внутри (commit лучше держать на границе сценария)
не пишет “вечно” в лог через автономную транзакциюЕсли автономные транзакции нужны (например, аудит), сделайте их зависимостью:
бизнес-логика сообщает “событие”
отдельный модуль решает, писать ли в таблицу, в каком режиме, и можно ли это отключить или подменить в тестеЭто напрямую связано с изоляцией данных и предсказуемостью отката.
Как это стыкуется с utPLSQL
utPLSQL даёт вам:
структуру тестов (suite/test, setup/teardown)
ассёрты (ut.expect(...))
запуск и отчётностьНо utPLSQL не отменяет необходимость проектировать зависимости. Чем лучше вы отделили доменную логику и сделали зависимости подменяемыми, тем больше у вас будет:
настоящих unit-тестов (быстрых и “без базы”)
простых интеграционных тестов (минимальные вставки в таблицы, rollback)Практический чеклист проектирования тестируемого PL/SQL
Публичный API пакета мал и стабилен, приватные детали скрыты в body.
Время, контекст, внешние вызовы не используются напрямую в доменной логике.
Доступ к таблицам вынесен в тонкие *_repo-пакеты.
Для внешних интеграций есть *_gateway.
Есть понятный способ подменить зависимости в тестах (параметры, провайдеры, синонимы, EBR или конфигурация).
Если используются пакетные переменные для тестовых двойников, есть обязательный teardown.
Пакеты не делают неожиданных commit, DDL и автономных транзакций внутри “юнитов”, которые вы хотите тестировать изолированно.Итоги
Тестируемость PL/SQL в Oracle начинается с проектирования: зависимости должны быть явными и подменяемыми.
Самая полезная архитектурная привычка — разделять доменную логику и инфраструктуру (таблицы, время, интеграции).
Тестовые двойники в PL/SQL чаще всего реализуются как стабы и шпионы, которые подключаются через слой зависимостей.
Выбор между definer rights и invoker rights влияет на то, какими правами должен обладать тестовый пользователь.
utPLSQL ускоряет и стандартизирует тестирование, но “магии” не делает: если код завязан на sysdate, контекст и внешние сервисы, тесты будут хрупкими, пока вы не введёте правильные границы.