惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Microsoft Azure Blog
Microsoft Azure Blog
H
Hacker News: Front Page
A
About on SuperTechFans
云风的 BLOG
云风的 BLOG
aimingoo的专栏
aimingoo的专栏
Martin Fowler
Martin Fowler
博客园 - 叶小钗
Last Week in AI
Last Week in AI
Recent Announcements
Recent Announcements
P
Palo Alto Networks Blog
Webroot Blog
Webroot Blog
Hacker News: Ask HN
Hacker News: Ask HN
IT之家
IT之家
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
T
Threat Research - Cisco Blogs
C
CERT Recently Published Vulnerability Notes
Google DeepMind News
Google DeepMind News
Hugging Face - Blog
Hugging Face - Blog
H
Help Net Security
P
Privacy & Cybersecurity Law Blog
C
Cisco Blogs
罗磊的独立博客
The GitHub Blog
The GitHub Blog
M
MIT News - Artificial intelligence
人人都是产品经理
人人都是产品经理
The Cloudflare Blog
Y
Y Combinator Blog
AWS News Blog
AWS News Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
K
Kaspersky official blog
博客园 - 司徒正美
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Security Archives - TechRepublic
Security Archives - TechRepublic
The Last Watchdog
The Last Watchdog
Jina AI
Jina AI
MyScale Blog
MyScale Blog
TaoSecurity Blog
TaoSecurity Blog
大猫的无限游戏
大猫的无限游戏
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
Cisco Talos Blog
Cisco Talos Blog
美团技术团队
T
Tor Project blog
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
博客园 - 【当耐特】
博客园 - 聂微东
V2EX - 技术
V2EX - 技术
I
Intezer
V
Visual Studio Blog
酷 壳 – CoolShell
酷 壳 – CoolShell

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Юнит-тестирование на уровне базы данных PostgreSQL
IBS_habrablo · 2026-05-14 · via Все публикации подряд на Хабре

Время на прочтение9 мин

Охват и читатели8.5K

Юнит-тесты в PostgreSQL, как и в других базах данных, не являются обязательными для CI/CD, но они крайне важны и фактически становятся стандартом. С помощью этих юнит-тестов мы уже нашли и исправили много ошибок в функциях на уровне БД, а также сократили загрузку ручных тестировщиков.

Привет, Хабр! В этой статье мы, старший разработчик Анастасия Цацкина и старший инженер-тестировщик Владимир Белинский из IBS, расскажем о нашем опыте внедрения юнит-тестирования на уровне БД.

Зачем нужно юнит-тестирование функций на уровне БД

Юнит-тестирование — важная часть поддержания качества приложения и процесса разработки. Оно позволяет:

  • Обнаруживать ошибки на ранних этапах. Можно выявлять ошибки до публикации на продуктовые сервера и влияния на другие функции.

  • Предотвратить регрессию. Юнит-тесты значительно облегчают процесс рефакторинга и изменение кода, т. к. легко убедиться в корректности изменений и в том, что изменения не сломали уже работающую функциональность. Это особенно важно в больших командах и при наличии зависимостей между функциями.

  • Оптимизировать производительность. Юнит-тесты позволяют вовремя выявлять деградацию производительности, например, при увеличении объема данных либо при изменении связанной функции.

  • Поддержать CI/CD. Можно реализовать быструю автоматическую проверку качества кода перед его деплоем. 

  • Повышать доверие к системе со стороны пользователей. Можно улучшить общую стабильность и надежность системы. Ошибки не попадают на продуктовые сервера, пользователи с ними не сталкиваются, соответственно, растет их доверие к системе.

Именно для PostgreSQL есть важный нюанс: здесь можно создать абсолютно неработающую функцию, потому что код валидируется в момент исполнения. Без юнит-тестов такая функция может бесконтрольно пройти по всему конвейеру CI/CD и дойти до сервера назначения со всеми вытекающими рисками и проблемами.

При этом юнит-тестирование в сочетании с инструментами CI/CD — это удобно, поскольку запуск тестов можно автоматизировать, настроив систему мониторинга. Так разработчики могут тестировать изменяемые функции при каждом коммите.

Почему юнит-тестирование функций в PostgreSQL внедряется редко?

Юнит-тестирование функции баз данных в целом и в PostgreSQL в частности не так широко распространено, как тестирование приложений на более высоком уровне. Часто связано это с тем, что:

  • У разработчиков баз данных отсутствует опыт в написании юнит-тестов. Тесты как сущность достаточно сильно отличаются от функций с логикой, к которым привыкли разработчики, поэтому внедрение юнит-тестов требует дополнительного обучения, освоения новых инструментов и навыков. 

  • Существует мнение, что юнит-тесты на уровне баз данных — это неэффективно и слишком сложно.

  • Не все понимают, как юнит-тесты могут повысить качество кода приложений и снизить риски.

  • В командах бывают барьеры для внедрения нового или нет отклика на такую идею со стороны руководителя проекта и команды разработки.

  • Если на проекте уже внедрены какие-то инструменты CI/CD, то не все они поддерживают нативную интеграцию с фреймворками для юнит-тирования. PostgreSQL сам по себе тоже может быть препятствием. 

  • Естественно, на написание юнит-тестов тратится определенное время. Не все готовы на это идти.

  • Часто есть фокус на других вариантах тестирования, например интеграционном и ручном. Безусловно, они также важны, у них свои цели и задачи, но они не заменяют юнит-тестов, а скорее дополняют их.

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

Юнит-тесты на нашем проекте

Мы начали внедрение юнит-тестирования с одного из наших проектов. 

В двух словах об архитектуре

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

Хотя проект существует уже 6 лет, он продолжает развиваться и дорабатываться. Команда разработчиков базы данных состоит из пяти человек.

PostgreSQL играет на проекте ключевую роль. Это не только хранилище данных. Вся логика заложена в функции и процедуры PostgreSQL (в общей сложности их около 700).

БД имеет многослойную архитектуру:

  • Самый низкоуровневый слой — слой исходных данных.

  • Бизнес-логика, т.е. сеттеры, которые рассчитывают данные и обновляют буферные таблицы, а также геттеры, извлекающие данные из этих таблиц для конкретных методик. Этот слой извне не видно.

  • Парадный слой, где представлены геттеры, отдающие по запросу данные наружу. Именно к этому слою обращается фронт проекта и внешние пользователи.

  • Самый верхнеуровневый — слой представления данных в удобном виде для бэкенда (JSON-ы, округление, цветовые показатели и т.п.).

Зачем проекту юнит-тесты

На нашем проекте есть тесты, которые работают на уровне Java-бэкенда. Там трудится другая команда, у которой есть свои задачи. Мы пишем код базы данных, и эти тесты закрывают не все наши потребности. 

Мы взялись за внедрение юнит-тестов на этом проекте по нескольким причинам. Во-первых, планировали полноценную настройку CI/CD-конвейера. Без юнит-тестирования после окончания настройки ошибки могли бы бесконтрольно уходить на препрод. Этого допустить мы не могли.

Кроме того, как упомянуто выше, вся бизнес-логика находится у нас в функциях базы данных, при этом реализовано очень много зависимостей между функциями, и эти функции постоянно дорабатываются. А еще у нас есть внешние пользователи, которые обращаются к функциям, часто используя нетипичные для фронта срезы. Это невозможно воспроизвести на фронте, а значит, нужно тестировать после доработки функций отдельно, что усложняет процесс разработки.

Так что юнит-тесты должны были нам серьезно упростить ситуацию.

Как запустили работу

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

Первые тесты для нас разрабатывали коллеги-тестировщики (старший и линейный специалисты), а мы учились подходу, совместно определяя объем тест-плана и работая над кодом функции юнит-теста. Со временем стали писать тесты сами — командой в три разработчика и одного старшего тестировщика. Разработчики создают тест-планы для юнит-тестов и сами тесты, а ревью проводит старший тестировщик.

Конечно, поначалу написание тестов отнимало много времени. Но сейчас пришли к тому, что среднее время разработки одного теста — около 2 часов. Если тестируется семейство более-менее похожих функций, то дело идет быстрее. Если же мы берем в разработку новую функцию, тест для нее создается медленнее. Однако в среднем это 2 часа, что намного меньше создания непосредственно тестируемой функции.

Промежуточные итоги

Внедрение еще идет, но по первым этапам можно подвести промежуточные итоги.

Сейчас у нас написано 37 юнит-тестов, в которых выполняется более 700 только задокументированных проверок. По факту их больше, поскольку вместе с проверкой на условия мы, как правило, проверяем наличие данных по нужному срезу. Пока мы тестируем геттеры, чуть позже перейдем и к сеттерам.

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

Естественно, мы заметили, что количество инцидентов по тестируемым функциям сократилось. Но самый главный эффект, который мы для себя видим, — ошибки не попадают на прод. Мы запускаем юнит-тесты перед релизом и уверены, что функция остается стабильной — мы уверены во внесенных изменениях. 

Какие используем инструменты

Для проекта мы выбрали простой в установке и использовании фреймворк PLPG Unit, разработанный как раз для решения подобных задач в PostgreSQL. Он не имеет дополнительных зависимостей и основан на языке plpgsql. Фреймворк свободно распространяется, а его лицензия допускает доработку функционала.

Вот несколько функций из этого фреймворка для примера:

  • assert.ok(text) — размещается в конце тела тестовой функции, чтобы сообщить, что тест прошел успешно;

  • assert.is_equal(IN have anyelement, IN want anyelement, OUT message text, OUT result boolean) — сравнивает два первых входных параметра и возвращает FAIL, если они не равны;

  • assert.is_less_than(IN x anyelement, IN y anyelement, OUT message text, OUT result boolean) — сравнивает два первых параметра и возвращает FAIL, если первый более или равен второму.

Хотя фреймворк PLPG Unit довольно примитивен, в этом и заключается одно из основных его преимуществ для нашей команды. С точки зрения разработчика PostgreSQL, это просто обычная функция — не нужно дополнительного обучения или навыков, чтобы им пользоваться.

Наша методология

  • Разработка юнит-теста начинается с тест-плана, который оформляется в Confluence. Это своего рода спецификация тестирующей функции.

  • Мы договариваемся не добавлять все возможные проверки сразу, чтобы был заметный прогресс в разработке юнит-тестов (иначе разработка одного теста будет занимать слишком много времени).

  • Фичи юнит-тестов, добавляемые в последующие версии, не портируются в предыдущие юнит-тесты (также в целях ускорения разработки).

  • Тестируем влияние входных параметров функций на результирующую таблицу, которую возвращает функция, и соответствие выходных параметров входным параметрам.

  • В основном мы производим позитивное тестирование. Но сейчас уже начали добавлять по несколько негативных проверок в каждый юнит-тест. Например, вызываем функцию с пустыми входными атрибутами — при этом она должна оставаться стабильной и не падать.

  • При проектировании автоматизированных проверок юнит-тестов мы опираемся на принципы тест-дизайна.

Пример юнит-теста

Юнит-тест — это простая функция на языке SQL, которая не имеет параметров и возвращает значение типа test_result. Вызывать тестовую функцию можно вручную, а можно пакетом (для этого предусмотрена функция фреймворка).

Пример теста:

CREATE OR REPLACE FUNCTION unit_tests.example2() 
RETURNS test_result
AS
$$
DECLARE 
  message test_result;
  result boolean;
  have integer;
  want integer;
BEGIN
want := 100;
SELECT 50 + 49 INTO have;

SELECT * FROM assert.is_equal
(have, want) INTO message, result;
--Test failed.
IF result = false THEN
RETURN message;
END IF;
--Test passed.
SELECT assert.ok('End of test.') INTO message; 
RETURN message; 
END
$$
LANGUAGE plpgsql;



SELECT * FROM unit_tests.example2();

В первой строке указано имя функции. Далее мы описываем тип возвращаемого значения и объявляем несколько переменных. После begin задаются ожидаемый и фактический результаты. Функция assert.is_equal их сравнивает.

В данном примере ожидаемый и фактический результат заведомо не равны — мы используем этот пример для демонстраций того, как юнит-тест отработает ошибку. Когда тест падает (result = FALSE), тест выдает сообщение message.

Запуск и отчетность

При ручном запуске данного примера мы получаем:

ASSERT FAILED : ASSERT IS_EQUAL FAILED.
Have -> 99
Want -> 100

В случае запуска тестов пакетом дополнительно указывается название тестирующей функции:

3. unit_tests.example2() --> ASSERT IS_EQUAL FAILED.
Have -> 99
Want -> 100

Результат сохраняется в таблицу для последующей отчетности. В ней есть информация о том, когда прогон был запущен, когда завершился, сколько тестов было запущено, сколько из них прошло или упало. У нас есть также вторая таблица с результатами. Она показывает информацию о каждом отдельном юнит-тесте — наименование функции, статус, данные о причинах падения.

Жизненный цикл тестов

Юнит-тестирование вполне допускает подход TDD (Test-Driven Design или разработка через тестирование). Юнит-тест можно написать на SQL еще до того, как была создана сама тестируемая функция (которая также пишется на SQL). Запустить тест можно уже после написания первой версии тестируемой функции. Впоследствии можно перезапускать его каждый раз после обновления функции (в качестве регрессионного теста).

В этом случае цикл жизни теста выглядит следующим образом:

  • Red (тест падает);

  • Green (тест прошел);

  • Refactor (сохраняем работоспособность кода).

Внутри нашего рабочего пространства мы создали раздел юнит-тестирования, где тестирующие функции группируем по смысловым группам.

Разработку новых тестов мы начинаем с тест-плана и создаем его сразу в Confluence. Пытались вести документацию в Word, но идея уже показала свою несостоятельность. Когда команда состоит из нескольких разработчиков, это крайне неудобно.

Тест-кейсы мы также группируем по смыслу. Сначала идут основные проверки, справедливые для любого вызова тестируемой функции, а потом — специальные проверки, для каждой из которых нужен собственный вызов тестируемой функции.

Функции тестов пишем на языке plpgsql. В начале теста всегда идет метакомментарий, где мы указываем ссылку на постановку, пример вызова и историю изменений.

У нас предусмотрены внутренние требования к неймингу тестирующей функции, возвращаемым значениям и т.п.:

  • тестирующая функция должна иметь имя: f_test_имя_тестируемой_функции();

  • функции юнит-тестов должны возвращать тип значения test_result. По этому типу функция unit_tests.begin() находит все функции тестов в базе данных;

  • функции юнит-тестов не имеют аргументов.

Для удобства работы в функцию пакетного запуска unit_tests.begin() мы добавили дополнительный входной атрибут schema_name, чтобы запускать тесты конкретной схемы.

Как было отмечено выше, запускать можно один тест по имени:

select test_schema.test_function();

все тесты через select:
select unit_tests.begin();

Если функции тестов содержат команды DML, то вызов unit_tests.begin() выполняем в транзакции с последующим откатом.

Пример скрипта, который позволяет выбрать все тесты, запущенные сегодня: 

select t.*, d.*
from unit_tests.tests t
join unit_tests.test_details d
 on d.test_id = t.test_id
where started_on > current_date;

Дальнейшие планы

  • Естественно, мы бы хотели покрыть юнит-тестами все запланированные на данный момент 183 функции (их общее количество — величина переменная).

  • Параллельно у нас идет настройка CI/CD с сервера разработки на препрод. Планируем завершить ее в ближайшее время.

  • Изначально мы не углубляемся в проверки — закрываем юнит-тестами самые критичные моменты. В будущем необходимо увеличить количество проверок.

  • Хотим добавить проверки времени выполнения функций.

  • Нужно вернуться — оптимизировать и отрефакторить уже написанные тесты.

  • Планируем формировать культуру, где качество кода и тестирование являются приоритетными задачами для всей команды. Юнит-тесты в этом помогают. К тому же они служат дополнительной документацией с вариантами использования функции, что тоже немаловажно.